Start time grid #62
@ -1,5 +1,5 @@
|
||||
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";
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 4em;
|
||||
}
|
||||
main {
|
||||
max-width: var(--max-page-width);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user