chore init
This commit is contained in:
1 parent
1b7406b1d3
commit
a0d312a884
60 files changed
+5046
-125
No files matched your search
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>© {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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user