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:
14 files changed
+1301
-5
No files matched your search
@@ -0,0 +1,7 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { AppShell } from '@/components/shell/AppShell';
|
||||
|
||||
export default function AppLayout({ children }: { children: ReactNode }) {
|
||||
return <AppShell>{children}</AppShell>;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { PageBody, PageHeader } from '@/components/shell/PageHeader';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Card, CardHeader } from '@/components/ui/Card';
|
||||
import { ENTRIES, TILES, TODOS } from '@/lib/demo/apercu';
|
||||
|
||||
export const metadata = { title: 'Aperçu RH · PlanFlow' };
|
||||
|
||||
const TILE_SWATCH: Record<string, string> = {
|
||||
warn: 'bg-warn-soft border-warn',
|
||||
info: 'bg-info-soft border-info',
|
||||
accent: 'bg-accent-soft border-accent',
|
||||
danger: 'bg-danger-soft border-danger',
|
||||
};
|
||||
|
||||
export default function ApercuPage() {
|
||||
return (
|
||||
<PageBody>
|
||||
<PageHeader
|
||||
title="Aperçu RH"
|
||||
subtitle="Vos tâches en cours sur Nantes Atlantis · lundi 10 août 2026"
|
||||
actions={
|
||||
<>
|
||||
<Button>Exporter</Button>
|
||||
<Button variant="primary">Nouvelle embauche</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="grid gap-2.5 [grid-template-columns:repeat(auto-fit,minmax(232px,1fr))]">
|
||||
{TILES.map((tile) => (
|
||||
<button
|
||||
key={tile.label}
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center gap-3 rounded-3 border border-line-1 bg-surface px-4 py-3.5 text-left transition-[border-color,box-shadow] duration-[var(--d-1)] ease-organic hover:border-line-3 hover:shadow-e1"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={`size-8 flex-none rounded-2 border ${TILE_SWATCH[tile.tone] ?? ''}`}
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-xs text-ink-2">
|
||||
{tile.label}
|
||||
</span>
|
||||
<span className="tnum block text-xl leading-tight font-semibold">
|
||||
{tile.value}
|
||||
</span>
|
||||
</span>
|
||||
<span aria-hidden className="flex-none text-ink-3">
|
||||
›
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 [grid-template-columns:repeat(auto-fit,minmax(380px,1fr))]">
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Entrées à venir"
|
||||
badge={<Badge tone="accent">4</Badge>}
|
||||
action={
|
||||
<Button size="sm" variant="ghost">
|
||||
Voir tout
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<ul>
|
||||
{ENTRIES.map((entry) => (
|
||||
<li
|
||||
key={entry.name}
|
||||
className="flex items-center gap-3 border-b border-line-1 px-4 py-2.5 last:border-b-0 hover:bg-surface-2"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="flex size-7 flex-none items-center justify-center rounded-full bg-surface-3 text-micro font-semibold text-ink-2"
|
||||
>
|
||||
{entry.initials}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-medium">
|
||||
{entry.name}
|
||||
</span>
|
||||
<span className="block truncate text-micro text-ink-3">
|
||||
{entry.job}
|
||||
</span>
|
||||
</span>
|
||||
<span className="tnum flex-none text-xs text-ink-2">
|
||||
{entry.date}
|
||||
</span>
|
||||
<Badge tone={entry.tone}>{entry.state}</Badge>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader title="À traiter cette semaine" />
|
||||
<ul>
|
||||
{TODOS.map((todo) => (
|
||||
<li
|
||||
key={todo.title}
|
||||
className="flex items-start gap-3 border-b border-line-1 px-4 py-3 last:border-b-0 hover:bg-surface-2"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="mt-1.5 size-2 flex-none rounded-full"
|
||||
style={{ background: todo.dot }}
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-sm font-medium">
|
||||
{todo.title}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-xs leading-[var(--lh-prose)] text-ink-2">
|
||||
{todo.detail}
|
||||
</span>
|
||||
</span>
|
||||
<span className="tnum flex-none whitespace-nowrap text-micro text-ink-3">
|
||||
{todo.when}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { WeekGrid } from '@/components/planning/WeekGrid';
|
||||
import { PageBody, PageHeader } from '@/components/shell/PageHeader';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { POSTE_CODES, POSTE_LABELS, posteTokens } from '@/lib/design/postes';
|
||||
import { UNASSIGNED_ROW, WEEK_DAYS, WEEK_LABEL, WEEK_ROWS } from '@/lib/demo/semaine';
|
||||
|
||||
export const metadata = { title: 'Planning · semaine 33 · PlanFlow' };
|
||||
|
||||
export default function SemainePage() {
|
||||
return (
|
||||
<PageBody>
|
||||
<PageHeader
|
||||
title="Planning · semaine 33"
|
||||
subtitle={`Nantes Atlantis · ${WEEK_LABEL.split('·')[1]?.trim()} · ${WEEK_ROWS.length} salariés planifiés`}
|
||||
actions={
|
||||
<>
|
||||
<Button>Dupliquer S‑32</Button>
|
||||
<Button>Imprimer</Button>
|
||||
<Button variant="primary">Publier la semaine</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge tone="warn">2 alertes de convention</Badge>
|
||||
<Badge tone="info">Samedi non publié</Badge>
|
||||
<Badge tone="neutral">5 besoins non couverts</Badge>
|
||||
</div>
|
||||
|
||||
<WeekGrid
|
||||
days={WEEK_DAYS}
|
||||
rows={WEEK_ROWS}
|
||||
unassignedRow={UNASSIGNED_ROW}
|
||||
/>
|
||||
|
||||
<PosteLegend />
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Légende des postes.
|
||||
*
|
||||
* Elle n'est pas décorative : la couleur seule ne dit pas quel poste est
|
||||
* lequel, et le code à trois lettres imprimé dans chaque bloc n'a de sens que
|
||||
* si sa correspondance est visible sur la même page.
|
||||
*/
|
||||
function PosteLegend() {
|
||||
return (
|
||||
<section className="rounded-3 border border-line-1 bg-surface p-4">
|
||||
<h2 className="mb-3 text-micro font-semibold tracking-[0.08em] text-ink-3 uppercase">
|
||||
Postes
|
||||
</h2>
|
||||
<ul className="flex flex-wrap gap-2">
|
||||
{POSTE_CODES.map((code) => {
|
||||
const tokens = posteTokens(code);
|
||||
return (
|
||||
<li
|
||||
key={code}
|
||||
className="flex items-center gap-1.5 rounded-2 px-2 py-1 text-micro"
|
||||
style={{
|
||||
background: tokens.bg,
|
||||
color: tokens.fg,
|
||||
border: `1px solid ${tokens.edge}`,
|
||||
}}
|
||||
>
|
||||
<span className="font-semibold tracking-wide">
|
||||
{code.toUpperCase()}
|
||||
</span>
|
||||
<span>{POSTE_LABELS[code]}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
+17
-5
@@ -1,4 +1,7 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
import { THEME_INIT_SCRIPT } from '@/components/shell/ThemeToggle';
|
||||
|
||||
import './globals.css';
|
||||
|
||||
@@ -7,14 +10,23 @@ export const metadata: Metadata = {
|
||||
description: 'Gestion du personnel, des plannings et des temps.',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
// Le nonce est posé par requête dans src/proxy.ts. C'est la seule façon
|
||||
// d'exécuter un script inline sous notre CSP, qui n'autorise pas
|
||||
// 'unsafe-inline'.
|
||||
const nonce = (await headers()).get('x-nonce') ?? undefined;
|
||||
|
||||
return (
|
||||
<html lang="fr">
|
||||
<body className="min-h-dvh bg-neutral-50 text-neutral-900 antialiased">
|
||||
{children}
|
||||
</body>
|
||||
<html lang="fr" data-theme="light" suppressHydrationWarning>
|
||||
<head>
|
||||
<script
|
||||
nonce={nonce}
|
||||
dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }}
|
||||
/>
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user