Text input component #30
733
frontend/package-lock.json
generated
733
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,10 +12,12 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"openapi-fetch": "^0.13.5",
|
"openapi-fetch": "^0.13.5",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router": "^7.4.1"
|
"react-router": "^7.4.1",
|
||||||
|
"tailwindcss": "^4.1.16"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.21.0",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { BrowserRouter, Route, Routes } from "react-router";
|
import { BrowserRouter, Route, Routes } from "react-router";
|
||||||
import Home from "./studentportalen/Home.tsx";
|
import ComponentLibrary from "./studentportalen/ComponentLibrary.tsx";
|
||||||
import Layout from "./studentportalen/Layout.tsx";
|
import Layout from "./studentportalen/Layout.tsx";
|
||||||
|
|
||||||
export default function Studentportalen() {
|
export default function Studentportalen() {
|
||||||
@ -8,7 +8,8 @@ export default function Studentportalen() {
|
|||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<Layout />}>
|
<Route element={<Layout />}>
|
||||||
<Route index element={<Home />} />
|
<Route index element={<ComponentLibrary />} />
|
||||||
|
<Route path="components" element={<ComponentLibrary />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
BIN
frontend/src/assets/TheSansB-W2ExtraLight.woff2
Normal file
BIN
frontend/src/assets/TheSansB-W2ExtraLight.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/TheSansB-W2ExtraLightItalic.woff2
Normal file
BIN
frontend/src/assets/TheSansB-W2ExtraLightItalic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/TheSansB-W3Light.woff2
Normal file
BIN
frontend/src/assets/TheSansB-W3Light.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/TheSansB-W3LightItalic.woff2
Normal file
BIN
frontend/src/assets/TheSansB-W3LightItalic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/TheSansB-W4SemiLight.woff2
Normal file
BIN
frontend/src/assets/TheSansB-W4SemiLight.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/TheSansB-W4SemiLightItalic.woff2
Normal file
BIN
frontend/src/assets/TheSansB-W4SemiLightItalic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/TheSansB-W5Plain.woff2
Normal file
BIN
frontend/src/assets/TheSansB-W5Plain.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/TheSansB-W5PlainItalic.woff2
Normal file
BIN
frontend/src/assets/TheSansB-W5PlainItalic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/TheSansB-W6SemiBold.woff2
Normal file
BIN
frontend/src/assets/TheSansB-W6SemiBold.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/TheSansB-W6SemiBoldItalic.woff2
Normal file
BIN
frontend/src/assets/TheSansB-W6SemiBoldItalic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/TheSansB-W7Bold.woff2
Normal file
BIN
frontend/src/assets/TheSansB-W7Bold.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/TheSansB-W7BoldItalic.woff2
Normal file
BIN
frontend/src/assets/TheSansB-W7BoldItalic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/TheSansB-W8ExtraBold.woff2
Normal file
BIN
frontend/src/assets/TheSansB-W8ExtraBold.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/TheSansB-W8ExtraBoldItalic.woff2
Normal file
BIN
frontend/src/assets/TheSansB-W8ExtraBoldItalic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/TheSansB-W9Black.woff2
Normal file
BIN
frontend/src/assets/TheSansB-W9Black.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/TheSansB-W9BlackItalic.woff2
Normal file
BIN
frontend/src/assets/TheSansB-W9BlackItalic.woff2
Normal file
Binary file not shown.
57
frontend/src/components/Button/Button.tsx
Normal file
57
frontend/src/components/Button/Button.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
||||||
|
|
||||||
|
export type ButtonVariant = "primary" | "secondary" | "red" | "green";
|
||||||
|
export type ButtonSize = "sm" | "md" | "lg";
|
||||||
|
|
||||||
|
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantClasses: Record<ButtonVariant, string> = {
|
||||||
|
primary:
|
||||||
|
"bg-primary text-base-canvas border border-primary hover:bg-secondary hover:text-base-canvas hover:border hover:border-primary focus-visible:bg-base-canvas focus-visible:text-base-ink-strong focus-visible:border focus-visible:border-primary focus-visible:outline focus-visible:outline-[length:var(--border-width-lg)] focus-visible:outline-sky-100",
|
||||||
|
secondary:
|
||||||
|
"bg-base-canvas text-base-ink-strong border-solid [border-width:var(--border-width-sm)] border-base-ink-soft hover:bg-base-canvas hover:text-base-ink-strong hover:border-base-ink-medium focus-visible:bg-base-canvas focus-visible:text-base-ink-strong focus-visible:border-primary focus-visible:outline focus-visible:outline-[length:var(--border-width-lg)] focus-visible:outline-sky-100",
|
||||||
|
red: "bg-other-red-100 text-su-white focus-visible:bg-base-canvas focus-visible:text-base-ink-strong focus-visible:border-primary focus-visible:border focus-visible:outline focus-visible:outline-[length:var(--border-width-lg)] focus-visible:outline-sky-100",
|
||||||
|
green:
|
||||||
|
"bg-other-green text-su-white focus-visible:bg-base-canvas focus-visible:text-base-ink-strong focus-visible:border-primary focus-visible:border focus-visible:outline focus-visible:outline-[length:var(--border-width-lg)] focus-visible:outline-sky-100",
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeClasses: Record<ButtonSize, string> = {
|
||||||
|
sm: "h-(--control-height-sm) min-w-(--button-min-width-sm) px-(--button-padding-x-sm) body-bold-md rounded-(--border-radius-sm)",
|
||||||
|
md: "h-(--control-height-md) min-w-(--button-min-width-md) px-(--button-padding-x-md) body-bold-md rounded-(--border-radius-sm)",
|
||||||
|
lg: "h-(--control-height-lg) min-w-(--button-min-width-lg) px-(--button-padding-x-lg) body-bold-lg rounded-(--border-radius-md)",
|
||||||
|
};
|
||||||
|
|
||||||
|
const textPaddingClasses: Record<ButtonSize, string> = {
|
||||||
|
sm: "px-(--button-text-padding-x-sm)",
|
||||||
|
md: "px-(--button-text-padding-x-md)",
|
||||||
|
lg: "px-(--button-text-padding-x-lg)",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Button({
|
||||||
|
variant = "primary",
|
||||||
|
size = "md",
|
||||||
|
className = "",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ButtonProps) {
|
||||||
|
const baseClasses = "inline-flex items-center justify-center cursor-pointer";
|
||||||
|
|
||||||
|
const classes = [
|
||||||
|
baseClasses,
|
||||||
|
variantClasses[variant],
|
||||||
|
sizeClasses[size],
|
||||||
|
className,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={classes} {...props}>
|
||||||
|
<span className={textPaddingClasses[size]}>{children}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
113
frontend/src/components/TextInput/TextInput.tsx
Normal file
113
frontend/src/components/TextInput/TextInput.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import type { InputHTMLAttributes, ReactNode, CSSProperties } from 'react';
|
||||||
|
// isValidElement: checks if something is a React element (e.g., <svg>, <MyComponent />)
|
||||||
|
// cloneElement: creates a copy of a React element with modified/additional props
|
||||||
|
import { cloneElement, isValidElement } from 'react';
|
||||||
|
|
||||||
|
export type TextInputSize = 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
|
// Omit<... 'size'> removes the native 'size' attribute from input elements so we can use our own
|
||||||
|
export interface TextInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
||||||
|
size?: TextInputSize;
|
||||||
|
icon?: ReactNode;
|
||||||
|
error?: boolean;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
customWidth?: string;
|
||||||
|
label?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const heightClasses: Record<TextInputSize, string> = {
|
||||||
|
sm: 'h-(--control-height-sm)',
|
||||||
|
md: 'h-(--control-height-md)',
|
||||||
|
lg: 'h-(--control-height-lg)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const widthClasses: Record<TextInputSize, string> = {
|
||||||
|
sm: 'w-(--text-input-default-width-md)',
|
||||||
|
md: 'w-(--text-input-default-width-md)',
|
||||||
|
lg: 'w-(--text-input-default-width-lg)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const radiusClasses: Record<TextInputSize, string> = {
|
||||||
|
sm: 'rounded-(--border-radius-sm)',
|
||||||
|
md: 'rounded-(--border-radius-sm)',
|
||||||
|
lg: 'rounded-(--border-radius-md)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const textClasses: Record<TextInputSize, string> = {
|
||||||
|
sm: 'body-normal-md',
|
||||||
|
md: 'body-normal-md',
|
||||||
|
lg: 'body-normal-lg',
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconContainerStyles: Record<TextInputSize, CSSProperties> = {
|
||||||
|
sm: { width: 'var(--control-height-sm)', height: 'var(--control-height-sm)' },
|
||||||
|
md: { width: 'var(--control-height-md)', height: 'var(--control-height-md)' },
|
||||||
|
lg: { width: 'var(--control-height-lg)', height: 'var(--control-height-lg)' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconStyles: Record<TextInputSize, CSSProperties> = {
|
||||||
|
sm: { width: 'var(--font-size-body-md)', height: 'var(--font-size-body-md)' },
|
||||||
|
md: { width: 'var(--font-size-body-md)', height: 'var(--font-size-body-md)' },
|
||||||
|
lg: { width: 'var(--font-size-body-lg)', height: 'var(--font-size-body-lg)' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseClasses = 'bg-base-canvas border-[length:var(--border-width-sm)] border-base-ink-medium';
|
||||||
|
|
||||||
|
// focus-within: applies styles when any child element (the input) has focus
|
||||||
|
const defaultStateClasses =
|
||||||
|
'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 =
|
||||||
|
'border-fire-100 outline outline-fire-100 outline-[length:var(--border-width-sm)] focus-within:border-primary focus-within:outline focus-within:outline-[length:var(--border-width-lg)] focus-within:outline-sky-100';
|
||||||
|
|
||||||
|
export default function TextInput({
|
||||||
|
size = 'md',
|
||||||
|
icon,
|
||||||
|
error = false,
|
||||||
|
fullWidth = false,
|
||||||
|
customWidth,
|
||||||
|
label,
|
||||||
|
message,
|
||||||
|
className = '',
|
||||||
|
...props
|
||||||
|
}: TextInputProps) {
|
||||||
|
const widthClass = fullWidth ? 'w-full' : widthClasses[size];
|
||||||
|
const widthStyle = customWidth ? { width: customWidth } : undefined;
|
||||||
|
const stateClasses = error ? errorStateClasses : defaultStateClasses;
|
||||||
|
const inputPadding = icon ? 'pr-(--padding-md)' : 'px-(--padding-md)';
|
||||||
|
|
||||||
|
const inputField = (
|
||||||
|
<div
|
||||||
|
className={`flex items-center ${baseClasses} ${heightClasses[size]} ${widthClass} ${radiusClasses[size]} ${stateClasses} ${className}`}
|
||||||
|
style={widthStyle}
|
||||||
|
>
|
||||||
|
{icon && (
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center shrink-0 text-base-ink-placeholder"
|
||||||
|
style={iconContainerStyles[size]}
|
||||||
|
>
|
||||||
|
{isValidElement<{ style?: CSSProperties }>(icon)
|
||||||
|
? cloneElement(icon, { style: iconStyles[size] })
|
||||||
|
: icon}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
className={`flex-1 min-w-0 h-full bg-transparent border-none outline-none text-base-ink-max placeholder:text-base-ink-placeholder ${inputPadding} ${textClasses[size]}`}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!label && !message) {
|
||||||
|
return inputField;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-(--spacing-sm)">
|
||||||
|
{label && <label className="body-bold-md text-base-ink-strong">{label}</label>}
|
||||||
|
{inputField}
|
||||||
|
{message && <span className="body-light-sm text-base-ink-strong">{message}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,11 +1,222 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* TheSans Font Family */
|
||||||
|
@font-face {
|
||||||
|
font-family: "TheSans";
|
||||||
|
src: url("./assets/TheSansB-W5Plain.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "TheSans";
|
||||||
|
src: url("./assets/TheSansB-W5PlainItalic.woff2") format("woff2");
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "TheSans Light";
|
||||||
|
src: url("./assets/TheSansB-W3Light.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "TheSans Light";
|
||||||
|
src: url("./assets/TheSansB-W3LightItalic.woff2") format("woff2");
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "TheSans SemiLight";
|
||||||
|
src: url("./assets/TheSansB-W4SemiLight.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "TheSans SemiLight";
|
||||||
|
src: url("./assets/TheSansB-W4SemiLightItalic.woff2") format("woff2");
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "TheSans Plain";
|
||||||
|
src: url("./assets/TheSansB-W5Plain.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "TheSans Plain";
|
||||||
|
src: url("./assets/TheSansB-W5PlainItalic.woff2") format("woff2");
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "TheSans SemiBold";
|
||||||
|
src: url("./assets/TheSansB-W6SemiBold.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "TheSans SemiBold";
|
||||||
|
src: url("./assets/TheSansB-W6SemiBoldItalic.woff2") format("woff2");
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
/* Colors */
|
||||||
|
--color-primary: #05305d;
|
||||||
|
--color-base-canvas: #ffffff;
|
||||||
|
--color-secondary: #34587f;
|
||||||
|
--color-sky-100: #b0dee4;
|
||||||
|
--color-sky-70: #c7e8ed;
|
||||||
|
--color-sky-35: #e4f4f7;
|
||||||
|
--color-sky-20: #eff9fa;
|
||||||
|
--color-base-ink-max: #000000;
|
||||||
|
--color-base-ink-strong: #4b4b4b;
|
||||||
|
--color-base-ink-medium: #bababa;
|
||||||
|
--color-base-ink-soft: #dadada;
|
||||||
|
--color-base-ink-placeholder: #757575;
|
||||||
|
--color-other-red-100: #aa1227;
|
||||||
|
--color-other-red-10: #f6e6e8;
|
||||||
|
--color-other-green: #539848;
|
||||||
|
--color-su-white: #ffffff;
|
||||||
|
--color-fire-100: #eb7124;
|
||||||
|
--color-fire-70: #f19b66;
|
||||||
|
--color-fire-35: #f8cdb4;
|
||||||
|
--color-fire-20: #fbe2d3;
|
||||||
|
|
||||||
|
/* Font sizes */
|
||||||
|
--font-size-body-md: 16px;
|
||||||
|
--font-size-body-lg: 18px;
|
||||||
|
|
||||||
|
/* Border radius */
|
||||||
|
--border-radius-sm: 3px;
|
||||||
|
--border-radius-md: 4px;
|
||||||
|
--border-radius-lg: 6px;
|
||||||
|
--border-radius-xl: 8px;
|
||||||
|
|
||||||
|
/* Border width */
|
||||||
|
--border-width-sm: 1px;
|
||||||
|
--border-width-lg: 3px;
|
||||||
|
|
||||||
|
/* Padding */
|
||||||
|
--padding-md: 12px;
|
||||||
|
--padding-lg: 24px;
|
||||||
|
--padding-xl: 48px;
|
||||||
|
|
||||||
|
/* Spacing */
|
||||||
|
--spacing-sm: 8px;
|
||||||
|
--spacing-md: 12px;
|
||||||
|
--spacing-lg: 24px;
|
||||||
|
--spacing-xl: 32px;
|
||||||
|
|
||||||
|
/* Control heights */
|
||||||
|
--control-height-sm: 32px;
|
||||||
|
--control-height-md: 40px;
|
||||||
|
--control-height-lg: 48px;
|
||||||
|
|
||||||
|
/* Button padding x */
|
||||||
|
--button-padding-x-sm: 6px;
|
||||||
|
--button-padding-x-md: 10px;
|
||||||
|
--button-padding-x-lg: 14px;
|
||||||
|
|
||||||
|
/* Button min width */
|
||||||
|
--button-min-width-sm: 72px;
|
||||||
|
--button-min-width-md: 72px;
|
||||||
|
--button-min-width-lg: 84px;
|
||||||
|
|
||||||
|
/* Button text padding x */
|
||||||
|
--button-text-padding-x-sm: 6px;
|
||||||
|
--button-text-padding-x-md: 6px;
|
||||||
|
--button-text-padding-x-lg: 6px;
|
||||||
|
|
||||||
|
/* Text input default width */
|
||||||
|
--text-input-default-width-md: 194px;
|
||||||
|
--text-input-default-width-lg: 218px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--color-primary: #ffffff;
|
||||||
|
--color-base-canvas: #000000;
|
||||||
|
--color-secondary: #d9d6d6;
|
||||||
|
--color-sky-100: #403d3d;
|
||||||
|
--color-sky-70: #2d2b2b;
|
||||||
|
--color-sky-35: #1f1e1e;
|
||||||
|
--color-sky-20: #141414;
|
||||||
|
--color-base-ink-max: #ffffff;
|
||||||
|
--color-base-ink-strong: #ffffff;
|
||||||
|
--color-base-ink-medium: #636363;
|
||||||
|
--color-base-ink-soft: #555555;
|
||||||
|
--color-base-ink-placeholder: #959595;
|
||||||
|
--color-other-red-100: #aa1227;
|
||||||
|
--color-other-red-10: #f6e6e8;
|
||||||
|
--color-other-green: #539848;
|
||||||
|
--color-su-white: #ffffff;
|
||||||
|
--color-fire-100: #eb7124;
|
||||||
|
--color-fire-70: #f19b66;
|
||||||
|
--color-fire-35: #f8cdb4;
|
||||||
|
--color-fire-20: #fbe2d3;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--color-su-primary: #002f5f;
|
--color-su-primary: #002f5f;
|
||||||
--color-su-primary-80: #33587f;
|
--color-su-primary-80: #33587f;
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
--bottom-nav-height: 4.5rem;
|
||||||
|
font-family: 'TheSans', system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Text styles - Body */
|
||||||
|
.body-light-sm {
|
||||||
|
font-family: "TheSans Light", "TheSans", system-ui, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-normal-md {
|
||||||
|
font-family: "TheSans SemiLight", "TheSans", system-ui, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-normal-lg {
|
||||||
|
font-family: 'TheSans SemiLight', 'TheSans', system-ui, sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-semibold-md {
|
||||||
|
font-family: 'TheSans Plain', 'TheSans', system-ui, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-semibold-lg {
|
||||||
|
font-family: 'TheSans Plain', 'TheSans', system-ui, sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-bold-md {
|
||||||
|
font-family: 'TheSans SemiBold', 'TheSans', system-ui, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-bold-lg {
|
||||||
|
font-family: 'TheSans SemiBold', 'TheSans', system-ui, sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text styles - Heading */
|
||||||
|
.heading-semibold-lg {
|
||||||
|
font-family: 'TheSans SemiBold', 'TheSans', system-ui, sans-serif;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
background-color: #141414;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|||||||
150
frontend/src/studentportalen/ComponentLibrary.tsx
Normal file
150
frontend/src/studentportalen/ComponentLibrary.tsx
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import Button from '../components/Button/Button';
|
||||||
|
import TextInput from '../components/TextInput/TextInput';
|
||||||
|
|
||||||
|
export default function ComponentLibrary() {
|
||||||
|
const [darkMode, setDarkMode] = useState(() => {
|
||||||
|
return document.documentElement.classList.contains('dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
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" />
|
||||||
|
<TextInput size="md" placeholder="Medium" />
|
||||||
|
<TextInput size="lg" placeholder="Large" />
|
||||||
|
</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={
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
size="md"
|
||||||
|
placeholder="Medium with icon"
|
||||||
|
icon={
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
size="lg"
|
||||||
|
placeholder="Large with icon"
|
||||||
|
icon={
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</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" />
|
||||||
|
<TextInput placeholder="Error state" error />
|
||||||
|
</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" />
|
||||||
|
<TextInput />
|
||||||
|
</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 />
|
||||||
|
<TextInput placeholder="Custom width" customWidth="300px" />
|
||||||
|
</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>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from 'vite';
|
||||||
import react from "@vitejs/plugin-react-swc";
|
import react from '@vitejs/plugin-react-swc';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react(), tailwindcss()],
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user