Start time grid #62
@ -1,5 +1,5 @@
|
|||||||
import { BrowserRouter, Route, Routes } from "react-router";
|
import { BrowserRouter, Route, Routes } from "react-router";
|
||||||
import ComponentLibrary from "./studentportalen/ComponentLibrary.tsx";
|
import ComponentLibrary from "./studentportalen/ComponentLibrary/ComponentLibrary";
|
||||||
import Layout from "./studentportalen/Layout.tsx";
|
import Layout from "./studentportalen/Layout.tsx";
|
||||||
|
|
||||||
export default function Studentportalen() {
|
export default function Studentportalen() {
|
||||||
|
|||||||
225
frontend/src/components/Dropdown/Dropdown.tsx
Normal file
225
frontend/src/components/Dropdown/Dropdown.tsx
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import { useState, useId, type SelectHTMLAttributes } from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { ChevronDownIcon } from "../Icon/Icon";
|
||||||
|
|
||||||
|
export type DropdownSize = "sm" | "md" | "lg";
|
||||||
|
|
||||||
|
export interface DropdownProps<T>
|
||||||
|
extends Omit<
|
||||||
|
SelectHTMLAttributes<HTMLSelectElement>,
|
||||||
|
"size" | "onChange" | "value" | "defaultValue"
|
||||||
|
> {
|
||||||
|
options: T[];
|
||||||
|
getOptionValue: (option: T) => string;
|
||||||
|
getOptionLabel: (option: T) => string;
|
||||||
|
size?: DropdownSize;
|
||||||
|
error?: boolean;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
customWidth?: string;
|
||||||
|
label: string;
|
||||||
|
hideLabel?: boolean;
|
||||||
|
message?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
/** Controlled mode - parent manages state */
|
||||||
|
value?: string;
|
||||||
|
/** Uncontrolled mode - component manages state with initial value */
|
||||||
|
defaultValue?: string;
|
||||||
|
onChange?: (value: string, option: T | undefined) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapperSizeClasses: Record<DropdownSize, string> = {
|
||||||
|
sm: clsx("h-(--control-height-sm)", "rounded-(--border-radius-sm)"),
|
||||||
|
md: clsx("h-(--control-height-md)", "rounded-(--border-radius-sm)"),
|
||||||
|
lg: clsx("h-(--control-height-lg)", "rounded-(--border-radius-md)"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const textSizeClasses: Record<DropdownSize, string> = {
|
||||||
|
sm: "body-normal-md",
|
||||||
|
md: "body-normal-md",
|
||||||
|
lg: "body-normal-lg",
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconContainerSizeClasses: Record<DropdownSize, string> = {
|
||||||
|
sm: clsx("w-(--control-height-sm) h-(--control-height-sm)"),
|
||||||
|
md: clsx("w-(--control-height-md) h-(--control-height-md)"),
|
||||||
|
lg: clsx("w-(--control-height-lg) h-(--control-height-lg)"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseClasses =
|
||||||
|
"relative inline-flex items-center bg-base-canvas border-[length:var(--border-width-sm)] border-base-ink-medium min-w-[110px]";
|
||||||
|
|
||||||
|
const defaultStateClasses = clsx(
|
||||||
|
"hover:border-base-ink-placeholder",
|
||||||
|
"focus-within:border-primary",
|
||||||
|
"focus-within:outline focus-within:outline-sky-100 focus-within:outline-[length:var(--border-width-lg)]",
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorStateClasses = clsx(
|
||||||
|
"border-fire-100",
|
||||||
|
"outline outline-fire-100 outline-[length:var(--border-width-sm)]",
|
||||||
|
"focus-within:border-primary",
|
||||||
|
"focus-within:outline focus-within:outline-sky-100 focus-within:outline-[length:var(--border-width-lg)]",
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function Dropdown<T>({
|
||||||
|
options,
|
||||||
|
getOptionValue,
|
||||||
|
getOptionLabel,
|
||||||
|
size = "md",
|
||||||
|
error = false,
|
||||||
|
fullWidth = false,
|
||||||
|
customWidth,
|
||||||
|
label,
|
||||||
|
hideLabel = false,
|
||||||
|
message,
|
||||||
|
placeholder,
|
||||||
|
className = "",
|
||||||
|
value,
|
||||||
|
defaultValue,
|
||||||
|
onChange,
|
||||||
|
...props
|
||||||
|
}: DropdownProps<T>) {
|
||||||
|
const selectId = useId();
|
||||||
|
const isControlled = value !== undefined;
|
||||||
|
const [internalValue, setInternalValue] = useState(defaultValue ?? "");
|
||||||
|
const currentValue = isControlled ? value : internalValue;
|
||||||
|
|
||||||
|
const selectedOption = options.find(
|
||||||
|
(o) => getOptionValue(o) === currentValue,
|
||||||
|
);
|
||||||
|
const selectedLabel = selectedOption
|
||||||
|
? getOptionLabel(selectedOption)
|
||||||
|
: placeholder || "";
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
if (!isControlled) {
|
||||||
|
setInternalValue(newValue);
|
||||||
|
}
|
||||||
|
const option = options.find((o) => getOptionValue(o) === newValue);
|
||||||
|
onChange?.(newValue, option);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Derived values
|
||||||
|
const hasValue = currentValue !== "";
|
||||||
|
const useFixedWidth = fullWidth || customWidth;
|
||||||
|
const widthStyle = customWidth ? { width: customWidth } : undefined;
|
||||||
|
const stateClasses = error ? errorStateClasses : defaultStateClasses;
|
||||||
|
const showVisibleLabel = !hideLabel;
|
||||||
|
|
||||||
|
// Find longest label for auto-width sizing, otherwise the dropdown will
|
||||||
|
// resize when different options are selected
|
||||||
|
const allLabels = [
|
||||||
|
placeholder,
|
||||||
|
...options.map((o) => getOptionLabel(o)),
|
||||||
|
].filter(Boolean);
|
||||||
|
const longestLabel = allLabels.reduce(
|
||||||
|
(a, b) => (a!.length > b!.length ? a : b),
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Invisible element that sets minimum width based on longest option
|
||||||
|
const autoWidthSizer = !useFixedWidth && (
|
||||||
|
<span
|
||||||
|
className={clsx("invisible pl-(--padding-md)", textSizeClasses[size])}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{longestLabel}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Shows current selection or placeholder
|
||||||
|
const displayLabel = (
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
textSizeClasses[size],
|
||||||
|
useFixedWidth
|
||||||
|
? "flex-1 pl-(--padding-md)"
|
||||||
|
: "absolute left-0 pl-(--padding-md)",
|
||||||
|
hasValue ? "text-base-ink-strong" : "text-base-ink-placeholder",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selectedLabel}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dropdown arrow icon
|
||||||
|
const chevron = (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"flex items-center justify-center shrink-0 text-base-ink-placeholder pointer-events-none",
|
||||||
|
iconContainerSizeClasses[size],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon size={size} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Native select (invisible, handles interaction)
|
||||||
|
const selectElement = (
|
||||||
|
<select
|
||||||
|
id={selectId}
|
||||||
|
aria-label={hideLabel ? label : undefined}
|
||||||
|
className="absolute inset-0 opacity-0 cursor-pointer"
|
||||||
|
value={currentValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{placeholder && (
|
||||||
|
<option value="" disabled>
|
||||||
|
{placeholder}
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
{options.map((o) => (
|
||||||
|
<option key={getOptionValue(o)} value={getOptionValue(o)}>
|
||||||
|
{getOptionLabel(o)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
|
||||||
|
// The styled dropdown control
|
||||||
|
const dropdownField = (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
baseClasses,
|
||||||
|
wrapperSizeClasses[size],
|
||||||
|
stateClasses,
|
||||||
|
fullWidth && "w-full",
|
||||||
|
!fullWidth && !customWidth && "w-fit",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={widthStyle}
|
||||||
|
>
|
||||||
|
{autoWidthSizer}
|
||||||
|
{displayLabel}
|
||||||
|
{chevron}
|
||||||
|
{selectElement}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!showVisibleLabel && !message) {
|
||||||
|
return dropdownField;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col gap-(--spacing-sm)",
|
||||||
|
fullWidth && "w-full",
|
||||||
|
!fullWidth && !customWidth && "w-fit",
|
||||||
|
)}
|
||||||
|
style={widthStyle}
|
||||||
|
>
|
||||||
|
{showVisibleLabel && (
|
||||||
|
<label htmlFor={selectId} className="body-bold-md text-base-ink-strong">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{dropdownField}
|
||||||
|
{message && (
|
||||||
|
<span className="body-light-sm text-base-ink-strong">{message}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -94,3 +94,19 @@ export function CheckmarkIcon({
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ChevronDownIcon({
|
||||||
|
size = "inherit",
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={clsx(iconSizeClasses[size], className)}
|
||||||
|
{...baseSvgProps}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M6 9l6 6 6-6" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,460 +0,0 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
import Button from "../components/Button/Button";
|
|
||||||
import TextInput from "../components/TextInput/TextInput";
|
|
||||||
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() {
|
|
||||||
const [darkMode, setDarkMode] = useState(() => {
|
|
||||||
return document.documentElement.classList.contains("dark");
|
|
||||||
});
|
|
||||||
const [selectedPerson, setSelectedPerson] = useState<string>("");
|
|
||||||
const [selectedPeople, setSelectedPeople] = useState<string[]>([]);
|
|
||||||
const [participants, setParticipants] = useState<string[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (darkMode) {
|
|
||||||
document.documentElement.classList.add("dark");
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove("dark");
|
|
||||||
}
|
|
||||||
}, [darkMode]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1>Component Library</h1>
|
|
||||||
|
|
||||||
<section className="mt-lg">
|
|
||||||
<h2 className="mb-md">Dark Mode</h2>
|
|
||||||
<Button variant="primary" onClick={() => setDarkMode(!darkMode)}>
|
|
||||||
{darkMode ? "Light Mode" : "Dark Mode"}
|
|
||||||
</Button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="mt-lg">
|
|
||||||
<h2 className="mb-md">Button Variants</h2>
|
|
||||||
<div className="flex flex-wrap gap-md">
|
|
||||||
<Button variant="primary">Primary</Button>
|
|
||||||
<Button variant="secondary">Secondary</Button>
|
|
||||||
<Button variant="red">Red</Button>
|
|
||||||
<Button variant="green">Green</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="mt-lg">
|
|
||||||
<h2 className="mb-md">Button Sizes</h2>
|
|
||||||
<div className="flex flex-wrap items-center gap-md">
|
|
||||||
<Button size="sm">Small</Button>
|
|
||||||
<Button size="md">Medium</Button>
|
|
||||||
<Button size="lg">Large</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section className="mt-lg">
|
|
||||||
<h2 className="mb-md">Text Input Sizes</h2>
|
|
||||||
<div className="flex flex-wrap items-center gap-md">
|
|
||||||
<TextInput
|
|
||||||
size="sm"
|
|
||||||
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>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="mt-lg">
|
|
||||||
<h2 className="mb-md">Text Input with Icon</h2>
|
|
||||||
<div className="flex flex-wrap items-center gap-md">
|
|
||||||
<TextInput
|
|
||||||
size="sm"
|
|
||||||
placeholder="Small with icon"
|
|
||||||
Icon={SearchIcon}
|
|
||||||
label="Small search"
|
|
||||||
hideLabel
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
size="md"
|
|
||||||
placeholder="Medium with icon"
|
|
||||||
Icon={SearchIcon}
|
|
||||||
label="Medium search"
|
|
||||||
hideLabel
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
size="lg"
|
|
||||||
placeholder="Large with icon"
|
|
||||||
Icon={SearchIcon}
|
|
||||||
label="Large search"
|
|
||||||
hideLabel
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="mt-lg">
|
|
||||||
<h2 className="mb-md">Text Input States</h2>
|
|
||||||
<div className="flex flex-wrap items-center gap-md">
|
|
||||||
<TextInput placeholder="Default" label="Default state" hideLabel />
|
|
||||||
<TextInput
|
|
||||||
placeholder="Error state"
|
|
||||||
error
|
|
||||||
label="Error state"
|
|
||||||
hideLabel
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="mt-lg">
|
|
||||||
<h2 className="mb-md">Text Input With/Without Placeholder</h2>
|
|
||||||
<div className="flex flex-wrap items-center gap-md">
|
|
||||||
<TextInput
|
|
||||||
placeholder="Placeholder"
|
|
||||||
label="With placeholder"
|
|
||||||
hideLabel
|
|
||||||
/>
|
|
||||||
<TextInput label="Without placeholder" hideLabel />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="mt-lg">
|
|
||||||
<h2 className="mb-md">Text Input Width Options</h2>
|
|
||||||
<div className="flex flex-col gap-md">
|
|
||||||
<TextInput
|
|
||||||
placeholder="Full width"
|
|
||||||
fullWidth
|
|
||||||
label="Full width input"
|
|
||||||
hideLabel
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
placeholder="Custom width"
|
|
||||||
customWidth="300px"
|
|
||||||
label="Custom width input"
|
|
||||||
hideLabel
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="mt-lg">
|
|
||||||
<h2 className="mb-md">Text Input with Label</h2>
|
|
||||||
<div className="flex flex-wrap items-start gap-md">
|
|
||||||
<TextInput label="Email" placeholder="Enter your email" />
|
|
||||||
<TextInput label="Password" placeholder="Enter password" error />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="mt-lg">
|
|
||||||
<h2 className="mb-md">Text Input with Label and Message</h2>
|
|
||||||
<div className="flex flex-wrap items-start gap-md">
|
|
||||||
<TextInput
|
|
||||||
label="Email"
|
|
||||||
placeholder="Enter your email"
|
|
||||||
error
|
|
||||||
message="This field is required"
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label="Username"
|
|
||||||
placeholder="Choose a username"
|
|
||||||
error
|
|
||||||
message="Must be at least 3 characters"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</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 />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import Button from "../../components/Button/Button";
|
||||||
|
|
||||||
|
export default function ButtonSection() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section>
|
||||||
|
<h2 className="mb-md">Button Variants</h2>
|
||||||
|
<div className="flex flex-wrap gap-md">
|
||||||
|
<Button variant="primary">Primary</Button>
|
||||||
|
<Button variant="secondary">Secondary</Button>
|
||||||
|
<Button variant="red">Red</Button>
|
||||||
|
<Button variant="green">Green</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-lg">
|
||||||
|
<h2 className="mb-md">Button Sizes</h2>
|
||||||
|
<div className="flex flex-wrap items-center gap-md">
|
||||||
|
<Button size="sm">Small</Button>
|
||||||
|
<Button size="md">Medium</Button>
|
||||||
|
<Button size="lg">Large</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,125 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import Combobox from "../../components/Combobox/Combobox";
|
||||||
|
import {
|
||||||
|
peopleOptions,
|
||||||
|
getPersonValue,
|
||||||
|
getPersonLabel,
|
||||||
|
getPersonSubtitle,
|
||||||
|
} from "./data";
|
||||||
|
|
||||||
|
export default function ComboboxSection() {
|
||||||
|
const [selectedPerson, setSelectedPerson] = useState<string>("");
|
||||||
|
const [selectedPeople, setSelectedPeople] = useState<string[]>([]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import Sidebar, { type ComponentCategory } from "./Sidebar";
|
||||||
|
import ButtonSection from "./ButtonSection";
|
||||||
|
import TextInputSection from "./TextInputSection";
|
||||||
|
import DropdownSection from "./DropdownSection";
|
||||||
|
import ListItemSection from "./ListItemSection";
|
||||||
|
import SearchResultListSection from "./SearchResultListSection";
|
||||||
|
import ComboboxSection from "./ComboboxSection";
|
||||||
|
import ListCardSection from "./ListCardSection";
|
||||||
|
import ParticipantPickerSection from "./ParticipantPickerSection";
|
||||||
|
|
||||||
|
export default function ComponentLibrary() {
|
||||||
|
const [darkMode, setDarkMode] = useState(() => {
|
||||||
|
return document.documentElement.classList.contains("dark");
|
||||||
|
});
|
||||||
|
const [selectedCategory, setSelectedCategory] =
|
||||||
|
useState<ComponentCategory>("Button");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (darkMode) {
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove("dark");
|
||||||
|
}
|
||||||
|
}, [darkMode]);
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
switch (selectedCategory) {
|
||||||
|
case "Button":
|
||||||
|
return <ButtonSection />;
|
||||||
|
case "TextInput":
|
||||||
|
return <TextInputSection />;
|
||||||
|
case "Dropdown":
|
||||||
|
return <DropdownSection />;
|
||||||
|
case "ListItem":
|
||||||
|
return <ListItemSection />;
|
||||||
|
case "SearchResultList":
|
||||||
|
return <SearchResultListSection />;
|
||||||
|
case "Combobox":
|
||||||
|
return <ComboboxSection />;
|
||||||
|
case "ListCard":
|
||||||
|
return <ListCardSection />;
|
||||||
|
case "ParticipantPicker":
|
||||||
|
return <ParticipantPickerSection />;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 min-h-0">
|
||||||
|
<Sidebar
|
||||||
|
selectedCategory={selectedCategory}
|
||||||
|
onSelectCategory={setSelectedCategory}
|
||||||
|
darkMode={darkMode}
|
||||||
|
onToggleDarkMode={() => setDarkMode(!darkMode)}
|
||||||
|
/>
|
||||||
|
<main className="flex-1 overflow-y-auto p-lg">
|
||||||
|
<h1 className="mb-lg">{selectedCategory}</h1>
|
||||||
|
{renderContent()}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,137 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import Dropdown from "../../components/Dropdown/Dropdown";
|
||||||
|
import { peopleOptions, getPersonValue, getPersonLabel } from "./data";
|
||||||
|
|
||||||
|
export default function DropdownSection() {
|
||||||
|
const [selectedPerson, setSelectedPerson] = useState<string>("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section>
|
||||||
|
<h2 className="mb-md">Dropdown Sizes</h2>
|
||||||
|
<div className="flex flex-wrap items-center gap-md">
|
||||||
|
<Dropdown
|
||||||
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
placeholder="Small"
|
||||||
|
size="sm"
|
||||||
|
label="Small dropdown"
|
||||||
|
hideLabel
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
placeholder="Medium"
|
||||||
|
size="md"
|
||||||
|
label="Medium dropdown"
|
||||||
|
hideLabel
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
placeholder="Large"
|
||||||
|
size="lg"
|
||||||
|
label="Large dropdown"
|
||||||
|
hideLabel
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-lg">
|
||||||
|
<h2 className="mb-md">Dropdown States</h2>
|
||||||
|
<div className="flex flex-wrap items-center gap-md">
|
||||||
|
<Dropdown
|
||||||
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
placeholder="Default"
|
||||||
|
label="Default state"
|
||||||
|
hideLabel
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
placeholder="Error state"
|
||||||
|
error
|
||||||
|
label="Error state"
|
||||||
|
hideLabel
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-lg">
|
||||||
|
<h2 className="mb-md">Dropdown with Label</h2>
|
||||||
|
<div className="flex flex-wrap items-start gap-md">
|
||||||
|
<Dropdown
|
||||||
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
placeholder="Select person"
|
||||||
|
label="Person"
|
||||||
|
value={selectedPerson}
|
||||||
|
onChange={(value) => setSelectedPerson(value)}
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
placeholder="Select person"
|
||||||
|
label="Person (error)"
|
||||||
|
error
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-lg">
|
||||||
|
<h2 className="mb-md">Dropdown with Label and Message</h2>
|
||||||
|
<div className="flex flex-wrap items-start gap-md">
|
||||||
|
<Dropdown
|
||||||
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
placeholder="Select person"
|
||||||
|
label="Person"
|
||||||
|
message="Please select a person"
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
placeholder="Select person"
|
||||||
|
label="Person"
|
||||||
|
error
|
||||||
|
message="This field is required"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-lg">
|
||||||
|
<h2 className="mb-md">Dropdown Width Options</h2>
|
||||||
|
<div className="flex flex-col gap-md">
|
||||||
|
<Dropdown
|
||||||
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
placeholder="Full width"
|
||||||
|
fullWidth
|
||||||
|
label="Full width dropdown"
|
||||||
|
hideLabel
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
placeholder="Custom width"
|
||||||
|
customWidth="300px"
|
||||||
|
label="Custom width dropdown"
|
||||||
|
hideLabel
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import ListCard from "../../components/ListCard/ListCard";
|
||||||
|
|
||||||
|
export default function ListCardSection() {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import ListItem from "../../components/ListItem/ListItem";
|
||||||
|
|
||||||
|
export default function ListItemSection() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,101 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import ParticipantPicker from "../../components/ParticipantPicker/ParticipantPicker";
|
||||||
|
import {
|
||||||
|
peopleOptions,
|
||||||
|
getPersonValue,
|
||||||
|
getPersonLabel,
|
||||||
|
getPersonSubtitle,
|
||||||
|
} from "./data";
|
||||||
|
|
||||||
|
export default function ParticipantPickerSection() {
|
||||||
|
const [participants, setParticipants] = useState<string[]>([]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import SearchResultList from "../../components/SearchResultList/SearchResultList";
|
||||||
|
import {
|
||||||
|
peopleOptions,
|
||||||
|
getPersonValue,
|
||||||
|
getPersonLabel,
|
||||||
|
getPersonSubtitle,
|
||||||
|
} from "./data";
|
||||||
|
|
||||||
|
export default function SearchResultListSection() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
57
frontend/src/studentportalen/ComponentLibrary/Sidebar.tsx
Normal file
57
frontend/src/studentportalen/ComponentLibrary/Sidebar.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import Button from "../../components/Button/Button";
|
||||||
|
|
||||||
|
export const componentCategories = [
|
||||||
|
"Button",
|
||||||
|
"TextInput",
|
||||||
|
"Dropdown",
|
||||||
|
"ListItem",
|
||||||
|
"SearchResultList",
|
||||||
|
"Combobox",
|
||||||
|
"ListCard",
|
||||||
|
"ParticipantPicker",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type ComponentCategory = (typeof componentCategories)[number];
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
selectedCategory: ComponentCategory;
|
||||||
|
onSelectCategory: (category: ComponentCategory) => void;
|
||||||
|
darkMode: boolean;
|
||||||
|
onToggleDarkMode: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Sidebar({
|
||||||
|
selectedCategory,
|
||||||
|
onSelectCategory,
|
||||||
|
darkMode,
|
||||||
|
onToggleDarkMode,
|
||||||
|
}: SidebarProps) {
|
||||||
|
return (
|
||||||
|
<nav className="w-64 shrink-0 border-r border-base-ink-soft bg-base-canvas p-md overflow-y-auto">
|
||||||
|
<h1 className="mb-md">Components</h1>
|
||||||
|
<div className="mb-lg">
|
||||||
|
<Button variant="secondary" size="sm" onClick={onToggleDarkMode}>
|
||||||
|
{darkMode ? "Light Mode" : "Dark Mode"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ul className="flex flex-col gap-xs">
|
||||||
|
{componentCategories.map((category) => (
|
||||||
|
<li key={category}>
|
||||||
|
<button
|
||||||
|
onClick={() => onSelectCategory(category)}
|
||||||
|
className={clsx(
|
||||||
|
"w-full text-left px-md py-sm rounded-(--border-radius-sm) body-normal-md cursor-pointer",
|
||||||
|
selectedCategory === category
|
||||||
|
? "bg-primary text-base-canvas"
|
||||||
|
: "text-base-ink-strong hover:bg-base-ink-soft",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,128 @@
|
|||||||
|
import TextInput from "../../components/TextInput/TextInput";
|
||||||
|
import { SearchIcon } from "../../components/Icon/Icon";
|
||||||
|
|
||||||
|
export default function TextInputSection() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section>
|
||||||
|
<h2 className="mb-md">Text Input Sizes</h2>
|
||||||
|
<div className="flex flex-wrap items-center gap-md">
|
||||||
|
<TextInput
|
||||||
|
size="sm"
|
||||||
|
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>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-lg">
|
||||||
|
<h2 className="mb-md">Text Input with Icon</h2>
|
||||||
|
<div className="flex flex-wrap items-center gap-md">
|
||||||
|
<TextInput
|
||||||
|
size="sm"
|
||||||
|
placeholder="Small with icon"
|
||||||
|
Icon={SearchIcon}
|
||||||
|
label="Small search"
|
||||||
|
hideLabel
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
size="md"
|
||||||
|
placeholder="Medium with icon"
|
||||||
|
Icon={SearchIcon}
|
||||||
|
label="Medium search"
|
||||||
|
hideLabel
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
size="lg"
|
||||||
|
placeholder="Large with icon"
|
||||||
|
Icon={SearchIcon}
|
||||||
|
label="Large search"
|
||||||
|
hideLabel
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-lg">
|
||||||
|
<h2 className="mb-md">Text Input States</h2>
|
||||||
|
<div className="flex flex-wrap items-center gap-md">
|
||||||
|
<TextInput placeholder="Default" label="Default state" hideLabel />
|
||||||
|
<TextInput
|
||||||
|
placeholder="Error state"
|
||||||
|
error
|
||||||
|
label="Error state"
|
||||||
|
hideLabel
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-lg">
|
||||||
|
<h2 className="mb-md">Text Input With/Without Placeholder</h2>
|
||||||
|
<div className="flex flex-wrap items-center gap-md">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Placeholder"
|
||||||
|
label="With placeholder"
|
||||||
|
hideLabel
|
||||||
|
/>
|
||||||
|
<TextInput label="Without placeholder" hideLabel />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-lg">
|
||||||
|
<h2 className="mb-md">Text Input Width Options</h2>
|
||||||
|
<div className="flex flex-col gap-md">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Full width"
|
||||||
|
fullWidth
|
||||||
|
label="Full width input"
|
||||||
|
hideLabel
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Custom width"
|
||||||
|
customWidth="300px"
|
||||||
|
label="Custom width input"
|
||||||
|
hideLabel
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-lg">
|
||||||
|
<h2 className="mb-md">Text Input with Label</h2>
|
||||||
|
<div className="flex flex-wrap items-start gap-md">
|
||||||
|
<TextInput label="Email" placeholder="Enter your email" />
|
||||||
|
<TextInput label="Password" placeholder="Enter password" error />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-lg">
|
||||||
|
<h2 className="mb-md">Text Input with Label and Message</h2>
|
||||||
|
<div className="flex flex-wrap items-start gap-md">
|
||||||
|
<TextInput
|
||||||
|
label="Email"
|
||||||
|
placeholder="Enter your email"
|
||||||
|
error
|
||||||
|
message="This field is required"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Username"
|
||||||
|
placeholder="Choose a username"
|
||||||
|
error
|
||||||
|
message="Must be at least 3 characters"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
frontend/src/studentportalen/ComponentLibrary/data.ts
Normal file
18
frontend/src/studentportalen/ComponentLibrary/data.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export interface Person {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
subtitle: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getPersonValue = (person: Person) => person.value;
|
||||||
|
export const getPersonLabel = (person: Person) => person.label;
|
||||||
|
export const getPersonSubtitle = (person: Person) => person.subtitle;
|
||||||
@ -1,5 +1,6 @@
|
|||||||
#layout {
|
#layout {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
padding-bottom: 4em;
|
||||||
}
|
}
|
||||||
main {
|
main {
|
||||||
max-width: var(--max-page-width);
|
max-width: var(--max-page-width);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user