Add the application shell, Aperçu and the week grid

Ports the shell and Aperçu screen from the Claude Design export, and
builds the week grid from the design system's grid specification.

The theme is applied by a nonced inline script before first paint.
Without it the page renders light and then swaps, which is a white
flash for anyone working in dark. The nonce our CSP already emits is
what makes an inline script possible at all.

Demo data lives in src/lib/demo behind a README stating it is fictional
and replaceable. Screens import from there and nowhere else, so the
directory can be deleted wholesale once real queries land.

Weekend columns are shaded because in retail those are the days that
carry counterparts — mayor-authorised Sundays and their compensatory
rest — and scheduling one by accident is the failure worth preventing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
This commit is contained in:
Claude committed 2026-08-07 22:12:23 +00:00
1 parent 8c0cfd3b0c
commit a24c1b821c
14 files changed
+1301 -5

No files matched your search

+158
View File
@@ -0,0 +1,158 @@
import { AbsenceBar } from '@/components/planning/AbsenceBar';
import { CounterStrip } from '@/components/planning/CounterStrip';
import { ShiftChip } from '@/components/planning/ShiftChip';
import { computeWeekCounters } from '@/domain/counters/week';
import { cx } from '@/lib/cx';
import {
fullName,
initials,
timeRange,
type DemoWeekRow,
} from '@/lib/demo/types';
export interface WeekGridProps {
days: readonly string[];
rows: DemoWeekRow[];
unassignedRow?: DemoWeekRow;
}
/**
* Grille hebdomadaire : lignes salariés × sept jours.
*
* Le samedi et le dimanche portent un fond distinct. Ce n'est pas décoratif :
* en commerce de détail, ce sont les jours à contreparties — dimanche du maire
* majoré et repos compensateur — et les distinguer d'un coup d'œil évite de les
* planifier par inadvertance.
*/
export function WeekGrid({ days, rows, unassignedRow }: WeekGridProps) {
return (
<div className="overflow-x-auto rounded-3 border border-line-1 bg-surface">
<div className="min-w-[1040px]">
<GridHeader days={days} />
{unassignedRow ? <Row row={unassignedRow} days={days} /> : null}
{rows.map((row) => (
<Row key={row.employee.id} row={row} days={days} />
))}
</div>
</div>
);
}
function GridHeader({ days }: { days: readonly string[] }) {
return (
<div className="sticky top-0 z-20 flex border-b border-line-2 bg-surface-2">
<div className="w-64 flex-none border-r border-line-1 px-3 py-2 text-micro font-semibold tracking-[0.06em] text-ink-3 uppercase">
Salarié
</div>
<div className="grid flex-1 grid-cols-7">
{days.map((day, index) => (
<div
key={day}
className={cx(
'border-r border-line-1 px-2 py-2 text-center text-xs font-semibold last:border-r-0',
index > 4 ? 'bg-surface-3 text-ink-2' : 'text-ink-1',
)}
>
{day}
</div>
))}
</div>
</div>
);
}
function Row({ row, days }: { row: DemoWeekRow; days: readonly string[] }) {
const counters = computeWeekCounters(row.counters);
const name = row.unassigned ? 'Non assigné' : fullName(row.employee);
return (
<div
className={cx(
'flex border-b border-line-1 last:border-b-0',
row.unassigned && 'bg-surface-2',
)}
>
<div className="w-64 flex-none border-r border-line-1 px-3 py-2">
<div className="flex items-center gap-2">
{row.unassigned ? (
<span
aria-hidden
className="flex size-6 items-center justify-center rounded-full border border-dashed border-line-3 text-micro text-ink-3"
>
?
</span>
) : (
<span
aria-hidden
className="flex size-6 items-center justify-center rounded-full bg-surface-3 text-micro font-semibold text-ink-2"
>
{initials(row.employee)}
</span>
)}
<span className="min-w-0">
<span
className={cx(
'block truncate text-sm font-medium',
row.unassigned ? 'text-ink-3' : 'text-ink-1',
)}
>
{name}
</span>
<span className="block truncate text-micro text-ink-3">
{row.employee.job}
</span>
</span>
</div>
<CounterStrip
counters={counters}
forfaitJours={row.employee.forfaitJours ?? false}
className="mt-1"
/>
</div>
<div className="relative grid flex-1 grid-cols-7">
{row.absence ? (
<AbsenceBar
kind={row.absence.kind}
label={row.absence.label}
duration={`${row.absence.span} j`}
startDay={row.absence.startDay}
span={row.absence.span}
className="top-1.5"
/>
) : null}
{days.map((day, index) => {
const shifts = row.days[index] ?? [];
return (
<div
key={day}
className={cx(
'flex min-h-[var(--row-h)] flex-col gap-1 border-r border-line-1 p-1 last:border-r-0',
index > 4 && 'bg-surface-2',
row.absence &&
index >= row.absence.startDay &&
index < row.absence.startDay + row.absence.span &&
'pt-7',
)}
>
{shifts.map((shift, position) => (
<ShiftChip
key={`${day}-${position}`}
poste={shift.poste}
time={timeRange(shift)}
state={shift.state ?? 'published'}
{...(shift.delta ? { delta: shift.delta } : {})}
{...(shift.alert ? { alert: shift.alert } : {})}
{...(shift.breakMinutes
? { breakMinutes: shift.breakMinutes }
: {})}
/>
))}
</div>
);
})}
</div>
</div>
);
}