diff --git a/.claude/skills/slide-variants/SKILL.md b/.claude/skills/slide-variants/SKILL.md new file mode 100644 index 0000000..2dbf907 --- /dev/null +++ b/.claude/skills/slide-variants/SKILL.md @@ -0,0 +1,69 @@ +--- +name: slide-variants +description: Use when building an Établi de l'IA séance deck from a storyboarded brief (D122) and Aurélien must compare 2–3 rendered treatments of a screen before choosing — « construis le deck », « montre-moi les variantes », « je prends B pour s1-e07 », « garde la A ». Authoring rules for the slides themselves stay in the `slides` skill. +--- + +# Variantes de slides (D122) + +The brief comes from app-formation-ai: `conception/generes/mandats//seances/seance-/brief.md`, +generated by `build_deck.py` from the storyboard (`/deck-storyboard`). Each screen carries a +Rôle and a Traitement; a screen marked **« À explorer »** carries 2–3 pistes. Deck-app builds +every piste as a live variant; Aurélien picks by looking at them on screen. + +## Layout + +- One deck per folder: `src/decks/-s/index.tsx`, served at `//` + (`npm run dev`, then `http://localhost:5173/fb-s1/`). The path, not a query: the presenter tab + keeps only `?presenter=1`. +- One file per capsule, `cNN-.tsx`, `export default [ … ]` in brief + order; `index.tsx` spreads them into ``. Capsule files can be built in parallel. +- `theme.css` in the deck folder sets `:root` tokens from the mandat's `profil.md` (colors, + fonts); `ui.tsx` holds the deck's shared pieces (Kicker with the day's path, card styles, `n()` + for notes). Logos copied into `assets/`. + +## Variants + +```tsx + + + … + … + … + + +``` + +`V` on the live slide cycles; a badge shows `s1-e07 · B animée (2/3)`. The choice is kept per id +in localStorage, shared with the presenter tab. Every variant renders the same content: D115 +holds — no fact, number or example the screen text lacks. + +## Choosing + +Aurélien says « s1-e07 : B ». Then, for that screen: delete the other ``s, inline the +winner's JSX directly in the ``, drop the `` wrapper. A deck is final when +`grep -r "` is empty. Report the choices back to the storyboard doc +(one line per screen) so the next cohort's deck starts from them. + +## Slide-by-slide review (R) + +In dev, **R** or the dock button (speech bubble) opens a panel on the current slide: Décision +`OK` / `Modifier` / `Retirer` + a note, saved by the Vite dev server (`/__review`, `vite.config.ts`) +to `src/decks//revue.json`, keyed by screen id. On « applique la revue »: +1. Read `revue.json`; handle every entry with `fait: false` and a decision other than `ok` + (`retirer` → delete the slide; `modifier` → do what the note says, D115 still holds). +2. Set `fait: true` on each handled entry (keep the note), report one line per screen. +3. Check the touched slides at 1280×720. A new note on a screen sets `fait` back to false. + +## Checks before handing over + +1. `npx tsc --noEmit -p .` clean. +2. Every slide at 1280×720 (a Zoom share) in the browser: screenshot each, variants included + (press V). Fix clipped text, overflow, contrast, a lone left-anchored block (`slides` rule 3). +3. Motion: nothing continuous, nothing faster than the engine's builds; 3D settles and stops. +4. `grep -c "{/\* s1-e" ` equals the brief's screen count; no duration on screen. + +## Common mistakes + +- Variants that differ in content (one adds an example): they must differ in form only. +- Inventing missing media or survey data instead of an empty, labelled frame. +- Leaving `` in a deck that ships. diff --git a/.gitignore b/.gitignore index da431f1..6ffddfb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -node_modules/ +node_modules dist/ .vite/ .DS_Store .env +public/media/ diff --git a/index.html b/index.html index fb4074a..1118131 100644 --- a/index.html +++ b/index.html @@ -1,15 +1,15 @@ - + - Replace — your deck title + Séance 2 — Utiliser efficacement les modèles de langage - +
diff --git a/public/fb-s1/librechat.png b/public/fb-s1/librechat.png new file mode 100644 index 0000000..e653773 Binary files /dev/null and b/public/fb-s1/librechat.png differ diff --git a/src/App.tsx b/src/App.tsx index f209b6d..55cbe4a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,977 +1,24 @@ -import Deck from './deck/Deck'; -import Slide from './deck/Slide'; -import Build from './deck/Build'; -import Reveal from './deck/Reveal'; -import Bento from './components/Bento'; -import Split from './components/Split'; -import CountUp from './components/CountUp'; -import TiltCard from './components/TiltCard'; -import Marquee from './components/Marquee'; -import VisualDashboard from './components/VisualDashboard'; -import StatGrid from './components/StatGrid'; -import Accordion from './components/Accordion'; -import Comparison from './components/Comparison'; -import Tabs from './components/Tabs'; -import Timeline from './components/Timeline'; -import CodeWindow from './components/CodeWindow'; -import BrowserFrame from './components/BrowserFrame'; -import SpotlightCard from './components/SpotlightCard'; -import { BarChart, LineChart, DonutChart } from './components/Charts'; -import Section from './components/Section'; -import Quote from './components/Quote'; -import Pricing from './components/Pricing'; -import Steps from './components/Steps'; -import Agenda from './components/Agenda'; -import Team from './components/Team'; -import Cover from './components/Cover'; -import BigNumber from './components/BigNumber'; -import Contrast from './components/Contrast'; -import Chat from './components/Chat'; -import Table from './components/Table'; -import Globe from './components/Globe'; +import { lazy, Suspense } from 'react'; +import type { ComponentType } from 'react'; -/* ══════════════════════════════════════════════════════════════════════ - ⚠️ THROWAWAY DEMO showing every component. DELETE these slides and AUTHOR - THE USER'S DECK. Each child of is one slide. Add speaker notes with - notes="…" on any slide (shown in presenter mode — press P). - ══════════════════════════════════════════════════════════════════════ */ -const panel = (extra = 0.22): React.CSSProperties => ({ - position: 'absolute', - inset: 0, - background: `radial-gradient(120% 100% at 30% 20%, color-mix(in srgb, var(--primary) ${ - extra * 100 - }%, transparent), transparent 60%), var(--surface-2)`, -}); -const card: React.CSSProperties = { - padding: 22, - borderRadius: 'var(--radius)', - background: 'var(--surface)', - border: '1px solid var(--hair)', -}; +/* One deck per folder in src/decks//index.tsx, picked by the first path + segment (/fb-s1/ → src/decks/fb-s1). The path, not a ?query, because the + presenter tab is opened as pathname + '?presenter=1' and keeps only that. */ +const decks = import.meta.glob<{ default: ComponentType }>('./decks/*/index.tsx'); +const id = window.location.pathname.split('/').filter(Boolean)[0] || 's2-gc'; +const load = decks[`./decks/${id}/index.tsx`]; +const Deck = load ? lazy(load) : null; export default function App() { + if (!Deck) + return ( +

+ Unknown deck « {id} ». Decks: {Object.keys(decks).map((k) => k.split('/')[2]).join(', ')} +

+ ); return ( - - {/* Cover */} - Bolt Slides} - subtitle="A responsive React deck engine. Delete this and build the real one." - foot="June 2026 · Component demo" - /> - - {/* Statement + click-build */} - -

- Dashboards are everywhere.{' '} - Insight isn't. -

- -

- Bolt Slides turns raw events into answers — automatically. -

-
-
- - {/* Agenda */} - - - {/* Contrast — the problem */} - - - {/* Split feature */} - - Everything, as it happens. - - } - body="Live metrics with sub-second latency — no pipelines to babysit." - media={ - <> -
-
- - - -
- - } - /> - - {/* Bento */} - , - body: 'events / min at peak.', - c: 5, - r: 2, - variant: 'glow', - }, - { - k: 'Uptime', - fig: , - c: 4, - }, - { k: 'Regions', fig: , c: 3, variant: 'accent' }, - { - k: 'Connectors', - title: '120+ native', - body: 'Snowflake, Kafka, dbt…', - c: 4, - }, - { k: 'Compliance', title: 'SOC 2 · HIPAA', c: 3 }, - ]} - /> - - {/* Globe */} - - Everywhere your data lives. - - } - body="Ingest close to the source; answer from the nearest edge." - markers={[ - { - location: [37.77, -122.41], - size: 0.08, - label: 'sfo1', - value: '221k evt/s', - }, - { location: [40.71, -74.0], size: 0.08 }, - { - location: [51.5, -0.12], - size: 0.07, - label: 'lhr1', - value: '188k evt/s', - }, - { location: [52.52, 13.4], size: 0.05 }, - { - location: [1.35, 103.82], - size: 0.07, - label: 'sin1', - value: '96k evt/s', - }, - { location: [35.68, 139.69], size: 0.06 }, - { location: [-33.87, 151.2], size: 0.05 }, - { location: [-23.55, -46.63], size: 0.05 }, - ]} - arcs={[ - { from: [37.77, -122.41], to: [51.5, -0.12] }, - { from: [51.5, -0.12], to: [1.35, 103.82] }, - { from: [37.77, -122.41], to: [-23.55, -46.63] }, - ]} - stats={[ - { value: '48%', label: 'North America' }, - { value: '31%', label: 'EMEA' }, - { value: '21%', label: 'APAC + LATAM' }, - ]} - /> - - {/* StatGrid — traction */} - , - label: 'ARR', - caption: 'up 3× year over year', - }, - { - value: , - label: 'Net retention', - caption: 'best in class', - }, - { - value: , - label: 'Enterprise logos', - caption: 'across six industries', - }, - ]} - /> - - {/* BigNumber */} - } - caption="events answered in under a second." - foot="Production traffic, trailing 30 days" - /> - - {/* Section divider */} -
- How it works. - - } - /> - - {/* Steps */} - - - {/* Chat */} - - - {/* Comparison */} - - -
- Why teams switch -
-

- The honest comparison. -

-
- -
- -
-
-
- - {/* Tabs */} - - -
- One platform -
-

- Built for every team. -

-
- - - Trace any request end-to-end, alert on anomalies, ship with - confidence. -

- ), - }, - { - label: 'Data', - content: ( -
- -
- ), - }, - { - label: 'Ops', - content: ( -

- One source of truth for uptime, cost, and capacity — no - spreadsheets. -

- ), - }, - ]} - /> -
-
- - {/* Split + code */} - - Drop-in simple. - - } - body="Add it to your app in three lines. No SDK to learn, no schema to define." - media={ - <> -
-
- -
- - } - /> - - {/* Browser frame */} - - -
- See it live -
-

- Your data, one screen. -

-
- -
- -
-
-
- Bolt Slides -
- {['Overview', 'Events', 'Funnels', 'Cohorts', 'Settings'].map( - (n, i) => ( -
- {n} -
- ) - )} -
-
-
-

- Overview -

- Last 30 days -
-
- {[ - ['Revenue', '$1.24M', '▲ 18.2%'], - ['Active users', '48,210', '▲ 9.4%'], - ['Churn', '1.9%', '▼ 0.6%'], - ].map(([l, v, d]) => ( -
-
- {l} -
-
- {v} -
-
- {d} -
-
- ))} -
-
- -
-
-
-
-
-
-
- - {/* Charts */} - - -
- The numbers -
-

- Growth you can see. -

-
- -
-
-
- Weekly active -
-
- -
-
-
-
- Revenue -
- -
-
- -
-
-
-
- - {/* Data table */} - - -
- Unit economics -
-

- Growth, by region. -

-
- - - - - - {/* Timeline */} - - -
- Where we're going -
-

- The roadmap. -

-
-
- -
-
- - {/* Pricing */} - - - {/* Spotlight principles */} - - -
- What we believe -
-

- Three principles. -

-
- -
- {[ - { - k: '01', - t: 'Fast by default', - d: 'Speed is a feature. Everything is realtime.', - }, - { - k: '02', - t: 'Yours to own', - d: 'Your data, your infra, no lock-in.', - }, - { - k: '03', - t: 'Honest pricing', - d: 'No per-seat tax. Scale without surprises.', - }, - ].map((p) => ( - -
- {p.k} -
-

- {p.t} -

-

- {p.d} -

-
- ))} -
-
-
- - {/* Accordion — FAQ */} - - -
- Common questions -
-

- Frequently asked. -

-
- -
- -
-
-
- - {/* Team */} - - - {/* Logos */} - - -
- Trusted by teams everywhere -
-
- - - - {/* Quote */} - - - {/* CTA */} - - -

- Let's talk. -

-

- hello@bolt.new -

-
-
- + + + ); } diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx new file mode 100644 index 0000000..51e820f --- /dev/null +++ b/src/components/Prompt.tsx @@ -0,0 +1,88 @@ +import { useState, type MouseEvent, type ReactNode } from 'react'; + +/* A prompt to copy, verbatim: mono text in a bordered box with a copy button. + `label` names it (e.g. "Version 1"); `muted` styles a "before" prompt. */ +export default function Prompt({ + children, + label = 'Prompt à copier', + muted, + size = 'md', +}: { + children: ReactNode; + label?: string; + muted?: boolean; + size?: 'sm' | 'md' | 'lg'; +}) { + const [copied, setCopied] = useState(false); + const fontSize = { + sm: 'clamp(11.5px, 1.05vw, 15px)', + md: 'clamp(13px, 1.3vw, 18px)', + lg: 'clamp(16px, 2vw, 28px)', + }[size]; + const copy = (e: MouseEvent) => { + const text = + e.currentTarget.parentElement?.parentElement?.querySelector('pre') + ?.innerText ?? ''; + navigator.clipboard?.writeText(text).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }); + }; + return ( +
+
+ {label} + +
+
+        {children}
+      
+
+ ); +} diff --git a/src/components/Review.tsx b/src/components/Review.tsx new file mode 100644 index 0000000..3a86594 --- /dev/null +++ b/src/components/Review.tsx @@ -0,0 +1,121 @@ +import { useEffect, useState } from 'react'; + +/* Slide review (dev only, R or the dock button): a decision and a note per + screen id, saved to src/decks//revue.json through the dev server. + Claude reads that file on « applique la revue », does the work and sets + `fait: true`; a new note on the same screen reopens it. */ +export type Decision = 'ok' | 'modifier' | 'retirer'; +export type Entry = { slide: number; decision: Decision; note: string; fait: boolean; maj: string }; +type Revue = Record; + +const deck = window.location.pathname.split('/').filter(Boolean)[0] || 's2-gc'; +const url = `/__review?deck=${deck}`; +const LABELS: Record = { ok: 'OK', modifier: 'Modifier', retirer: 'Retirer' }; + +export default function Review({ id, slide, onClose }: { id: string; slide: number; onClose: () => void }) { + const [revue, setRevue] = useState({}); + const [decision, setDecision] = useState('ok'); + const [note, setNote] = useState(''); + const [etat, setEtat] = useState<'' | 'enregistré' | 'erreur'>(''); + + useEffect(() => { + fetch(url) + .then((r) => r.json() as Promise) + .then((r) => { + setRevue(r); + setDecision(r[id]?.decision ?? 'ok'); + setNote(r[id]?.note ?? ''); + setEtat(''); + }) + .catch(() => setEtat('erreur')); + }, [id]); + + const save = async () => { + const next = { ...revue, [id]: { slide: slide + 1, decision, note, fait: false, maj: new Date().toISOString() } }; + const r = await fetch(url, { method: 'POST', body: JSON.stringify(next) }).catch(() => null); + if (r?.ok) setRevue(next); + setEtat(r?.ok ? 'enregistré' : 'erreur'); + }; + + const e = revue[id]; + const ouvertes = Object.values(revue).filter((x) => !x.fait && x.decision !== 'ok').length; + + return ( +
{ + k.stopPropagation(); + if (k.key === 'Escape') onClose(); + if (k.key === 'Enter' && (k.metaKey || k.ctrlKey)) save(); + }} + style={{ + position: 'fixed', + right: 16, + bottom: 88, + zIndex: 60, + width: 'min(360px, calc(100vw - 32px))', + display: 'grid', + gap: 10, + padding: 16, + borderRadius: 'var(--radius-sm)', + background: 'var(--surface)', + border: '1px solid var(--hair)', + boxShadow: 'var(--shadow)', + color: 'var(--fg)', + font: '14px/1.4 var(--font-body)', + }} + > +
+ + Revue · {id} (slide {slide + 1}) + + +
+ {e?.fait && Traité par Claude — une nouvelle note rouvre l'écran.} +
+ {(Object.keys(LABELS) as Decision[]).map((d) => ( + + ))} +
+