Start time grid #62

Merged
stne3960 merged 30 commits from start_time_grid into main 2026-01-16 14:17:09 +01:00
16 changed files with 979 additions and 461 deletions
Showing only changes of commit 70d25ac20a - Show all commits

View File

@ -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() {

View 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>
);
}

View File

@ -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>
);
}

View File

@ -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 />
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View 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>
);
}

View File

@ -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>
</>
);
}

View 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;

View File

@ -1,5 +1,6 @@
#layout {
min-height: 100vh;
padding-bottom: 4em;
}
main {
max-width: var(--max-page-width);