Port the design tokens and the planning primitives
Brings in the token set from the Claude Design export: warm neutrals, semantic status colours, and the twelve-hue categorical poste palette, in light and dark. Declared through Tailwind's @theme so utilities compile to var(--color-*) and follow the theme without per-component branching. The poste palette derives every hue from one formula with an alternating lightness step, so neighbouring hues never land at the same lightness. ShiftChip always prints the poste code alongside the fill: colour carries meaning in the grid, and a grid has to stay readable in print, in greyscale, and to someone who confuses red and green. Week counters live in src/domain/counters rather than in the component, because the same numbers will feed the hours report and the payroll export later. Durations are whole minutes: decimal hours drift by a minute in ways that are invisible on screen and wrong on a payslip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
This commit is contained in:
10 files changed
+856
No files matched your search
@@ -0,0 +1,43 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { cx } from '@/lib/cx';
|
||||
|
||||
export type Tone = 'neutral' | 'accent' | 'ok' | 'warn' | 'danger' | 'info';
|
||||
|
||||
const TONES: Record<Tone, string> = {
|
||||
neutral: 'bg-surface-3 text-ink-2 border-line-2',
|
||||
accent: 'bg-accent-soft text-accent-soft-ink border-accent',
|
||||
ok: 'bg-ok-soft text-ok-soft-ink border-ok',
|
||||
warn: 'bg-warn-soft text-warn-soft-ink border-warn',
|
||||
danger: 'bg-danger-soft text-danger-soft-ink border-danger',
|
||||
info: 'bg-info-soft text-info-soft-ink border-info',
|
||||
};
|
||||
|
||||
export interface BadgeProps {
|
||||
tone?: Tone;
|
||||
/** Sans liseré : pour les compteurs discrets de la navigation. */
|
||||
subtle?: boolean;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** Pastille compacte. Porte un état ou un décompte, jamais une action. */
|
||||
export function Badge({
|
||||
tone = 'neutral',
|
||||
subtle = false,
|
||||
children,
|
||||
className,
|
||||
}: BadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={cx(
|
||||
'inline-flex shrink-0 items-center rounded-full px-2 py-px text-micro font-semibold',
|
||||
subtle ? 'border-transparent' : 'border',
|
||||
TONES[tone],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
import { cx } from '@/lib/cx';
|
||||
|
||||
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||
export type ButtonSize = 'sm' | 'md';
|
||||
|
||||
const VARIANTS: Record<ButtonVariant, string> = {
|
||||
primary:
|
||||
'border-accent bg-accent text-accent-ink hover:bg-accent-hover active:bg-accent-press',
|
||||
secondary:
|
||||
'border-line-3 bg-surface text-ink-1 hover:bg-surface-2 active:bg-surface-3',
|
||||
ghost:
|
||||
'border-transparent bg-transparent text-ink-2 hover:bg-surface-2 hover:text-ink-1',
|
||||
danger:
|
||||
'border-danger bg-danger text-danger-ink hover:bg-danger-hover active:bg-danger-hover',
|
||||
};
|
||||
|
||||
const SIZES: Record<ButtonSize, string> = {
|
||||
sm: 'h-[26px] px-2.5 text-xs',
|
||||
md: 'h-8 px-3.5 text-sm',
|
||||
};
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Button({
|
||||
variant = 'secondary',
|
||||
size = 'md',
|
||||
className,
|
||||
type = 'button',
|
||||
...rest
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
className={cx(
|
||||
'inline-flex shrink-0 items-center justify-center gap-1.5 rounded-2 border font-medium',
|
||||
'cursor-pointer transition-colors duration-[var(--d-1)] ease-organic',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
VARIANTS[variant],
|
||||
SIZES[size],
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { cx } from '@/lib/cx';
|
||||
|
||||
export interface CardProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Card({ children, className }: CardProps) {
|
||||
return (
|
||||
<section
|
||||
className={cx(
|
||||
'overflow-hidden rounded-3 border border-line-1 bg-surface',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export interface CardHeaderProps {
|
||||
title: ReactNode;
|
||||
/** Décompte ou statut affiché contre le titre. */
|
||||
badge?: ReactNode;
|
||||
action?: ReactNode;
|
||||
}
|
||||
|
||||
export function CardHeader({ title, badge, action }: CardHeaderProps) {
|
||||
return (
|
||||
<header className="flex items-center gap-2.5 border-b border-line-1 px-4 py-3">
|
||||
<h2 className="text-md font-semibold">{title}</h2>
|
||||
{badge}
|
||||
<span className="flex-1" />
|
||||
{action}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export interface EmptyStateProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function EmptyState({ title, description, className }: EmptyStateProps) {
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'flex flex-col items-center gap-2.5 px-6 py-16 text-center',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="size-9 rounded-2 border-[1.5px] border-line-3 bg-surface-2"
|
||||
/>
|
||||
<p className="text-md font-semibold">{title}</p>
|
||||
{description ? (
|
||||
<p className="max-w-[46ch] text-sm leading-[var(--lh-prose)] text-ink-2">
|
||||
{description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user