chore init

This commit is contained in:
devcodetools committed 2026-09-16 21:34:09 +02:00
1 parent 1b7406b1d3
commit a0d312a884
60 files changed
+5046 -125

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
import type { ButtonHTMLAttributes } from "react";
type ButtonVariant = "primary" | "ghost" | "danger";
type ButtonSize = "sm" | "md";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
}
const variantClasses: Record<ButtonVariant, string> = {
primary: "bg-accent text-white hover:bg-accent/90",
ghost: "border border-border bg-white text-ink hover:bg-paper",
danger: "bg-danger text-white hover:bg-danger/90",
};
const sizeClasses: Record<ButtonSize, string> = {
sm: "rounded-sm px-2 py-1 text-xs",
md: "rounded-md px-3 py-2 text-sm",
};
export function Button({
variant = "primary",
size = "md",
className = "",
type = "button",
...props
}: ButtonProps) {
return (
<button
type={type}
className={`inline-flex items-center justify-center gap-1.5 font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent disabled:cursor-not-allowed disabled:opacity-50 ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
{...props}
/>
);
}
+20
View File
@@ -0,0 +1,20 @@
import Link from "next/link";
export function Footer() {
const year = new Date().getFullYear();
return (
<footer className="print-hidden border-t border-border bg-white px-6 py-4 text-center text-sm text-muted">
<span>&copy; {year} CV-Maker</span>
<span aria-hidden className="mx-2 text-border">
•
</span>
<Link
href="/mentions-legales"
className="font-medium underline-offset-2 hover:text-accent hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
>
Mentions légales
</Link>
</footer>
);
}
+25
View File
@@ -0,0 +1,25 @@
import type { ButtonHTMLAttributes } from "react";
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
"aria-label": string;
danger?: boolean;
}
export function IconButton({
"aria-label": ariaLabel,
danger = false,
type = "button",
className = "",
...props
}: IconButtonProps) {
return (
<button
type={type}
aria-label={ariaLabel}
className={`inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent disabled:opacity-50 ${
danger ? "text-danger hover:bg-danger/10" : "text-muted hover:bg-paper hover:text-ink"
} ${className}`}
{...props}
/>
);
}
+56
View File
@@ -0,0 +1,56 @@
"use client";
import { type KeyboardEvent } from "react";
export interface TabItem<T extends string> {
id: T;
label: string;
}
interface TabsProps<T extends string> {
items: TabItem<T>[];
activeId: T;
onChange: (id: T) => void;
}
export function Tabs<T extends string>({ items, activeId, onChange }: TabsProps<T>) {
function handleKeyDown(event: KeyboardEvent<HTMLButtonElement>) {
const currentIndex = items.findIndex((item) => item.id === activeId);
if (event.key === "ArrowRight") {
event.preventDefault();
const next = items.at((currentIndex + 1) % items.length);
if (next) onChange(next.id);
} else if (event.key === "ArrowLeft") {
event.preventDefault();
const prev = items.at((currentIndex - 1 + items.length) % items.length);
if (prev) onChange(prev.id);
}
}
return (
<div role="tablist" aria-label="Sections du CV" className="flex flex-wrap gap-1">
{items.map((item) => {
const selected = item.id === activeId;
return (
<button
key={item.id}
role="tab"
id={`tab-${item.id}`}
aria-selected={selected}
aria-controls={`panel-${item.id}`}
tabIndex={selected ? 0 : -1}
onClick={() => onChange(item.id)}
onKeyDown={handleKeyDown}
className={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent ${
selected
? "bg-accent text-white"
: "border border-border bg-white text-muted hover:text-ink"
}`}
>
{item.label}
</button>
);
})}
</div>
);
}
+11
View File
@@ -0,0 +1,11 @@
import type { TextareaHTMLAttributes } from "react";
export function Textarea({ className = "", ...props }: TextareaHTMLAttributes<HTMLTextAreaElement>) {
return (
<textarea
suppressHydrationWarning
className={`w-full resize-y rounded-md border border-border bg-white p-3 font-mono text-sm text-ink placeholder:text-muted focus:outline-2 focus:outline-offset-2 focus:outline-accent ${className}`}
{...props}
/>
);
}