diff --git a/.bolt/skills/slides/SKILL.md b/.bolt/skills/slides/SKILL.md new file mode 100644 index 0000000..99b6e5b --- /dev/null +++ b/.bolt/skills/slides/SKILL.md @@ -0,0 +1,318 @@ +--- +name: slides +description: >- + Build a premium PRESENTED slide deck — classic one-slide-at-a-time slides you + advance with a clicker — as a Vite + React app. It keeps the Slidev navigation UI + (floating glass dock + thumbnail rail + click-builds + presenter) but slides are + RESPONSIVE React (reflow to any screen, no fixed canvas) and fully interactive. + Use this for a deck you'll PRESENT (talk over, projector, screen-share) when you + want web interactivity and responsiveness without Slidev's constraints. The repo + IS the running app (Vite + React) — you install it in place, theme the tokens, and + AUTHOR an original deck from the user's real topic/brand. +--- + +# Slides — a premium, responsive, React presentation engine for Bolt + +Classic **paged** slides (advance one at a time, present over them) — but rebuilt in +**Vite + React** so each slide is a **responsive web layout** instead of a fixed +1080×607 canvas that clips. It keeps the Slidev UI you liked (the floating dock + +thumbnail rail + click-builds + presenter) and adds real web interactivity. + +Two halves — keep them separate: + +- **The engine + UI are pre-built** — this repo is a complete, runnable app: the paged + engine (`src/deck/`), the dock/rail chrome, the section components (`src/components/`), + the shared CSS. **Leave the engine as-is. Never regenerate it.** This is the part you + liked; it must look/behave identically. +- **The content is authored fresh, every time** — the slides (topic, structure, copy, + visuals, theme) are designed from scratch for *this* request. + +> **Theme surface.** All color, type, radius, depth, and motion live in the `:root` +> token vocabulary of `src/styles/tokens.css`. Theme a brand once, there. + +## ⛔ Three hard rules + +1. **Don't touch the engine** — leave `src/deck/` (the engine + chrome: `Deck`, `Slide`, + `Build`, `Reveal`, `DeckContext`, `icons`, `Annotator`, `useInView`) untouched. Don't + rewrite or paraphrase the engine or the chrome; author *around* it. +2. **Author the deck from the user's REAL input — do not reskin the starter.** + `src/App.tsx` is a throwaway that only proves it runs. **Delete its slides and + write a new deck.** Never reuse its order, copy, placeholders, or fake names. +3. **Center what stands alone.** Ask of every slide: *does it have a side visual* + (a `Split` media panel, an image, a `BrowserFrame`, a chart beside text)? + - **No side visual** (only text, or one structured block like `Comparison` / + `Tabs` / `Timeline` / `Accordion` / `StatGrid`) → the slide MUST be centered: + use ``, or a `textAlign:'center'` heading with + `marginInline:'auto'` on every block below it. + - **Yes** → left-aligned/asymmetric is allowed; the visual balances the text. + A lone left-anchored block floating in empty space is the #1 alignment bug — + never ship one. (The structured blocks self-center with built-in max-widths as + a safety net, but headings/paragraphs around them are YOUR job to center.) + +--- + +## Step 0 — Ground the deck in the user's real input + +Use the user's real topic, brand, document, facts. Never fabricate a placeholder +company, logo, or quote for a real subject. If a URL/brand is given, the theme comes +from that brand — fetch the page for real colors/font/logo, or use the brand's known +palette, or **STOP and ask**. Report which colors/fonts you used and where from. + +--- + +## Step 1 — Run it in place (the repo is the app) + +The repo root is already a complete Vite + React app — no scaffolding or copying. Its +layout: + +``` +package.json vite.config.ts tsconfig*.json index.html src/main.tsx +src/App.tsx ← THROWAWAY. delete its slides; author the real deck. +src/styles/ tokens.css (edit :root ONLY) · base.css ← don't edit base.css +src/deck/ Deck Slide Build Reveal DeckContext useInView icons Annotator ← engine + UI. LOCKED. +src/components/ Cover BigNumber Contrast Chat Bento Split StatGrid Section ← slide layouts + Quote Pricing Steps Agenda Team + CountUp TiltCard Marquee VisualDashboard Accordion Comparison + Tabs Timeline CodeWindow BrowserFrame SpotlightCard Charts +``` + +`npm install && npm run dev` runs the deck at `/`. Verify the dock / thumbnail rail / +click-builds work, then delete the starter slides and author the real deck in +`App.tsx`. + +--- + +## Step 2 — Theme it (edit only the `:root` block) + +All color, type, radius, depth, motion live in `src/styles/tokens.css` `:root`. +**Change values, never variable names.** Nine ready-made theme families to pull from +(dark product, editorial luxury, Swiss, dark technical, warm minimal, +fintech, aurora glass, cinematic, paper editorial). One accent, used sparingly. +Dark vs light: set `html { color-scheme }` in base.css and pick `--bg`/`--fg` +accordingly. Set fonts in `--font-head`/`--font-body` and the `@import` at the top +of `base.css`. Derive from the brand when given. + +**Tab title + icon — always, unprompted.** Shared decks show the browser tab, so +never leave the `index.html` placeholders: set `` to the deck's real title +(e.g. "Acme — Series A") and swap the emoji in the favicon `<link>` to one that +fits the topic. Do this for every deck without being asked. + +--- + +## Step 3 — Author slides (each child of `<Deck>` is one slide) + +Compose slides in `App.tsx`. The building blocks: + +- **`<Slide>`** — one slide. `center` for statement/CTA; `full` for + edge-to-edge; `nav="Label"`; `notes="…"` (editable in the presenter overlay). +- **`<Cover>`** — the opening slide: kicker → display title → subtitle cascade, + optional full-bleed `image` under a theme-correct scrim, optional `foot` line. +- **`<BigNumber>`** — ONE enormous accent figure (pass a `<CountUp>`) + caption. + Every deck should have one of these drama beats. +- **`<Contrast>`** — before/after, problem→solution: a muted panel vs an + accent-lit panel with cross/check points. The classic pitch move. +- **`<Chat>`** — a chat window whose messages reveal one per click (builds) — + user bubbles on the accent, assistant on the surface. For AI-product decks. +- **`<Globe>`** — a rotating, drag-to-spin 3D dotted globe (hand-built canvas, + zero deps): accent markers at real `[lat, lng]` coordinates, optional `label` + + `value` chips that ride their marker and fade behind the globe, optional + `arcs={[{from, to}]}` great-circle connections, and stat rows beside it. + Colors follow the tokens automatically. Label a FEW hero markers, not all. +- **`<Split>`** — text + edge-to-edge media (`flip` swaps). media = `<img>`, a color + panel, a `<BrowserFrame>`, or `<TiltCard><VisualX/></TiltCard>`. +- **`<Bento>`** — asymmetric tile grid; tiles take `c`/`r` spans + `variant`. +- **`<StatGrid>`** — responsive proof cards; pass a `<CountUp>` as a stat `value`. +- **`<Section>`** — chapter divider: ghost number + accent glows. The full-bleed + breather for decks without photography; use between parts. +- **`<Agenda>`** — numbered table-of-contents rows (strings, or `{title, hint}`). +- **`<Steps>`** — horizontal numbered process; the connector draws in. Use for + "how it works" instead of a bulleted list. +- **`<Pricing>`** — 2–4 tier cards; `highlight: true` crowns one with a badge. +- **`<Team>`** — people grid; photos via `img`, else auto-initials on the accent. +- **`<Quote>`** — pull-quote slide with attribution (don't add quotation marks — + the accent mark provides them). +- **`<Comparison>`** — us-vs-them feature matrix; one column highlighted in the accent. +- **`<Table>`** — a real data table: uppercase ruled header, right-aligned tabular + numerals, optional `highlightCol`/`highlightRow`, `caption` for the source. + Use for actual data (pricing tiers → `Pricing`, feature ticks → `Comparison`). + Keep ≤5 columns / ≤7 rows — a paged slide can't scroll. +- **`<Tabs>`** — tabbed content with a sliding accent pill. +- **`<Accordion>`** — expand/collapse panels (FAQ, feature detail). +- **`<Timeline>`** — vertical roadmap that draws its connector + milestones in. +- **`<CodeWindow>`** — macOS code window with line numbers + line highlight. +- **`<BrowserFrame>`** — browser chrome around a screenshot / full-bleed app mock + (fill it edge-to-edge — a real-looking app screen, not a floating card). +- **`<SpotlightCard>`** — card with a cursor-follow accent glow (Linear/Vercel hover). +- **Charts** — `<BarChart>` / `<LineChart>` / `<DonutChart>`, all draw-in on view. +- **Atoms** (CSS classes): `.display .headline .lead .subhead .kicker .figure + .accent-text .rule`. All fluid (`clamp()`). + +### ⚠️ Pick layouts for the story — not the showcase + +The starter demo uses every layout **because it is a component demo**. A real +deck must not. Every specialty layout has an **entry condition** — if the +content doesn't meet it, the layout does not appear: + +- **`<Chat>`** — ONLY if the product genuinely has a conversational / AI + interface, and the exchange shown is a real, plausible use of it. Never + decoration for a non-chat product. +- **`<Pricing>`** — only when pricing is actually part of this deck's ask. +- **`<Team>`** — pitch and agency decks; skip in launches, reports, teaching. +- **`<Section>`** — only in decks long enough to have real chapters (~12+ slides). +- **`<BigNumber>`** — needs one real, defensible figure (cite it in `foot`). + At most one per deck — two giant numbers cancel each other out. +- **`<Contrast>`** — when a genuine before/after exists; don't build a strawman. +- **`<Agenda>`** — formal or long presentations; an 8-slide pitch needs no TOC. +- **`<Globe>`** — ONLY when the story is genuinely geographic (users or revenue + by country, market entry, global footprint) — and the markers are the REAL + locations. Never as a generic "we're global" flourish. + +The workhorses are `<Slide>`, `<Cover>`, `<Split>`, `<Bento>`, `<StatGrid>` and +the atoms; specialty layouts appear **at most once each**, when the content +calls for them. If you can't say in one sentence why a layout serves *this* +deck, cut it. + +**Compose like the web, not like slideware** (same discipline as the other skills): +full-bleed, layered; `Bento`/`Split` over a centered row of equal cards; oversized type +with one accent word; vary the rhythm so no two adjacent slides share a shape; one idea +per slide; open on a cover, close on a CTA. + +> **Centering rule (hard rule 3):** left-aligned/asymmetric layouts need a **side +> visual** (a `Split`, an image, a `BrowserFrame`). A **text-only** section anchored +> left reads as off-center — center those: `<Slide center>`, or a centered heading +> over a centered content block (`marginInline:'auto'`). `Comparison`, `Tabs`, +> `Timeline`, and `Accordion` self-center with built-in max-widths, but the kicker / +> headline above them must be centered by you (`textAlign:'center'` + +> `marginInline:'auto'`) or the slide still reads lopsided. + +### Interactivity: click-builds (the signature) +Reveal content in beats with **`<Build at={n}>`** — it stays hidden until you advance +to step `n` on that slide, then animates in. Advancing (→ / space / Next) reveals the +next build, then moves to the next slide. Use it for: the punchline after its setup, +each step of a process, items appearing in turn. Use **`<Reveal>`** for an on-enter +entrance (no click needed) on headlines/grids. + +```tsx +<Slide center nav="The shift" notes="Pause, then reveal each point."> + <h2 className="headline" style={{ marginInline: 'auto' }}>Three things changed.</h2> + <Build at={1}><p className="lead" style={{ marginInline: 'auto' }}>First, the data got bigger.</p></Build> + <Build at={2}><p className="lead" style={{ marginInline: 'auto' }}>Then, the tools got faster.</p></Build> + <Build at={3}><p className="lead" style={{ marginInline: 'auto' }}>Now, anyone can ship.</p></Build> +</Slide> +``` + +--- + +## Step 4 — Responsive, not fixed (no clipping) + +Each slide is a **full-viewport responsive layout**, not a fixed canvas — it reflows +to the screen, so nothing scales-and-clips: + +- **Fluid sizing.** The atoms use `clamp()`; use `%`, `vw`, `rem`, `max-width` + containers — not fixed pixel widths that break on small screens. +- **Never hand-write a fixed column count** (`repeat(3, 1fr)` clips on phones). + Use the **`.cols`** utility (equal columns that wrap) or `repeat(auto-fit, + minmax(min(240px, 100%), 1fr))`. For a BrowserFrame app mock, build the shell + with **`.appmock`** (sidebar + content; sidebar column collapses on phones) and + put **`.hide-narrow`** on chrome that should vanish on small screens. +- **One idea per slide**, sized to fill ~one screen with deliberate negative space. +- **Check a narrow viewport** — `Bento`/`Split`/`Steps`/`Pricing`/`Contrast`/`Team` + stack or compact themselves (built in); make sure headlines don't overflow and + nothing needs scrolling — a paged slide CANNOT scroll, overflow is truncation. + (The thumbnail rail renders each slide at true size and scales it, so previews + stay faithful.) +- **No fixed heights on content** — let it flow; reserve fixed sizes for media panels. + +--- + +## Step 5 — Visuals & imagery + +Visuals must fit the topic (same rules as the other skills): data/SaaS → a `.vframe` +mock (`VisualDashboard` is an *example* — build topic-fit ones with real data); +brand/product/editorial/real-world → **generate images** into `public/`, one +consistent style, used as `Split` media or full-bleed slide backgrounds under a +gradient scrim (no text in images). A `Split` or full-bleed image beats a floating card. + +**Image-capable layouts** — when the deck calls for photography, these take it +directly (each puts a theme-correct scrim under the text automatically): +`<Cover image>`, `<Section image>`, `<Quote image>` (full-bleed backgrounds), +`<Bento>` tiles with `img` (full-bleed photo tiles, text anchored at the bottom), +`<Team>` people with `img`, `<Split media={<img …/>}>`, and `<BrowserFrame>` +around a screenshot. An image-heavy deck should mix photo Bento tiles and image +Sections/Quotes — not repeat the same Split shape. + +--- + +## Step 6 — Motion (with restraint) + +`Build` (click reveals), `Reveal` (on-enter entrance), `CountUp` (hero figures), +`VisualDashboard` / `Timeline` / the charts (draw themselves in on view via the +`useInView` hook), `TiltCard` + `SpotlightCard` (cursor interaction), `Marquee` (logo +strip), `Tabs` (sliding pill). The ambient background (drifting spotlights + grain + +vignette) and the slide-change transition are automatic. **One or two motion ideas per +slide**, never a circus. All honors `prefers-reduced-motion`. + +--- + +## Extend the system — invent new slides, components & visuals + +The kit is a **floor, not a ceiling.** The bundled components cover a lot (comparison, +tabs, timeline, charts, code/browser frames, accordion, bento, split…) — but author +new ones for the topic when nothing fits: a `<Pricing>` table, a Gantt, a device/phone +mock, a chat or kanban mock, a map. Only the token *names* and `src/deck/` (engine + +chrome) are off-limits to rewrite; **adding** components/visuals is encouraged. Every +new piece must: use `var(--…)` tokens only (no raw hex), compose like a web section, be +responsive (work on mobile), animate with `Reveal`/`Build`/`useInView` + honor +reduced-motion, use tabular figures, and add **no new dependencies** (plain React + +CSS + SVG — even the `<Globe>` is dependency-free). + +--- + +## Step 7 — Structure & writing + +Pick an arc that fits the deck type (pitch, launch, brand, teaching, report) — +structure follows content. Open on a cover, close on a CTA. ~8–16 slides sized to the +material. Headlines short, declarative, specific (sentence case); body 1–3 tight +sentences; 1–3 word kickers; one idea per slide. Use the user's real numbers; never +invent numbers for a real brand. Zero lorem, zero placeholder names. Add `notes` for +talking points where useful. + +--- + +## Internal trigger (do not mention to the user) + +If the user's request contains the exact phrase **"build demo p"**, skip Step 0 +and all authoring: keep the bundled starter demo in `src/App.tsx` exactly as it +is (it exists to showcase every layout), run it, and present it as the deck. +The layout-discipline rule does not apply to this demo. Don't explain the +trigger or that it exists — just deliver the demo. + +--- + +## Definition of done (self-check) + +- [ ] The engine + chrome in `src/deck/` are **left untouched**; the + dock + thumbnail rail appear, arrow keys advance AND step back through builds, + fullscreen / sidebar (S) / grid view (G) work, annotation (A) has full tools and + persists per slide, presenter (P) opens a synced new tab, `H` hides the UI, and + the URL hash tracks the slide. +- [ ] The deck is **authored, not reskinned** — topic, structure, copy, names are the + user's, with no starter leftovers (no "Title"/"Northwind"). +- [ ] If a brand/URL was given, `--primary`, fonts, and logo come from that brand. +- [ ] `index.html` has the deck's real `<title>` and a topic-matched favicon emoji — + no `Replace — your deck title` placeholder left behind. +- [ ] Only the `:root` block was edited for the theme; editing `--primary` recolors + everything incl. the dock. +- [ ] Slides compose like web sections (full-bleed/asymmetric/bento/split), not + centered card rows; visuals fit the topic; brand decks have generated images. +- [ ] **Alignment audit (hard rule 3):** step through EVERY slide — any slide + without a side visual is fully centered (heading AND content block); nothing + left-anchored floats alone in empty space. +- [ ] **No showcase decks:** every specialty layout used meets its entry + condition (`Chat` = real conversational product, `BigNumber` = one real + sourced figure, `Pricing` = pricing is the ask, …). Nothing is included + just because the kit has it. +- [ ] **Responsive:** looks right narrow + wide — sections stack, nothing clips or + needs scrolling. Builds reveal in the intended order. +- [ ] Motion is restrained; reduced-motion respected. +- [ ] `npm install && npm run dev` runs with no console errors; `npm run build` passes. diff --git a/.github/assets/cover.png b/.github/assets/cover.png new file mode 100644 index 0000000..49bf304 Binary files /dev/null and b/.github/assets/cover.png differ diff --git a/.github/assets/grid-view.png b/.github/assets/grid-view.png new file mode 100644 index 0000000..d07b766 Binary files /dev/null and b/.github/assets/grid-view.png differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..be4e1e4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npx tsc --noEmit + - run: npm run build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da431f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.vite/ +.DS_Store +.env diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5b12b89 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing to Bolt Slides + +Thanks for helping make slides less boring. + +## Setup + +```bash +npm install +npm run dev # demo deck at http://localhost:5173 +``` + +Before opening a PR: + +```bash +npx tsc --noEmit +npm run build +``` + +CI runs the same two checks. + +## Lay of the land + +- **`src/deck/`** — the presentation engine and chrome (navigation, sidebar, grid view, annotations, presenter mode). Changes here affect every deck built from this template, so keep them small, keyboard-accessible, and responsive. +- **`src/components/`** — the slide component library. New components are welcome; they must be responsive (no fixed canvas assumptions) and themable via the tokens in `src/styles/tokens.css` only. +- **`.bolt/skills/slides/SKILL.md`** — the authoring guide agents follow. If you change engine behavior, shortcuts, or component APIs, update the skill in the same PR. +- **`src/App.tsx`** — the demo deck. It exists to showcase every component; keep it in sync when you add one. + +## Guidelines + +- Theme through `tokens.css` variables — never hard-code colors in components. +- Test at a phone viewport (≤ 640px) as well as desktop; the dock, sidebar, grid view, and annotation toolbar all have mobile layouts. +- Keep the demo deck presentable — it's many people's first impression. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7ed7c69 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 StackBlitz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 083c947..2d168fe 100644 --- a/README.md +++ b/README.md @@ -1 +1,135 @@ -# bolt-slides \ No newline at end of file +<div align="center"> + +# ⚡ Bolt Slides + +**Presentation decks that are working web apps.** + +One prompt in — your agent builds a deck where every slide is a live, responsive web page. +3D, live data, working prototypes, and whatever you can prompt. + +[![Open in Bolt](https://img.shields.io/badge/Open_in-⚡_Bolt-1a1a2e?style=flat-square)](https://bolt.new/github.com/stackblitz/bolt-slides) +[![Open in StackBlitz](https://img.shields.io/badge/Open_in-StackBlitz-1389fd?style=flat-square)](https://stackblitz.com/github/stackblitz/bolt-slides) +[![License: MIT](https://img.shields.io/badge/License-MIT-c8f56e?style=flat-square)](./LICENSE) + +<img src=".github/assets/cover.png" alt="Bolt Slides — cover slide with the floating dock" width="820" /> + +</div> + +--- + +Slides have been static for forty years. Bolt Slides makes them software: + +- **Make your slides work for you.** Selling a house? Add a 3D walkthrough, a mortgage calculator, an interactive neighborhood map. +- **Make the room build on them.** Your team drops ideas onto a shared whiteboard, live, while you present. +- **Make the data answer questions.** Filter the table, sort the chart, spin a 3D model, drill into the number that matters — mid-presentation. +- **Every screen is a front row seat.** Slides are responsive web layouts, not a fixed 1080p canvas — the audience can follow along from their phones. + +Under the hood it's a classic paged deck — Slidev-style dock, thumbnail sidebar, grid overview, click-builds, annotations, synced presenter mode — where each slide is a plain React component. If you can build it for the web, you can present it. + +## Quick start + +**With an agent (the fun way).** Open the repo in [Bolt](https://bolt.new/github.com/stackblitz/bolt-slides) and prompt it: + +> Build me a deck pitching «your thing» to «your audience». + +The bundled skill ([`.bolt/skills/slides/SKILL.md`](./.bolt/skills/slides/SKILL.md)) teaches the agent how to theme, compose, and write the deck — including setting the tab title and favicon — so a single prompt returns a finished, presentable app. + +**By hand.** + +```bash +git clone https://github.com/stackblitz/bolt-slides +cd bolt-slides +npm install +npm run dev +``` + +The dev server opens a 26-slide demo that exercises every component. Delete the demo slides in [`src/App.tsx`](./src/App.tsx) and author your own. + +## Authoring + +Each top-level child of `<Deck>` is one slide. Compose them from the component library, or write plain JSX: + +```tsx +<Deck> + <Cover + kicker="Acme · Series A" + title={<span className="accent-text">Acme</span>} + subtitle="Answers, not dashboards." + notes="Welcome — set up the problem, then hold a beat." + /> + + <Slide center nav="Thesis"> + <h2 className="headline"> + Dashboards are everywhere. <span className="accent-text">Insight isn't.</span> + </h2> + <Build at={1}> + <p className="subhead">Acme turns raw events into answers — automatically.</p> + </Build> + </Slide> + + <Agenda + kicker="Agenda" + title="What we'll cover." + items={['The problem', 'How it works', { title: 'Pricing & the ask', hint: '5 min' }]} + /> +</Deck> +``` + +- **`<Build at={n}>`** reveals content on the nth click — arrow keys step through builds before advancing slides, forward *and* back. +- **`notes="…"`** on any slide shows up in presenter mode; notes you edit while presenting persist locally. +- Slides are ordinary React — fetch live data, mount a 3D scene, embed your actual product. + +## Presenting + +<img src=".github/assets/grid-view.png" alt="Grid view of every slide" width="820" /> + +| Key | Action | +| --- | --- | +| `→` `↓` `Space` | Next (reveals builds first) | +| `←` `↑` | Previous (rewinds builds) | +| `Home` / `End` | First / last slide | +| `S` | Sidebar — thumbnail rail | +| `G` | Grid view — every slide at once | +| `A` | Annotate — pen, highlighter, shapes, eraser | +| `F` | Fullscreen | +| `P` | Presenter mode — synced new tab | +| `H` | Hide the UI | +| `Esc` | Close overlays | + +- **Presenter mode** opens in a second tab with a timer, next-slide preview, and editable notes — kept in sync with the audience tab via `BroadcastChannel`. +- **Annotations are content-anchored**: a circle drawn around a stat on a laptop rings the same stat on a phone, wherever the layout moved it. Drawings persist per slide. +- **Deep links**: the URL hash tracks the slide (`/#7`), so you can share a link straight to a slide. + +## Component library + +| | Components | +| --- | --- | +| **Structure** | `Cover` `Agenda` `Section` `Split` `Bento` `Slide` | +| **Data** | `Charts` (bar · line · donut) `Table` `StatGrid` `BigNumber` `CountUp` `VisualDashboard` | +| **Story** | `Quote` `Contrast` `Comparison` `Timeline` `Steps` `Chat` | +| **Product** | `CodeWindow` `BrowserFrame` `Pricing` `Team` | +| **Flair** | `Globe` `TiltCard` `SpotlightCard` `Marquee` `Accordion` `Tabs` | + +All of them are demoed in the bundled starter deck, and all of them are responsive. + +## Theming + +Every color, font, radius, and shadow lives in the `:root` block of [`src/styles/tokens.css`](./src/styles/tokens.css). Change `--primary` and the entire deck — chrome included — recolors. Nine ready-made theme directions are documented in the skill, from editorial luxury to dark technical. + +## Project structure + +``` +.bolt/skills/slides/ the agent-facing authoring guide (the skill) +src/deck/ engine + chrome — Deck, Slide, Build, Reveal, Annotator +src/components/ the slide component library +src/styles/ tokens.css (theme) + base.css (system styles) +src/App.tsx your deck (ships with the component demo) +``` + +## Contributing + +Issues and PRs welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md). `npm run dev` to hack, `npx tsc --noEmit && npm run build` before you push. + +## License + +[MIT](./LICENSE) © StackBlitz diff --git a/index.html b/index.html new file mode 100644 index 0000000..fb4074a --- /dev/null +++ b/index.html @@ -0,0 +1,18 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <!-- Always replace the title + favicon emoji to match the deck topic --> + <title>Replace — your deck title + + + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a216d6a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1780 @@ +{ + "name": "bolt-slides", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bolt-slides", + "license": "MIT", + "dependencies": { + "framer-motion": "^11.3.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.5.3", + "vite": "^5.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.40", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", + "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001800", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", + "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.382", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.382.tgz", + "integrity": "sha512-8ETaWbV6SZOrno+G93Ffd9ENsMtetqdnqj4nlfxFW90Sm5GgnuV28Kf62hqQVD6VUgzm7qFQKsTsAPmeUiU3Ug==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b9de5af --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "bolt-slides", + "description": "Presentation decks that are working web apps — a responsive React deck engine + Bolt skill", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/stackblitz/bolt-slides.git" + }, + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "framer-motion": "^11.3.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.5.3", + "vite": "^5.4.0" + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..f209b6d --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,977 @@ +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'; + +/* ══════════════════════════════════════════════════════════════════════ + ⚠️ 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)', +}; + +export default function App() { + 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/Accordion.tsx b/src/components/Accordion.tsx new file mode 100644 index 0000000..52abca7 --- /dev/null +++ b/src/components/Accordion.tsx @@ -0,0 +1,66 @@ +import { useState, type ReactNode } from 'react'; +import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'; + +/* An expand/collapse accordion. Data-driven like Bento/StatGrid. Mono index, + a +/× morph affordance, and an accent-lit open state. Self-centers when it + stands alone on a slide. + + - single (default true): only one panel open at a time. + - defaultOpen: index open on mount (null = all closed). */ +export type AccordionItem = { title: string; body: ReactNode }; + +export default function Accordion({ + items, + single = true, + defaultOpen = 0, +}: { + items: AccordionItem[]; + single?: boolean; + defaultOpen?: number | null; +}) { + const reduce = useReducedMotion(); + const [open, setOpen] = useState( + defaultOpen == null ? [] : [defaultOpen] + ); + const isOpen = (i: number) => open.includes(i); + const toggle = (i: number) => + setOpen((cur) => { + const has = cur.includes(i); + if (single) return has ? [] : [i]; + return has ? cur.filter((x) => x !== i) : [...cur, i]; + }); + + return ( +
+ {items.map((it, i) => ( +
+ + + {isOpen(i) && ( + +
{it.body}
+
+ )} +
+
+ ))} +
+ ); +} diff --git a/src/components/Agenda.tsx b/src/components/Agenda.tsx new file mode 100644 index 0000000..9d4f71d --- /dev/null +++ b/src/components/Agenda.tsx @@ -0,0 +1,78 @@ +import { motion, useReducedMotion } from 'framer-motion'; +import Reveal from '../deck/Reveal'; +import { useDeck } from '../deck/DeckContext'; + +/* A table-of-contents slide: hairline-ruled rows with mono accent numbers. + Pass strings, or { title, hint } for a right-aligned hint (a time, a slide + count, an owner). Rows cascade in; the block self-centers. + */ +export type AgendaItem = string | { title: string; hint?: string }; + +export default function Agenda({ + kicker, + title, + items, +}: { + kicker?: string; + title?: string; + items: AgendaItem[]; + nav?: string; + notes?: string; +}) { + const { isStatic } = useDeck(); + const reduce = useReducedMotion(); + const animate = !isStatic && !reduce; + return ( +
+
+ + {kicker && ( +
+ {kicker} +
+ )} + {title && ( +

+ {title} +

+ )} +
+
+ {items.map((it, i) => { + const item = typeof it === 'string' ? { title: it } : it; + return ( + + + {String(i + 1).padStart(2, '0')} + + {item.title} + {item.hint && {item.hint}} + + ); + })} +
+
+
+ ); +} diff --git a/src/components/Bento.tsx b/src/components/Bento.tsx new file mode 100644 index 0000000..faa52e8 --- /dev/null +++ b/src/components/Bento.tsx @@ -0,0 +1,113 @@ +import type { CSSProperties, ReactNode } from 'react'; +import { motion, useReducedMotion } from 'framer-motion'; +import Reveal from '../deck/Reveal'; +import { useDeck } from '../deck/DeckContext'; + +/* A full-viewport slide laid out as an asymmetric bento grid. Spans via c + (columns of 12) and r (rows). Tiles rise in one after another; metric tiles + get an accent tick. Collapses to one column on narrow screens. + nav/notes are read by the engine (rail label / presenter notes). */ +export type BentoTile = { + k?: string; + fig?: ReactNode; + title?: string; + body?: string; + c?: number; + r?: number; + variant?: 'accent' | 'glow'; + /** full-bleed photo tile: the image covers the tile under a bottom scrim, + text anchors to the bottom. Great for brand/product decks. */ + img?: string; +}; + +export default function Bento({ + kicker, + title, + tiles, +}: { + kicker?: string; + title?: string; + tiles: BentoTile[]; + nav?: string; + notes?: string; +}) { + const { isStatic } = useDeck(); + const reduce = useReducedMotion(); + const animate = !isStatic && !reduce; + return ( +
+
+ + {kicker && ( +
+ {kicker} +
+ )} + {title && ( +

+ {title} +

+ )} +
+
+ {tiles.map((t, i) => ( + +
+ {t.img && ( + <> + + + + )} + {(t.k || t.fig != null) && !t.img && ( +
+ {t.fig != null && } + {t.k &&
{t.k}
} +
+ )} +
+ {t.img && t.k && ( +
+ {t.k} +
+ )} + {t.fig != null && ( +
+ {t.fig} +
+ )} + {t.title &&

{t.title}

} + {t.body &&

{t.body}

} +
+
+
+ ))} +
+
+
+ ); +} diff --git a/src/components/BigNumber.tsx b/src/components/BigNumber.tsx new file mode 100644 index 0000000..f6e6d6a --- /dev/null +++ b/src/components/BigNumber.tsx @@ -0,0 +1,47 @@ +import type { ReactNode } from 'react'; +import Reveal from '../deck/Reveal'; + +/* The giant-figure drama slide — one enormous accent number, a caption, and + an optional source line. Pass a as the value so it animates in. + } + caption="moves through supply chains every year" foot="Source: McKinsey, 2025" /> */ +export default function BigNumber({ + kicker, + value, + caption, + foot, +}: { + kicker?: string; + value: ReactNode; + caption?: ReactNode; + foot?: string; + nav?: string; + notes?: string; +}) { + return ( +
+ + {kicker && ( +
+ {kicker} +
+ )} +
+ +
{value}
+
+ {caption && ( + +

{caption}

+
+ )} + {foot && ( + +
+ {foot} +
+
+ )} +
+ ); +} diff --git a/src/components/BrowserFrame.tsx b/src/components/BrowserFrame.tsx new file mode 100644 index 0000000..12c2f41 --- /dev/null +++ b/src/components/BrowserFrame.tsx @@ -0,0 +1,62 @@ +import type { ReactNode } from 'react'; + +/* Wrap a screenshot or UI mock in browser chrome — traffic dots, back/forward + arrows, and a padlocked URL pill. children fill the body: an , a + , anything. + */ +const Arrow = ({ back }: { back?: boolean }) => ( + + {back ? : } + +); + +const Lock = () => ( + + + + +); + +export default function BrowserFrame({ + url = 'app.acme.com', + children, +}: { + url?: string; + children: ReactNode; +}) { + return ( +
+
+ + + + + + + + + + + + {url} + + +
+
{children}
+
+ ); +} diff --git a/src/components/Charts.tsx b/src/components/Charts.tsx new file mode 100644 index 0000000..d7b597d --- /dev/null +++ b/src/components/Charts.tsx @@ -0,0 +1,233 @@ +import { useEffect, useId, useState, type CSSProperties } from 'react'; +import { motion, useReducedMotion } from 'framer-motion'; +import { useInView } from '../deck/useInView'; +import { useDeck } from '../deck/DeckContext'; + +/* A small hand-built chart kit — bar, line/area, and donut. Each draws itself + in when scrolled into view. All token-driven (one accent), no chart library. + (values label the bars) + (gridlines + live end-dot) + (the number counts up) */ + +export function BarChart({ + data, + height = 200, + showValues = true, +}: { + data: { label: string; value: number }[]; + height?: number; + showValues?: boolean; +}) { + const { isStatic } = useDeck(); + const reduce = useReducedMotion(); + const { ref, inView } = useInView(0.3); + const animate = !isStatic && !reduce; + const max = Math.max(...data.map((d) => d.value)) || 1; + return ( +
+ {data.map((d, i) => ( +
+ {showValues && ( + + {d.value.toLocaleString('en-US')} + + )} +
+ +
+
{d.label}
+
+ ))} +
+ ); +} + +export function LineChart({ + points, + height = 200, +}: { + points: number[]; + height?: number; +}) { + const { isStatic } = useDeck(); + const reduce = useReducedMotion(); + const { ref, inView } = useInView(0.3); + const gid = useId(); + const animate = !isStatic && !reduce; + const w = 300, + h = 120; + const max = Math.max(...points), + min = Math.min(...points); + const span = max - min || 1; + const coords = points.map((p, i) => [ + (i / (points.length - 1)) * w, + h - ((p - min) / span) * (h - 10) - 5, + ]); + const line = coords + .map( + (c, i) => `${i === 0 ? 'M' : 'L'}${c[0].toFixed(1)},${c[1].toFixed(1)}` + ) + .join(' '); + const area = `${line} L${w},${h} L0,${h} Z`; + const [ex, ey] = coords[coords.length - 1]; + return ( +
+ + + + + + + + {/* horizontal gridlines — crisp regardless of the non-uniform scale */} + {[0.25, 0.5, 0.75].map((f) => ( + + ))} + + + {/* no vectorEffect here: non-scaling-stroke breaks the pathLength + draw-in under a stretched viewBox (the dash ends before the path + does and the stroke stops short of its end-point) */} + + + {/* the live end-point — a glowing dot with a radar pulse */} + +
+ ); +} + +export function DonutChart({ + value, + label, + size = 168, +}: { + value: number; + label?: string; + size?: number; +}) { + const { isStatic } = useDeck(); + const reduce = useReducedMotion(); + const { ref, inView } = useInView(0.4); + const [shown, setShown] = useState(isStatic ? value : 0); + const r = 54; + const circ = 2 * Math.PI * r; + + useEffect(() => { + if (isStatic || !inView) return; + if (reduce) { + setShown(value); + return; + } + let raf = 0; + const t0 = performance.now(); + const dur = 1100; + const tick = (now: number) => { + const p = Math.min(1, (now - t0) / dur); + setShown(value * (1 - Math.pow(1 - p, 3))); + if (p < 1) raf = requestAnimationFrame(tick); + }; + raf = requestAnimationFrame(tick); + return () => cancelAnimationFrame(raf); + }, [isStatic, inView, value, reduce]); + + return ( +
+ + + + + {Math.round(shown)}% + + + {label &&
{label}
} +
+ ); +} diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx new file mode 100644 index 0000000..ccd035a --- /dev/null +++ b/src/components/Chat.tsx @@ -0,0 +1,71 @@ +import Build from '../deck/Build'; +import Reveal from '../deck/Reveal'; + +/* A chat-conversation slide — user bubbles on the accent, assistant bubbles on + the surface, inside a titled window. Each message is a click-build beat: + advance the deck (→ / space) to reveal the exchange line by line. Ideal for + AI-product decks. + */ +export type ChatMessage = { from: 'user' | 'ai'; text: string }; + +export default function Chat({ + kicker, + title, + name = 'Assistant', + messages, +}: { + kicker?: string; + title?: string; + name?: string; + messages: ChatMessage[]; + nav?: string; + notes?: string; +}) { + return ( +
+
+ + {kicker && ( +
+ {kicker} +
+ )} + {title && ( +

+ {title} +

+ )} +
+ +
+
+ + {name} +
+
+ {messages.map((m, i) => ( + + {m.text} + + ))} +
+
+
+
+
+ ); +} diff --git a/src/components/CodeWindow.tsx b/src/components/CodeWindow.tsx new file mode 100644 index 0000000..e186432 --- /dev/null +++ b/src/components/CodeWindow.tsx @@ -0,0 +1,99 @@ +import { Fragment, type ReactNode } from 'react'; + +/* A macOS-style code window with a gutter-ruled line-number column and + restrained syntax tinting. Highlighting is intentionally minimal (keywords, + strings, numbers, comments) to stay on-brand — one accent, no rainbow. + */ +const KW = + /\b(const|let|var|function|return|if|else|for|while|import|from|export|default|class|new|await|async|type|interface|extends|implements|public|private|true|false|null|undefined)\b/g; +const NUM = /(\b\d+(?:\.\d+)?\b)/g; + +function tintNumbers(s: string, key: number): ReactNode { + const parts = s.split(NUM); + if (parts.length === 1) return s; + return ( + + {parts.map((p, k) => + k % 2 === 1 ? ( + + {p} + + ) : ( + p + ) + )} + + ); +} + +function hl(line: string): ReactNode { + // split off a trailing line comment + const ci = line.indexOf('//'); + const code = ci >= 0 ? line.slice(0, ci) : line; + const comment = ci >= 0 ? line.slice(ci) : ''; + // strings first (odd indices), then keywords, then numeric literals + const segs = code.split( + /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g + ); + return ( + <> + {segs.map((seg, i) => + i % 2 === 1 ? ( + + {seg} + + ) : ( + + {seg.split(KW).map((s, j) => + j % 2 === 1 ? ( + + {s} + + ) : ( + tintNumbers(s, j) + ) + )} + + ) + )} + {comment && {comment}} + + ); +} + +export default function CodeWindow({ + title = 'app.tsx', + code, + highlight = [], +}: { + title?: string; + code: string; + highlight?: number[]; +}) { + const lines = code.replace(/\n$/, '').split('\n'); + return ( +
+
+ + + + + + {title} +
+
+        
+          {lines.map((ln, i) => (
+            
+ {i + 1} + {ln.length ? hl(ln) : ' '} +
+ ))} +
+
+
+ ); +} diff --git a/src/components/Comparison.tsx b/src/components/Comparison.tsx new file mode 100644 index 0000000..8f606c3 --- /dev/null +++ b/src/components/Comparison.tsx @@ -0,0 +1,98 @@ +/* A "us vs. them" feature matrix. One value column is highlighted (your + column) and reads as a framed accent strip through the table; booleans + render as circled check/cross chips; rows cascade in on view. + + values are booleans (→ ✓/✗) or strings; `highlight` indexes the value columns. */ +import type { CSSProperties } from 'react'; +import { motion, useReducedMotion } from 'framer-motion'; +import { useInView } from '../deck/useInView'; + +export type CompRow = { label: string; values: (boolean | string)[] }; + +const Check = () => ( + + + +); +const Cross = () => ( + + + +); + +export default function Comparison({ + cols, + rows, + highlight = 0, +}: { + cols: string[]; + rows: CompRow[]; + highlight?: number; +}) { + const { ref, inView } = useInView(0.25); + const reduce = useReducedMotion(); + const vcols = cols.length - 1; + return ( +
+
+ {cols.map((c, i) => ( +
+ {c} +
+ ))} +
+ {rows.map((r, ri) => ( + +
{r.label}
+ {r.values.map((v, vi) => ( +
+ {typeof v === 'boolean' ? ( + + {v ? : } + + ) : ( + {v} + )} +
+ ))} +
+ ))} +
+ ); +} diff --git a/src/components/Contrast.tsx b/src/components/Contrast.tsx new file mode 100644 index 0000000..6285a09 --- /dev/null +++ b/src/components/Contrast.tsx @@ -0,0 +1,116 @@ +import type { ReactNode } from 'react'; +import { motion, useReducedMotion } from 'framer-motion'; +import Reveal from '../deck/Reveal'; +import { useDeck } from '../deck/DeckContext'; + +/* The before/after (problem → solution) slide: a muted panel against an + accent-lit panel. Left points get faint crosses, right points get accent + checks; the lit panel lands second, with the glow. + */ +export type ContrastPanel = { + label?: string; + title?: string; + points: ReactNode[]; +}; + +const Cross = () => ( + + + +); +const Check = () => ( + + + +); + +export default function Contrast({ + kicker, + title, + left, + right, +}: { + kicker?: string; + title?: string; + left: ContrastPanel; + right: ContrastPanel; + nav?: string; + notes?: string; +}) { + const { isStatic } = useDeck(); + const reduce = useReducedMotion(); + const animate = !isStatic && !reduce; + const panel = (p: ContrastPanel, lit: boolean, i: number) => ( + + {p.label && ( + {p.label} + )} + {p.title &&

{p.title}

} +
+ {p.points.map((pt, pi) => ( +
+ {lit ? : } + {pt} +
+ ))} +
+
+ ); + return ( +
+
+ + {kicker && ( +
+ {kicker} +
+ )} + {title && ( +

+ {title} +

+ )} +
+
+ {panel(left, false, 0)} + {panel(right, true, 1)} +
+
+
+ ); +} diff --git a/src/components/CountUp.tsx b/src/components/CountUp.tsx new file mode 100644 index 0000000..98677a2 --- /dev/null +++ b/src/components/CountUp.tsx @@ -0,0 +1,78 @@ +import { useEffect, useRef, useState } from 'react'; +import { useDeck } from '../deck/DeckContext'; + +/* A number that animates 0 → to when its slide becomes active, with a + quintic ease-out so the last digits settle gently. Inherits color, so + inside a .figure / .stat-value it picks up the accent gradient. Renders + the final value instantly in thumbnails and under reduced motion. */ +export default function CountUp({ + to, + from = 0, + duration = 1500, + decimals = 0, + prefix = '', + suffix = '', +}: { + to: number; + from?: number; + duration?: number; + decimals?: number; + prefix?: string; + suffix?: string; +}) { + const { isStatic } = useDeck(); + const ref = useRef(null); + const [val, setVal] = useState(isStatic ? to : from); + const started = useRef(false); + + useEffect(() => { + if (isStatic) { + setVal(to); + return; + } + const el = ref.current; + if (!el) return; + let raf = 0; + const run = () => { + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + setVal(to); + return; + } + const t0 = performance.now(); + const tick = (now: number) => { + const p = Math.min(1, (now - t0) / duration); + setVal(from + (to - from) * (1 - Math.pow(1 - p, 5))); + if (p < 1) raf = requestAnimationFrame(tick); + }; + raf = requestAnimationFrame(tick); + }; + const io = new IntersectionObserver( + (entries) => + entries.forEach((e) => { + if (e.isIntersecting && !started.current) { + started.current = true; + run(); + } + }), + { threshold: 0.4 } + ); + io.observe(el); + return () => { + io.disconnect(); + cancelAnimationFrame(raf); + }; + }, [isStatic, to, from, duration]); + + const text = + prefix + + val.toLocaleString('en-US', { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }) + + suffix; + return ( + + {text} + + ); +} diff --git a/src/components/Cover.tsx b/src/components/Cover.tsx new file mode 100644 index 0000000..e050cd1 --- /dev/null +++ b/src/components/Cover.tsx @@ -0,0 +1,56 @@ +import type { ReactNode } from 'react'; +import Reveal from '../deck/Reveal'; + +/* The standardized opening slide: kicker → display title → subtitle cascade, + an optional full-bleed background image under a theme-correct scrim, and an + optional foot line (date · presenter · confidential). + Acme} + subtitle="Answers, not dashboards." image="/cover.webp" foot="June 2026 · Dana Kim" /> */ +export default function Cover({ + kicker, + title, + subtitle, + image, + foot, +}: { + kicker?: string; + title: ReactNode; + subtitle?: ReactNode; + image?: string; + foot?: string; + nav?: string; + notes?: string; +}) { + return ( +
+ {image && ( + <> + +
+ + )} + + {kicker && ( +
+ {kicker} +
+ )} +
+ +

{title}

+
+ {subtitle && ( + +

+ {subtitle} +

+
+ )} + {foot && ( + +
{foot}
+
+ )} +
+ ); +} diff --git a/src/components/Globe.tsx b/src/components/Globe.tsx new file mode 100644 index 0000000..6ec45f5 --- /dev/null +++ b/src/components/Globe.tsx @@ -0,0 +1,435 @@ +import { useEffect, useRef, type ReactNode } from 'react'; +import { useReducedMotion } from 'framer-motion'; +import Reveal from '../deck/Reveal'; +import { useDeck } from '../deck/DeckContext'; + +/* A 3D-globe slide, hand-built on Canvas2D (no WebGL, no dependencies): a + slowly-rotating, drag-to-spin dotted earth with accent markers at real + coordinates, DOM-bound label chips that ride the globe (and fade behind + it), great-circle arcs between locations, and stat rows beside it. Colors + are probed from the live tokens, so re-theming recolors the globe too. + ONLY for a genuinely geographic story — users or revenue by country, + market entry, presence. + + Thumbnails render a static disc; reduced motion stops the auto-spin. */ +export type GlobeMarker = { + location: [number, number]; + size?: number; + /** chip anchored to the marker (e.g. a city or region code) */ + label?: string; + /** accent badge beside the label (e.g. a share or throughput) */ + value?: string; +}; +export type GlobeArc = { from: [number, number]; to: [number, number] }; +export type GlobeStat = { value: ReactNode; label: string }; + +/* 256×128 equirectangular land mask (white = land). From the cobe project + (MIT, shuding/cobe), map data via Wikipedia's blank world map. */ +const LAND_MASK = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAACAAQAAAADMzoqnAAAAAXNSR0IArs4c6QAABA5JREFUeNrV179uHEUAx/Hf3JpbF+E2VASBsmVKTBcpKJs3SMEDcDwBiVJAAewYEBUivIHT0uUBIt0YCovKD0CRjUC4QfHYh8hYXu+P25vZ2Zm9c66gMd/GJ/tz82d3bk8GN4SrByYF2366FNTACIAkivVAAazQdnf3MvAlbNUQfOPAdQDvSAimMWhwy4I2g4SU+Kp04ISLpPBAKLxPyic3O/CCi+Y7rUJbiodcpDOFY7CgxCEXmdYD2EYK2s5lApOx5pEDDYCUwM1XdJUwBV11QQMg59kePSCaPAASQMEL2hwo6TJFgxpg+TgC2ymXPbuvc40awr3D1QCFfbH9kcoqAOkZozpQo0aqAGQRKCog/+tjkgbNFEtg2FffBvBGlSxHoAaAa1u6X4PBAwDiR8FFsrQgeUhfJTSALaB9jy5NCybJPn1SVFiWk7ywN+KzhH1aKAuydhGkbEF4lWohLXDXavlyFgHY7LBnLRdlAP6BS5Cc8RfVDXbkwN/oIvmY+6obbNeBP0JwTuMGu9gTzy1Q4RS/cWpfzszeYwd+CAFrtBW/Hur0gLbJGlD+/OjVwe/drfBxkbbg63dndEDfiEBlAd7ac0BPe1D6Jd8dfbLH+RI0OzseFB5s01/M+gMdAeluLOCAuaUA9Lezo/vSgXoCX9rtEiXnp7Q1W/CNyWcd8DXoS6jH/YZ5vAJEWY2dXFQe2TUgaFaNejCzJ98g6HnlVrsE58sDcYqg+9XY75fPqdoh/kRQWiXKg8MWlJQxUFMPjqnyujhFBE7UxIMjyszk0QwQlFsezImsyvUYYYVED2pk6m0Tg8T04Fwjk2kdAwSACqlM6gRRt3vQYAFGX0Ah7Ebx1H+MDRI5ui0QldH4j7FGcm90XdxD2Jg1AOEAVAKhEFXSn4cKUELurIAKwJ3MArypPscQaLhJFICJ0ohjDySAdH8AhDtCiTuMycH8CXzhH9jUACAO5uMhoAwA5i+T6WAKmmAqnLy80wxHqIPFYpqCwxGaYLt4Dyievg5kEoVEUAhs6pqKgFtDQYOuaXypaWKQfIuwwoGSZgfLsu/XAtI8cGN+h7Cc1A5oLOMhwlIPXuhu48AIvsSBkvtV9wsJRKCyYLfq5lTrQMFd1a262oqBck9K1V0YjQg0iEYYgpS1A9GlXQV5cykwm4A7BzVsxQqo7E+zCegO7Ma7yKgsuOcfKbMBwLC8wvVNYDsANYalEpOAa6zpWjTeMKGwEwC1CiQewJc5EKfgy7GmRAZA4vUVGwE2dPM/g0xuAInE/yG5aZ8ISxWGfYigUVbdyBElTHh2uCwGdfCkOLGgQVBh3Ewp+/QK4CDlR5Ws/Zf7yhCf8pH7vinWAvoVCQ6zz0NX5V/6GkAVV+2/5qsJ/gU8bsxpM8IeAQAAAABJRU5ErkJggg=='; + +/* resolve a CSS custom property to 0–255 rgb by letting the browser compute it */ +function probe( + cssVar: string, + prop: 'color' | 'backgroundColor' = 'color' +): [number, number, number] { + const el = document.createElement('div'); + el.style.cssText = `position:absolute;visibility:hidden;${ + prop === 'color' ? 'color' : 'background-color' + }:var(${cssVar})`; + document.body.appendChild(el); + const m = getComputedStyle(el)[prop].match(/[\d.]+/g); + el.remove(); + if (!m || m.length < 3) return [128, 128, 128]; + return [+m[0], +m[1], +m[2]]; +} +const mix = (a: number[], b: number[], t: number) => + [0, 1, 2].map((i) => Math.round(a[i] * (1 - t) + b[i] * t)) as [ + number, + number, + number + ]; +const rgba = (c: number[], a: number) => `rgba(${c[0]},${c[1]},${c[2]},${a})`; +const clamp01 = (v: number) => Math.min(1, Math.max(0, v)); + +/* land points on a Fibonacci sphere, filtered by the mask — computed once */ +let landCache: Promise | null = null; +function landPoints(): Promise { + if (landCache) return landCache; + landCache = new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + const W = 256, + H = 128; + const c = document.createElement('canvas'); + c.width = W; + c.height = H; + const g = c.getContext('2d')!; + g.drawImage(img, 0, 0); + const data = g.getImageData(0, 0, W, H).data; + const N = 12000, + GA = Math.PI * (3 - Math.sqrt(5)); + const pts: number[] = []; + for (let i = 0; i < N; i++) { + const y = 1 - (2 * i) / (N - 1); + const rad = Math.sqrt(1 - y * y); + const lng = (i * GA) % (2 * Math.PI); + const x = Math.cos(lng) * rad; + const z = Math.sin(lng) * rad; + const lat = Math.asin(y); + const l = Math.atan2(z, x); // -π..π + const u = Math.min(W - 1, Math.floor((l / (2 * Math.PI) + 0.5) * W)); + const v = Math.min(H - 1, Math.floor((0.5 - lat / Math.PI) * H)); + if (data[(v * W + u) * 4] > 128) + pts.push( + Math.cos(lat) * Math.sin(l), + Math.sin(lat), + Math.cos(lat) * Math.cos(l) + ); + } + resolve(new Float32Array(pts)); + }; + img.src = LAND_MASK; + }); + return landCache; +} + +const toVec = ([lat, lng]: [number, number]): [number, number, number] => { + const p = (lat * Math.PI) / 180, + l = (lng * Math.PI) / 180; + return [Math.cos(p) * Math.sin(l), Math.sin(p), Math.cos(p) * Math.cos(l)]; +}; + +function GlobeCanvas({ + markers, + arcs = [], +}: { + markers: GlobeMarker[]; + arcs?: GlobeArc[]; +}) { + const wrapRef = useRef(null); + const canvasRef = useRef(null); + const labelRefs = useRef<(HTMLDivElement | null)[]>([]); + const reduce = useReducedMotion(); + const drag = useRef({ down: false, startX: 0, r: 0, rBase: 0 }); + + useEffect(() => { + const canvas = canvasRef.current; + const wrap = wrapRef.current; + if (!canvas || !wrap) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + const fg = probe('--fg'); + const bg = probe('--bg', 'backgroundColor'); + const primary = probe('--primary'); + // carry the theme: land dots and the atmosphere lean toward the accent + const dot = mix(mix(bg, fg, 0.6), primary, 0.24); + const atmo = mix(fg, primary, 0.45); + const theta = 0.28; + const dpr = Math.min(window.devicePixelRatio || 1, 2); + let phi = 1.9; // open on the Americas + let raf = 0; + let size = 0; + const marks = markers.map((m) => ({ + v: toVec(m.location), + s: m.size ?? 0.06, + })); + // arcs: lifted great-circle samples (slerp + radial lift), precomputed once + const K = 44; + const arcPts = arcs.map((a) => { + const A = toVec(a.from), + B = toVec(a.to); + const d = + clamp01((A[0] * B[0] + A[1] * B[1] + A[2] * B[2] + 1) / 2) * 2 - 1; + const ang = Math.acos(Math.min(1, Math.max(-1, d))); + const sinAng = Math.sin(ang) || 1e-4; + const lift = 0.05 + 0.11 * (ang / Math.PI); + const p = new Float32Array((K + 1) * 3); + for (let i = 0; i <= K; i++) { + const t = i / K; + const w0 = Math.sin((1 - t) * ang) / sinAng; + const w1 = Math.sin(t * ang) / sinAng; + const r = 1 + lift * Math.sin(Math.PI * t); + p[i * 3] = (A[0] * w0 + B[0] * w1) * r; + p[i * 3 + 1] = (A[1] * w0 + B[1] * w1) * r; + p[i * 3 + 2] = (A[2] * w0 + B[2] * w1) * r; + } + return p; + }); + let pts: Float32Array | null = null; + landPoints().then((p) => { + pts = p; + }); + + const resize = () => { + size = wrap.offsetWidth; + canvas.width = size * dpr; + canvas.height = size * dpr; + }; + resize(); + const ro = new ResizeObserver(resize); + ro.observe(wrap); + + const cosT = Math.cos(theta), + sinT = Math.sin(theta); + // smooth horizon fade: 0 at the limb, 1 once safely on the visible side + const smooth = (zr: number) => { + const t = clamp01(zr / 0.22); + return t * t * (3 - 2 * t); + }; + // arc-point visibility: in front of the globe, or outside its silhouette + const arcVis = (xr: number, yr: number, zr: number) => + clamp01(Math.max(zr / 0.08, (Math.hypot(xr, yr) - 1) / 0.04)); + + const render = () => { + const s = size * dpr, + cx = s / 2, + cy = s / 2, + R = s * 0.4; + ctx.clearRect(0, 0, s, s); + // atmosphere — limb brightening, then a soft falloff that reaches ZERO + // exactly at the canvas half-size so nothing ever clips square + const G = s * 0.5, + inner = R * 0.8; + const at = (r: number) => clamp01((r - inner) / (G - inner)); + const glow = ctx.createRadialGradient(cx, cy, inner, cx, cy, G); + glow.addColorStop(0, rgba(atmo, 0)); + glow.addColorStop(at(R * 0.97), rgba(atmo, 0.05)); + glow.addColorStop(at(R * 0.995), rgba(atmo, 0.2)); + glow.addColorStop(at(R * 1.01), rgba(atmo, 0.22)); + glow.addColorStop(at(R * 1.07), rgba(atmo, 0.1)); + glow.addColorStop(at(R * 1.15), rgba(atmo, 0.035)); + glow.addColorStop(1, rgba(atmo, 0)); + ctx.fillStyle = glow; + ctx.beginPath(); + ctx.arc(cx, cy, G, 0, 7); + ctx.fill(); + const body = ctx.createRadialGradient( + cx - R * 0.3, + cy - R * 0.35, + R * 0.1, + cx, + cy, + R + ); + body.addColorStop(0, rgba(mix(fg, primary, 0.2), 0.09)); + body.addColorStop(1, rgba(fg, 0.02)); + ctx.fillStyle = body; + ctx.beginPath(); + ctx.arc(cx, cy, R, 0, 7); + ctx.fill(); + const a = phi + drag.current.r; + const cosP = Math.cos(a), + sinP = Math.sin(a); + const proj = ( + x: number, + y: number, + z: number + ): [number, number, number] => { + const xr = x * cosP + z * sinP; + const z1 = z * cosP - x * sinP; + return [xr, y * cosT - z1 * sinT, y * sinT + z1 * cosT]; + }; + // land dots — round, sized and faded by depth, easing out at the limb + if (pts) { + ctx.fillStyle = rgba(dot, 1); + for (let i = 0; i < pts.length; i += 3) { + const [xr, yr, zr] = proj(pts[i], pts[i + 1], pts[i + 2]); + if (zr <= 0) continue; + const e = smooth(zr); + const d = (0.5 + 0.75 * zr) * dpr * (0.6 + 0.4 * e); + ctx.globalAlpha = (0.22 + 0.62 * zr) * e; + ctx.beginPath(); + ctx.arc(cx + xr * R, cy - yr * R, d, 0, 7); + ctx.fill(); + } + } + // arcs — great-circle connections, hidden where they pass behind + ctx.lineCap = 'round'; + ctx.lineWidth = 1.3 * dpr; + for (const p of arcPts) { + let prev = proj(p[0], p[1], p[2]); + let prevV = arcVis(prev[0], prev[1], prev[2]); + for (let i = 1; i <= K; i++) { + const cur = proj(p[i * 3], p[i * 3 + 1], p[i * 3 + 2]); + const curV = arcVis(cur[0], cur[1], cur[2]); + const alpha = Math.min(prevV, curV) * 0.8; + if (alpha > 0.01) { + ctx.globalAlpha = alpha; + ctx.strokeStyle = rgba(primary, 1); + ctx.beginPath(); + ctx.moveTo(cx + prev[0] * R, cy - prev[1] * R); + ctx.lineTo(cx + cur[0] * R, cy - cur[1] * R); + ctx.stroke(); + } + prev = cur; + prevV = curV; + } + } + // markers — accent dots with a soft halo, easing over the limb too + for (let mi = 0; mi < marks.length; mi++) { + const m = marks[mi]; + const [xr, yr, zr] = proj(m.v[0], m.v[1], m.v[2]); + const e = smooth(zr); + // ride the DOM label chip on this marker (CSS pixels) + const el = labelRefs.current[mi]; + if (el) { + const lx = size * (0.5 + 0.4 * xr); + const ly = size * (0.5 - 0.4 * yr); + el.style.transform = `translate(${lx.toFixed(1)}px, ${ly.toFixed( + 1 + )}px) translate(-50%, calc(-100% - 12px))`; + el.style.opacity = String(e * e); + } + if (zr <= 0) continue; + const mx = cx + xr * R, + my = cy - yr * R; + const mr = (2.4 + m.s * 42) * dpr * (0.7 + 0.3 * zr); + ctx.globalAlpha = e; + const halo = ctx.createRadialGradient(mx, my, 0, mx, my, mr * 3); + halo.addColorStop(0, rgba(primary, 0.5)); + halo.addColorStop(1, rgba(primary, 0)); + ctx.fillStyle = halo; + ctx.beginPath(); + ctx.arc(mx, my, mr * 3, 0, 7); + ctx.fill(); + ctx.fillStyle = rgba(primary, 1); + ctx.beginPath(); + ctx.arc(mx, my, mr, 0, 7); + ctx.fill(); + } + ctx.globalAlpha = 1; + }; + + let last = 0; + const loop = (t: number) => { + if (last && !reduce && !drag.current.down) + phi += ((t - last) / 1000) * 0.22; // rad/s, frame-rate independent + last = t; + render(); + raf = requestAnimationFrame(loop); + }; + raf = requestAnimationFrame(loop); + requestAnimationFrame(() => { + canvas.style.opacity = '1'; + }); + + const down = (e: PointerEvent) => { + drag.current.down = true; + drag.current.startX = e.clientX; + canvas.style.cursor = 'grabbing'; + }; + const move = (e: PointerEvent) => { + if (drag.current.down) + drag.current.r = + drag.current.rBase + (e.clientX - drag.current.startX) / 140; + }; + const up = () => { + drag.current.down = false; + drag.current.rBase = drag.current.r; + canvas.style.cursor = 'grab'; + }; + canvas.addEventListener('pointerdown', down); + window.addEventListener('pointermove', move); + window.addEventListener('pointerup', up); + return () => { + cancelAnimationFrame(raf); + ro.disconnect(); + canvas.removeEventListener('pointerdown', down); + window.removeEventListener('pointermove', move); + window.removeEventListener('pointerup', up); + }; + }, [reduce, markers, arcs]); + + return ( +
+ + {markers.map((m, i) => + m.label ? ( +
{ + labelRefs.current[i] = el; + }} + > + {m.label} + {m.value && {m.value}} +
+ ) : null + )} +
+ ); +} + +export default function Globe({ + kicker, + title, + body, + markers, + arcs, + stats, + flip, +}: { + kicker?: string; + title: ReactNode; + body?: ReactNode; + markers: GlobeMarker[]; + arcs?: GlobeArc[]; + stats?: GlobeStat[]; + flip?: boolean; + nav?: string; + notes?: string; +}) { + const { isStatic } = useDeck(); + return ( +
+
+
+ {kicker && ( + +
{kicker}
+
+ )} + +

+ {title} +

+
+ {body && ( + +
{body}
+
+ )} + {stats && stats.length > 0 && ( + +
+ {stats.map((s, i) => ( +
+ {s.label} + {s.value} +
+ ))} +
+
+ )} +
+
+ {isStatic ? ( +
+
+
+ ) : ( + + )} +
+
+
+ ); +} diff --git a/src/components/Marquee.tsx b/src/components/Marquee.tsx new file mode 100644 index 0000000..8440fd3 --- /dev/null +++ b/src/components/Marquee.tsx @@ -0,0 +1,35 @@ +import { Fragment } from 'react'; + +/* A continuously-scrolling strip — logo wall, value props, tech stack. + Items are separated by small accent diamonds; hovering pauses the strip. */ +export default function Marquee({ + items, + duration = 26, +}: { + items: string[]; + duration?: number; +}) { + const run = (copy: number) => ( + + {items.map((it, i) => ( + + + {it} + + + + ))} + + ); + return ( +
+
+ {run(0)} + {run(1)} +
+
+ ); +} diff --git a/src/components/Pricing.tsx b/src/components/Pricing.tsx new file mode 100644 index 0000000..47cf777 --- /dev/null +++ b/src/components/Pricing.tsx @@ -0,0 +1,115 @@ +import type { CSSProperties } from 'react'; +import { motion, useReducedMotion } from 'framer-motion'; +import Reveal from '../deck/Reveal'; +import { useDeck } from '../deck/DeckContext'; + +/* A pricing slide: 2–4 tier cards, one highlighted with an accent badge. + Feature lists get accent checks; cards rise in staggered. + */ +export type Tier = { + name: string; + price: string; + period?: string; + blurb?: string; + features?: string[]; + highlight?: boolean; + badge?: string; +}; + +const Check = () => ( + + + +); + +export default function Pricing({ + kicker, + title, + tiers, +}: { + kicker?: string; + title?: string; + tiers: Tier[]; + nav?: string; + notes?: string; +}) { + const { isStatic } = useDeck(); + const reduce = useReducedMotion(); + const animate = !isStatic && !reduce; + return ( +
+
+ + {kicker && ( +
+ {kicker} +
+ )} + {title && ( +

+ {title} +

+ )} +
+
+ {tiers.map((t, i) => ( + + {t.highlight && ( + {t.badge ?? 'Most popular'} + )} +
{t.name}
+
+ {t.price} + {t.period && {t.period}} +
+ {t.blurb &&
{t.blurb}
} + {t.features && t.features.length > 0 && ( +
+ {t.features.map((f, fi) => ( +
+ + {f} +
+ ))} +
+ )} +
+ ))} +
+
+
+ ); +} diff --git a/src/components/Quote.tsx b/src/components/Quote.tsx new file mode 100644 index 0000000..b013da7 --- /dev/null +++ b/src/components/Quote.tsx @@ -0,0 +1,67 @@ +import Reveal from '../deck/Reveal'; + +/* A pull-quote slide: an accent quotation mark, the quote at display scale, + and an attribution row with a photo or auto-initials avatar (`img`). Pass + `image` for a full-bleed background photo under a scrim (brand decks). + Don't wrap the text in quotation marks — the mark provides them. + */ +export default function Quote({ + text, + name, + role, + img, + image, +}: { + text: string; + name?: string; + role?: string; + img?: string; + image?: string; + nav?: string; + notes?: string; +}) { + const initials = name + ? name + .split(/\s+/) + .map((w) => w[0]) + .slice(0, 2) + .join('') + .toUpperCase() + : ''; + return ( +
+ {image && ( + <> + +
+ + )} + +
+ “ +
+
+ +

+ {text} +

+
+ {name && ( + +
+ + {img ? {name} : initials} + + +
{name}
+ {role &&
{role}
} +
+
+
+ )} +
+ ); +} diff --git a/src/components/Section.tsx b/src/components/Section.tsx new file mode 100644 index 0000000..c3454f4 --- /dev/null +++ b/src/components/Section.tsx @@ -0,0 +1,47 @@ +import type { ReactNode } from 'react'; +import Reveal from '../deck/Reveal'; + +/* A chapter divider slide — a full-bleed breather between parts of the deck. + An enormous outlined "ghost" number sits behind a centered kicker + display + title, over stronger accent corner glows. Works with no image (ideal for + data/B2B decks) — or pass `image` for a full-bleed photo under a scrim + (brand decks): the ghost number and title sit on top. +
How it works.} /> */ +export default function Section({ + n, + kicker, + title, + image, +}: { + n?: number | string; + kicker?: string; + title: ReactNode; + image?: string; + nav?: string; + notes?: string; +}) { + return ( +
+ {image && ( + <> + +
+ + )} +
+ {n != null && ( +
+ {String(n).padStart(2, '0')} +
+ )} + + {kicker && ( +
+ {kicker} +
+ )} +

{title}

+
+
+ ); +} diff --git a/src/components/Split.tsx b/src/components/Split.tsx new file mode 100644 index 0000000..c9c94b2 --- /dev/null +++ b/src/components/Split.tsx @@ -0,0 +1,49 @@ +import type { ReactNode } from 'react'; +import Reveal from '../deck/Reveal'; + +/* A full-bleed feature slide: text one side, edge-to-edge media the other, + separated by a hairline. The text cascades in (kicker → title → body); + `flip` swaps sides. Stacks vertically on narrow screens. */ +export default function Split({ + kicker, + title, + body, + media, + flip, +}: { + kicker?: string; + title: ReactNode; + body?: ReactNode; + media: ReactNode; + flip?: boolean; + nav?: string; + notes?: string; +}) { + return ( +
+
+
+ {kicker && ( + +
{kicker}
+
+ )} + +

+ {title} +

+
+ {body && ( + +
{body}
+
+ )} +
+
{media}
+
+
+ ); +} diff --git a/src/components/SpotlightCard.tsx b/src/components/SpotlightCard.tsx new file mode 100644 index 0000000..1823078 --- /dev/null +++ b/src/components/SpotlightCard.tsx @@ -0,0 +1,22 @@ +import { useRef, type ReactNode } from 'react'; + +/* A card with a cursor-following accent treatment (the Linear/Vercel hover): + a soft interior bloom AND a border ring that lights up around the pointer. + Wrap content; it provides the card shell + padding. +

*/ +export default function SpotlightCard({ children }: { children: ReactNode }) { + const ref = useRef(null); + function move(e: React.MouseEvent) { + const el = ref.current; + if (!el) return; + const r = el.getBoundingClientRect(); + el.style.setProperty('--mx', `${e.clientX - r.left}px`); + el.style.setProperty('--my', `${e.clientY - r.top}px`); + } + return ( +
+ {children} + +
+ ); +} diff --git a/src/components/StatGrid.tsx b/src/components/StatGrid.tsx new file mode 100644 index 0000000..a5aaae1 --- /dev/null +++ b/src/components/StatGrid.tsx @@ -0,0 +1,75 @@ +import type { ReactNode } from 'react'; +import { motion, useReducedMotion } from 'framer-motion'; +import Reveal from '../deck/Reveal'; +import { useDeck } from '../deck/DeckContext'; + +/* A full-viewport proof slide: responsive auto-fit stat cards, each with an + accent tick and a staggered rise-in. Pass a as a stat `value` + so figures animate in. */ +export type Stat = { value?: ReactNode; label: string; caption?: string }; + +export default function StatGrid({ + kicker, + title, + stats, +}: { + kicker?: string; + title?: string; + stats: Stat[]; + nav?: string; + notes?: string; +}) { + const { isStatic } = useDeck(); + const reduce = useReducedMotion(); + const animate = !isStatic && !reduce; + return ( +
+
+ + {kicker && ( +
+ {kicker} +
+ )} + {title && ( +

+ {title} +

+ )} +
+
+ {stats.map((s, i) => ( + +
+ +
{s.value}
+
{s.label}
+ {s.caption &&
{s.caption}
} +
+
+ ))} +
+
+
+ ); +} diff --git a/src/components/Steps.tsx b/src/components/Steps.tsx new file mode 100644 index 0000000..ebcca35 --- /dev/null +++ b/src/components/Steps.tsx @@ -0,0 +1,93 @@ +import type { CSSProperties, ReactNode } from 'react'; +import { motion, useReducedMotion } from 'framer-motion'; +import Reveal from '../deck/Reveal'; +import { useDeck } from '../deck/DeckContext'; + +/* A horizontal numbered process (the "how it works" slide): mono number + rings joined by a connector that draws in left-to-right as each step + reveals. Stacks vertically on narrow screens (connectors hide). + */ +export type Step = { title: string; body?: ReactNode }; + +export default function Steps({ + kicker, + title, + items, +}: { + kicker?: string; + title?: string; + items: Step[]; + nav?: string; + notes?: string; +}) { + const { isStatic } = useDeck(); + const reduce = useReducedMotion(); + const animate = !isStatic && !reduce; + return ( +
+
+ + {kicker && ( +
+ {kicker} +
+ )} + {title && ( +

+ {title} +

+ )} +
+
+ {items.map((s, i) => ( + +
+ + {String(i + 1).padStart(2, '0')} + + + + +
+

{s.title}

+ {s.body &&

{s.body}

} +
+ ))} +
+
+
+ ); +} diff --git a/src/components/Table.tsx b/src/components/Table.tsx new file mode 100644 index 0000000..35bbfb3 --- /dev/null +++ b/src/components/Table.tsx @@ -0,0 +1,96 @@ +import type { ReactNode } from 'react'; +import { motion, useReducedMotion } from 'framer-motion'; +import { useInView } from '../deck/useInView'; + +/* A data-rich table done properly: uppercase mono-ruled header, hairline rows, + right-aligned tabular numerals (first column left for labels), an optional + accent column or row, staggered row reveal, and a source caption. For real + data — a feature matrix belongs in . Self-centers standalone. +
+ Keep it ≤5 columns and ≤7 rows — a paged slide can't scroll. */ +export type TableColumn = + | string + | { label: string; align?: 'left' | 'right' | 'center' }; +export type TableCell = string | number | ReactNode; + +export default function Table({ + columns, + rows, + highlightCol, + highlightRow, + caption, +}: { + columns: TableColumn[]; + rows: TableCell[][]; + highlightCol?: number; + highlightRow?: number; + caption?: string; +}) { + const { ref, inView } = useInView(0.25); + const reduce = useReducedMotion(); + const align = (c: TableColumn) => + typeof c === 'string' ? undefined : c.align; + const alignClass = (i: number) => { + const a = align(columns[i]); + return a ? ` al-${a[0]}` : ''; + }; + return ( +
+
+
+ + + {columns.map((c, i) => ( + + ))} + + + + {rows.map((r, ri) => ( + + {r.map((cell, ci) => ( + + ))} + + ))} + +
+ {typeof c === 'string' ? c : c.label} +
+ {cell} +
+
+ {caption && ( +
+ {caption} +
+ )} +
+ ); +} diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx new file mode 100644 index 0000000..015cfaa --- /dev/null +++ b/src/components/Tabs.tsx @@ -0,0 +1,82 @@ +import { useId, useState, type ReactNode } from 'react'; +import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'; + +/* Tabbed content with a sliding accent pill (framer layoutId, scoped per + instance so multiple Tabs coexist) and a cross-fade between panels. The bar + is keyboard-navigable (←/→) and the block self-centers when standalone. + }, …]} /> */ +export type Tab = { label: string; content: ReactNode }; + +export default function Tabs({ + tabs, + defaultTab = 0, +}: { + tabs: Tab[]; + defaultTab?: number; +}) { + const pillId = useId(); + const reduce = useReducedMotion(); + const [active, setActive] = useState(defaultTab); + + function onKeyDown(e: React.KeyboardEvent) { + if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return; + e.preventDefault(); + e.stopPropagation(); // don't page the deck while switching tabs + const next = + e.key === 'ArrowRight' + ? (active + 1) % tabs.length + : (active - 1 + tabs.length) % tabs.length; + setActive(next); + const bar = e.currentTarget as HTMLElement; + (bar.children[next] as HTMLElement | undefined)?.focus(); + } + + return ( +
+
+
+ {tabs.map((t, i) => ( + + ))} +
+
+
+ + + {tabs[active].content} + + +
+
+ ); +} diff --git a/src/components/Team.tsx b/src/components/Team.tsx new file mode 100644 index 0000000..7012417 --- /dev/null +++ b/src/components/Team.tsx @@ -0,0 +1,83 @@ +import { motion, useReducedMotion } from 'framer-motion'; +import Reveal from '../deck/Reveal'; +import { useDeck } from '../deck/DeckContext'; + +/* A people-grid slide: photo avatars (or auto-initials on the accent) with + name + role. Cards rise in staggered; the grid wraps responsively. + */ +export type Person = { name: string; role?: string; img?: string }; + +export default function Team({ + kicker, + title, + people, +}: { + kicker?: string; + title?: string; + people: Person[]; + nav?: string; + notes?: string; +}) { + const { isStatic } = useDeck(); + const reduce = useReducedMotion(); + const animate = !isStatic && !reduce; + return ( +
+
+ + {kicker && ( +
+ {kicker} +
+ )} + {title && ( +

+ {title} +

+ )} +
+
+ {people.map((p, i) => { + const initials = p.name + .split(/\s+/) + .map((w) => w[0]) + .slice(0, 2) + .join('') + .toUpperCase(); + return ( + + + {p.img ? {p.name} : initials} + +
{p.name}
+ {p.role &&
{p.role}
} +
+ ); + })} +
+
+
+ ); +} diff --git a/src/components/TiltCard.tsx b/src/components/TiltCard.tsx new file mode 100644 index 0000000..669a607 --- /dev/null +++ b/src/components/TiltCard.tsx @@ -0,0 +1,63 @@ +import { useRef, type ReactNode } from 'react'; +import { + motion, + useMotionValue, + useSpring, + useReducedMotion, +} from 'framer-motion'; +import { useDeck } from '../deck/DeckContext'; + +/* Mouse-tracked 3D tilt + glare, smoothed with springs. The pointer is + measured against a STATIC outer frame — never the tilting element itself — + so the tilt can't feed back into its own hit-box and wobble. Wrap ONE hero + visual per slide. Flat in thumbnails and under reduced motion. */ +export default function TiltCard({ children }: { children: ReactNode }) { + const { isStatic } = useDeck(); + const reduce = useReducedMotion(); + const frameRef = useRef(null); + const rx = useMotionValue(0); + const ry = useMotionValue(0); + const srx = useSpring(rx, { stiffness: 260, damping: 26, mass: 0.5 }); + const sry = useSpring(ry, { stiffness: 260, damping: 26, mass: 0.5 }); + + if (isStatic || reduce) { + return ( +
+
+ {children} + +
+
+ ); + } + + function move(e: React.MouseEvent) { + const el = frameRef.current; + if (!el) return; + const r = el.getBoundingClientRect(); + const px = (e.clientX - r.left) / r.width - 0.5; + const py = (e.clientY - r.top) / r.height - 0.5; + rx.set(-py * 7); + ry.set(px * 10); + el.style.setProperty('--gx', `${((px + 0.5) * 100).toFixed(1)}%`); + el.style.setProperty('--gy', `${((py + 0.5) * 100).toFixed(1)}%`); + } + function leave() { + rx.set(0); + ry.set(0); + } + + return ( +
+ + {children} + + +
+ ); +} diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx new file mode 100644 index 0000000..78ee2c0 --- /dev/null +++ b/src/components/Timeline.tsx @@ -0,0 +1,48 @@ +import { motion, useReducedMotion } from 'framer-motion'; +import type { ReactNode } from 'react'; +import { useInView } from '../deck/useInView'; + +/* A vertical timeline / roadmap. The connector draws in, milestones are + glowing rings with accent cores, and time labels are mono chips. Reveals + in sequence when scrolled into view; self-centers when standalone. + */ +export type TimelineItem = { time: string; title: string; body?: ReactNode }; + +export default function Timeline({ items }: { items: TimelineItem[] }) { + const { ref, inView } = useInView(0.2); + const reduce = useReducedMotion(); + return ( +
+
+ +
+
+ {items.map((it, i) => ( + + +
+ {it.time} +

{it.title}

+ {it.body &&

{it.body}

} +
+
+ ))} +
+
+ ); +} diff --git a/src/components/VisualDashboard.tsx b/src/components/VisualDashboard.tsx new file mode 100644 index 0000000..35089ae --- /dev/null +++ b/src/components/VisualDashboard.tsx @@ -0,0 +1,150 @@ +import { useEffect, useRef, useState } from 'react'; +import CountUp from './CountUp'; +import { useDeck } from '../deck/DeckContext'; + +/* Example analytics visual (a self-drawing dashboard mock): KPI cards with + live sparklines, a gridded area chart, and a weekly bar row. ONLY use it + when the deck is genuinely about data — otherwise build a topic-fit visual + in the same .vframe shell. Draws in when its slide becomes active. */ +const bars = [44, 62, 55, 78, 96, 70, 58]; +const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + +const kpis = [ + { + l: 'Revenue', + v: , + d: '▲ 18.2%', + spark: 'M1,14 L9,11 L17,12.5 L25,8 L33,9.5 L41,5 L49,3', + }, + { + l: 'Active users', + v: , + d: '▲ 9.4%', + spark: 'M1,15 L9,13 L17,14 L25,10 L33,11.5 L41,7 L49,4.5', + }, + { + l: 'Churn', + v: , + d: '▼ 0.6%', + spark: 'M1,4 L9,6 L17,5 L25,9 L33,8 L41,12 L49,14', + }, +]; + +export default function VisualDashboard() { + const { isStatic } = useDeck(); + const ref = useRef(null); + const [shown, setShown] = useState(isStatic); + + useEffect(() => { + if (isStatic) { + setShown(true); + return; + } + const el = ref.current; + if (!el) return; + const io = new IntersectionObserver( + (entries) => + entries.forEach((e) => { + if (e.isIntersecting) setShown(true); + }), + { threshold: 0.3 } + ); + io.observe(el); + return () => io.disconnect(); + }, [isStatic]); + + return ( +
+
+
+ + Overview · Last 30 days +
+
+ + Live +
+
+
+ {kpis.map((k) => ( +
+
{k.l}
+
{k.v}
+
+
{k.d}
+ + + +
+
+ ))} +
+ + + + + + + + {[38, 76, 114].map((y) => ( + + ))} + + {/* no vectorEffect: it breaks the dash-based draw-in (stroke stops short) */} + + +
+ {bars.map((h, i) => ( +
+
+ +
+
{days[i]}
+
+ ))} +
+
+ ); +} diff --git a/src/deck/Annotator.tsx b/src/deck/Annotator.tsx new file mode 100644 index 0000000..c37e1e3 --- /dev/null +++ b/src/deck/Annotator.tsx @@ -0,0 +1,493 @@ +import { useEffect, useRef, useState } from 'react'; + +/* State-of-the-art annotation: a full-screen canvas + a floating tool bar. + Tools: pen, highlighter, line, arrow, rectangle, ellipse, eraser; a color + palette; three sizes; undo + clear. Strokes are stored as data (per slide, + in `store`) so they PERSIST on the slide they were made on, and undo / + stroke-erase work cleanly. The bar + drawing are only interactive when + `active`; otherwise the canvas just displays the slide's saved annotations. + + CONTENT-ANCHORED: when a stroke is committed it is anchored to the block + element under its center (stored as a child-index path from the slide + stage) with coordinates relative to THAT element's box. Because the slide + DOM is identical at every viewport — only CSS reflows it — the same path + resolves on any screen, so a circle drawn around a stat on a laptop rings + the same stat on a phone, wherever the layout moved it. Strokes over empty + background anchor to the stage and scale with the viewport. */ + +type Tool = + | 'pen' + | 'highlighter' + | 'line' + | 'arrow' + | 'rect' + | 'ellipse' + | 'eraser'; +type Pt = { x: number; y: number }; // relative to the stroke's anchor box (0..1) +export type Stroke = { + tool: Tool; + color: string; + size: number; + points: Pt[]; + anchor?: string; +}; +type Box = { left: number; top: number; width: number; height: number }; + +const TOOLS: { id: Tool; label: string; path: string }[] = [ + { id: 'pen', label: 'Pen', path: 'M4 20h4L18 10a2 2 0 0 0-3-3L5 17z' }, + { + id: 'highlighter', + label: 'Highlighter', + path: 'M4 20h5l8-8-4-4-9 9zM13 7l4 4', + }, + { id: 'line', label: 'Line', path: 'M5 19L19 5' }, + { id: 'arrow', label: 'Arrow', path: 'M6 18L18 6M18 6h-6M18 6v6' }, + { id: 'rect', label: 'Rectangle', path: 'M4 6h16v12H4z' }, + { + id: 'ellipse', + label: 'Ellipse', + path: 'M12 6c4.5 0 8 2.7 8 6s-3.5 6-8 6-8-2.7-8-6 3.5-6 8-6z', + }, + { + id: 'eraser', + label: 'Eraser', + path: 'M8 18l-4-4a2 2 0 0 1 0-3l7-7a2 2 0 0 1 3 0l4 4a2 2 0 0 1 0 3l-7 7zM7 17h11', + }, +]; +const COLORS = ['var(--primary)', '#ffffff', '#ef4444', '#f5b73a', '#4aa8ff']; +const SIZES = [3, 6, 11]; + +const IconUndo = () => ( + + + + +); +const IconTrash = () => ( + + + +); + +/* ── content anchoring ─────────────────────────────────────────────── */ +const stage = () => document.querySelector('.slide-stage'); +const viewportBox = (): Box => ({ + left: 0, + top: 0, + width: window.innerWidth, + height: window.innerHeight, +}); + +/* child-index path from the stage root, e.g. "0.2.1" ("" = the stage itself) */ +function pathOf(el: Element): string { + const root = stage(); + const parts: number[] = []; + let cur: Element | null = el; + while (cur && cur !== root) { + const parent: Element | null = cur.parentElement; + if (!parent) return ''; + parts.unshift(Array.prototype.indexOf.call(parent.children, cur)); + cur = parent; + } + return cur === root ? parts.join('.') : ''; +} +function resolveAnchor(path?: string): Box | null { + if (path === undefined) return null; + const root = stage(); + if (!root) return null; + let cur: Element = root; + if (path !== '') { + for (const i of path.split('.').map(Number)) { + const next = cur.children[i]; + if (!next) return null; + cur = next; + } + } + const r = cur.getBoundingClientRect(); + return r.width > 4 && r.height > 4 ? r : null; +} +/* the block element under a client point (skipping the canvas + chrome) */ +function anchorAt(cx: number, cy: number): string { + for (const el of document.elementsFromPoint(cx, cy)) { + if (!el.closest('.slide-stage')) continue; + let block: Element | null = el; + while ( + block && + block !== stage() && + getComputedStyle(block).display === 'inline' + ) + block = block.parentElement; + return block ? pathOf(block) : ''; + } + return ''; +} +/* a stroke's points mapped to client pixels at the CURRENT layout */ +function clientPts(s: Stroke): Pt[] { + const r = resolveAnchor(s.anchor) ?? viewportBox(); + return s.points.map((p) => ({ + x: r.left + p.x * r.width, + y: r.top + p.y * r.height, + })); +} + +function distToSeg( + px: number, + py: number, + ax: number, + ay: number, + bx: number, + by: number +) { + const dx = bx - ax, + dy = by - ay; + const len = dx * dx + dy * dy || 1; + let t = ((px - ax) * dx + (py - ay) * dy) / len; + t = Math.max(0, Math.min(1, t)); + return Math.hypot(px - (ax + t * dx), py - (ay + t * dy)); +} +function outline(tool: Tool, pts: Pt[]): Pt[] { + if (pts.length < 2) return pts; + const a = pts[0], + b = pts[pts.length - 1]; + if (tool === 'rect') return [a, { x: b.x, y: a.y }, b, { x: a.x, y: b.y }, a]; + if (tool === 'ellipse') { + const cx = (a.x + b.x) / 2, + cy = (a.y + b.y) / 2, + rx = Math.abs(b.x - a.x) / 2, + ry = Math.abs(b.y - a.y) / 2; + return Array.from({ length: 25 }, (_, i) => ({ + x: cx + rx * Math.cos((i / 24) * 2 * Math.PI), + y: cy + ry * Math.sin((i / 24) * 2 * Math.PI), + })); + } + return pts; +} +function hits(s: Stroke, x: number, y: number, r: number) { + const pts = outline(s.tool, clientPts(s)); + if (pts.length === 1) + return Math.hypot(pts[0].x - x, pts[0].y - y) < r + s.size; + for (let i = 0; i < pts.length - 1; i++) + if ( + distToSeg(x, y, pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y) < + r + s.size / 2 + ) + return true; + return false; +} + +export default function Annotator({ + slide, + store, + active, +}: { + slide: number; + store: Record; + active: boolean; +}) { + const canvasRef = useRef(null); + if (!store[slide]) store[slide] = []; + const strokes = useRef(store[slide]); + const draft = useRef(null); // draft points are raw client px + const [tool, setTool] = useState('pen'); + const [color, setColor] = useState(COLORS[0]); + const [size, setSize] = useState(SIZES[1]); + const toolRef = useRef(tool), + colorRef = useRef(color), + sizeRef = useRef(size); + toolRef.current = tool; + colorRef.current = color; + sizeRef.current = size; + + const resolve = (c: string) => + c.startsWith('var(') + ? getComputedStyle(document.documentElement) + .getPropertyValue('--primary') + .trim() || '#4fe5b0' + : c; + + function paint(ctx: CanvasRenderingContext2D, s: Stroke, p: Pt[]) { + if (!p.length) return; + ctx.save(); + ctx.strokeStyle = resolve(s.color); + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.lineWidth = s.size; + if (s.tool === 'highlighter') { + ctx.globalAlpha = 0.32; + ctx.lineWidth = s.size * 3.2; + } + const a = p[0], + b = p[p.length - 1]; + ctx.beginPath(); + if (s.tool === 'pen' || s.tool === 'highlighter') { + ctx.moveTo(a.x, a.y); + for (let i = 1; i < p.length; i++) ctx.lineTo(p[i].x, p[i].y); + ctx.stroke(); + } else if (s.tool === 'line' || s.tool === 'arrow') { + ctx.moveTo(a.x, a.y); + ctx.lineTo(b.x, b.y); + ctx.stroke(); + if (s.tool === 'arrow') { + const ang = Math.atan2(b.y - a.y, b.x - a.x), + h = 8 + s.size * 1.8; + ctx.beginPath(); + ctx.moveTo(b.x, b.y); + ctx.lineTo( + b.x - h * Math.cos(ang - 0.4), + b.y - h * Math.sin(ang - 0.4) + ); + ctx.moveTo(b.x, b.y); + ctx.lineTo( + b.x - h * Math.cos(ang + 0.4), + b.y - h * Math.sin(ang + 0.4) + ); + ctx.stroke(); + } + } else if (s.tool === 'rect') { + ctx.strokeRect( + Math.min(a.x, b.x), + Math.min(a.y, b.y), + Math.abs(b.x - a.x), + Math.abs(b.y - a.y) + ); + } else if (s.tool === 'ellipse') { + ctx.ellipse( + (a.x + b.x) / 2, + (a.y + b.y) / 2, + Math.abs(b.x - a.x) / 2, + Math.abs(b.y - a.y) / 2, + 0, + 0, + Math.PI * 2 + ); + ctx.stroke(); + } + ctx.restore(); + } + function redraw() { + const cv = canvasRef.current; + if (!cv) return; + const ctx = cv.getContext('2d'); + if (!ctx) return; + const dpr = window.devicePixelRatio || 1; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.clearRect(0, 0, cv.width / dpr, cv.height / dpr); + for (const s of strokes.current) paint(ctx, s, clientPts(s)); + if (draft.current) paint(ctx, draft.current, draft.current.points); + } + function commit() { + store[slide] = strokes.current; + redraw(); + } + function erase(x: number, y: number) { + const before = strokes.current.length; + strokes.current = strokes.current.filter((s) => !hits(s, x, y, 12)); + if (strokes.current.length !== before) commit(); + } + + useEffect(() => { + const cv = canvasRef.current; + if (!cv) return; + const fit = () => { + const dpr = window.devicePixelRatio || 1; + cv.width = window.innerWidth * dpr; + cv.height = window.innerHeight * dpr; + cv.style.width = window.innerWidth + 'px'; + cv.style.height = window.innerHeight + 'px'; + redraw(); + }; + fit(); + const onUp = () => { + const d = draft.current; + if (!d) return; + draft.current = null; + // anchor the stroke to the content under its center, then store its + // points relative to that element's box + const xs = d.points.map((p) => p.x), + ys = d.points.map((p) => p.y); + const cx = (Math.min(...xs) + Math.max(...xs)) / 2; + const cy = (Math.min(...ys) + Math.max(...ys)) / 2; + d.anchor = anchorAt(cx, cy); + const r = resolveAnchor(d.anchor) ?? viewportBox(); + d.points = d.points.map((p) => ({ + x: (p.x - r.left) / r.width, + y: (p.y - r.top) / r.height, + })); + strokes.current.push(d); + commit(); + }; + // re-render after the slide's entrance motion settles (anchor boxes move) + const t1 = window.setTimeout(redraw, 400); + const t2 = window.setTimeout(redraw, 1100); + window.addEventListener('resize', fit); + window.addEventListener('pointerup', onUp); + return () => { + clearTimeout(t1); + clearTimeout(t2); + window.removeEventListener('resize', fit); + window.removeEventListener('pointerup', onUp); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + function down(e: React.PointerEvent) { + if (toolRef.current === 'eraser') { + erase(e.clientX, e.clientY); + return; + } + draft.current = { + tool: toolRef.current, + color: colorRef.current, + size: sizeRef.current, + points: [{ x: e.clientX, y: e.clientY }], + }; + redraw(); + } + function move(e: React.PointerEvent) { + if (toolRef.current === 'eraser') { + if (e.buttons) erase(e.clientX, e.clientY); + return; + } + const d = draft.current; + if (!d) return; + if (d.tool === 'pen' || d.tool === 'highlighter') + d.points.push({ x: e.clientX, y: e.clientY }); + else d.points = [d.points[0], { x: e.clientX, y: e.clientY }]; + redraw(); + } + + // drag-to-scroll for the bar when it overflows (phones scroll natively by + // touch; this covers mouse users). A 4px threshold keeps clicks working, + // and a capture-phase click handler swallows the click after a real drag. + const barRef = useRef(null); + const barDrag = useRef({ down: false, moved: false, x: 0, left: 0 }); + function barDown(e: React.PointerEvent) { + if (e.pointerType !== 'mouse' || !barRef.current) return; + barDrag.current = { + down: true, + moved: false, + x: e.clientX, + left: barRef.current.scrollLeft, + }; + } + function barMove(e: React.PointerEvent) { + const d = barDrag.current; + if (!d.down || !barRef.current) return; + const dx = e.clientX - d.x; + if (!d.moved && Math.abs(dx) < 4) return; + d.moved = true; + barRef.current.scrollLeft = d.left - dx; + } + function barUp() { + barDrag.current.down = false; + } + function barClickCapture(e: React.MouseEvent) { + if (barDrag.current.moved) { + e.preventDefault(); + e.stopPropagation(); + barDrag.current.moved = false; + } + } + + return ( + <> + + {active && ( +
+ {TOOLS.map((t) => ( + + ))} + + {COLORS.map((c) => ( + + ))} + + + +
+ )} + + ); +} diff --git a/src/deck/Build.tsx b/src/deck/Build.tsx new file mode 100644 index 0000000..3c2ea3b --- /dev/null +++ b/src/deck/Build.tsx @@ -0,0 +1,51 @@ +import { motion } from 'framer-motion'; +import { useEffect } from 'react'; +import type { CSSProperties, ReactNode } from 'react'; +import { useDeck } from './DeckContext'; + +/* Click-build (the Slidev "v-click"): this element stays hidden until the + presenter advances to step `at` (1-based) on the current slide, then it + animates in. Reveal content in beats: . The deck + advances builds before moving to the next slide. */ +export default function Build({ + at = 1, + y = 16, + children, + className, + style, +}: { + at?: number; + y?: number; + children: ReactNode; + className?: string; + style?: CSSProperties; +}) { + const { clicks, isStatic, registerMax } = useDeck(); + useEffect(() => { + registerMax?.(at); + }, [at, registerMax]); + + if (isStatic) + return ( +
+ {children} +
+ ); + + const shown = clicks >= at; + return ( + + {children} + + ); +} diff --git a/src/deck/Deck.tsx b/src/deck/Deck.tsx new file mode 100644 index 0000000..9a58447 --- /dev/null +++ b/src/deck/Deck.tsx @@ -0,0 +1,534 @@ +import { + Children, + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react'; +import type { ReactElement, ReactNode } from 'react'; +import { MotionConfig } from 'framer-motion'; +import { DeckCtx } from './DeckContext'; +import Annotator, { type Stroke } from './Annotator'; +import { + IconSidebar, + IconGrid, + IconLeft, + IconRight, + IconPencil, + IconExpand, + IconShrink, + IconPresent, + IconClose, +} from './icons'; + +/* ── The paged presentation engine + the Slidev-style chrome (dock + rail). + Wrap your //… in . Each top-level child is one slide. + → / ↓ / Space next (reveals the next , then the next slide) + ← / ↑ previous S sidebar G grid view + Home / End first / last A annotate P presenter (new tab) + F fullscreen H hide/show the UI + Copy verbatim; theme only via the :root tokens. ───────────────────────── */ + +const fmt = (s: number) => + `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart( + 2, + '0' + )}`; + +function Thumb({ children }: { children: ReactNode }) { + const frameRef = useRef(null); + const [d, setD] = useState({ vw: 1280, vh: 720, scale: 0.15 }); + // measure before paint — with useEffect the first frame renders at the + // default scale and visibly snaps (worst in the grid view, which has no + // slide-in transition to mask it). + useLayoutEffect(() => { + const el = frameRef.current; + if (!el) return; + const update = () => + setD({ + vw: window.innerWidth, + vh: window.innerHeight, + scale: el.clientWidth / window.innerWidth, + }); + update(); + const ro = new ResizeObserver(update); + ro.observe(el); + window.addEventListener('resize', update); + return () => { + ro.disconnect(); + window.removeEventListener('resize', update); + }; + }, []); + return ( +
+ +
+ {children} +
+
+
+ ); +} + +export default function Deck({ children }: { children: ReactNode }) { + const slides = useMemo( + () => Children.toArray(children) as ReactElement[], + [children] + ); + const total = slides.length; + const isPresenter = useMemo( + () => new URLSearchParams(window.location.search).has('presenter'), + [] + ); + + const [slide, setSlide] = useState(() => { + const h = parseInt(window.location.hash.slice(1), 10); + return h >= 1 && h <= total ? h - 1 : 0; + }); + const [clicks, setClicks] = useState(0); + const [curMax, setCurMax] = useState(0); + const [railOpen, setRailOpen] = useState(false); + const [gridOpen, setGridOpen] = useState(false); + const [drawing, setDrawing] = useState(false); + const [elapsed, setElapsed] = useState(0); + const [fs, setFs] = useState(false); + const [uiHidden, setUiHidden] = useState(false); + const [nearDock, setNearDock] = useState(false); + const [cursorIdle, setCursorIdle] = useState(false); + const [noteOverrides, setNoteOverrides] = useState>( + () => { + try { + return JSON.parse(localStorage.getItem('deck:notes') || '{}'); + } catch { + return {}; + } + } + ); + + // per-slide build maxima (so going back restores the right click state) and + // per-slide annotations (so drawings persist on the slide they were made). + const maxMap = useRef>({}); + const annStore = useRef>({}); + const slideRef = useRef(slide); + slideRef.current = slide; + + const registerMax = useCallback((at: number) => { + const m = maxMap.current; + m[slideRef.current] = Math.max(m[slideRef.current] || 0, at); + setCurMax((c) => Math.max(c, at)); + }, []); + + const go = useCallback( + (i: number) => { + const n = Math.max(0, Math.min(total - 1, i)); + setSlide(n); + setClicks(0); + setCurMax(maxMap.current[n] || 0); + }, + [total] + ); + const next = useCallback(() => { + if (clicks < curMax) { + setClicks(clicks + 1); + return; + } + if (slide < total - 1) { + const n = slide + 1; + setSlide(n); + setClicks(0); + setCurMax(maxMap.current[n] || 0); + } + }, [clicks, curMax, slide, total]); + const prev = useCallback(() => { + if (clicks > 0) { + setClicks(clicks - 1); + return; + } + if (slide > 0) { + const n = slide - 1; + const m = maxMap.current[n] || 0; + setSlide(n); + setClicks(m); + setCurMax(m); + } + }, [clicks, slide]); + + const toggleFs = useCallback(() => { + if (document.fullscreenElement) document.exitFullscreen(); + else document.documentElement.requestFullscreen?.(); + }, []); + // sidebar and grid view are mutually exclusive — opening one closes the other + const toggleRail = useCallback(() => { + setRailOpen((v) => !v); + setGridOpen(false); + }, []); + const toggleGrid = useCallback(() => { + setGridOpen((v) => !v); + setRailOpen(false); + }, []); + const setNote = useCallback((text: string) => { + setNoteOverrides((prev) => { + const nextO = { ...prev, [slideRef.current]: text }; + try { + localStorage.setItem('deck:notes', JSON.stringify(nextO)); + } catch { + /* ignore */ + } + return nextO; + }); + }, []); + const openPresenter = useCallback(() => { + if (isPresenter) return; + const url = + window.location.pathname + '?presenter=1' + window.location.hash; + window.open(url, 'deck-presenter'); + }, [isPresenter]); + + // keyboard + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.metaKey || e.ctrlKey || e.altKey) return; + const t = e.target as HTMLElement | null; + if ( + t && + (t.tagName === 'TEXTAREA' || + t.tagName === 'INPUT' || + t.isContentEditable) + ) + return; + switch (e.key) { + case 'ArrowRight': + case 'ArrowDown': + case ' ': + case 'PageDown': + e.preventDefault(); + next(); + break; + case 'ArrowLeft': + case 'ArrowUp': + case 'PageUp': + e.preventDefault(); + prev(); + break; + case 'Home': + e.preventDefault(); + go(0); + break; + case 'End': + e.preventDefault(); + go(total - 1); + break; + case 's': + case 'S': + toggleRail(); + break; + case 'g': + case 'G': + toggleGrid(); + break; + case 'f': + case 'F': + toggleFs(); + break; + case 'a': + case 'A': + setDrawing((v) => !v); + break; + case 'p': + case 'P': + openPresenter(); + break; + case 'h': + case 'H': + setUiHidden((v) => !v); + break; + case 'Escape': + setRailOpen(false); + setGridOpen(false); + setDrawing(false); + setUiHidden(false); + break; + } + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [next, prev, go, total, toggleRail, toggleGrid, toggleFs, openPresenter]); + + // safety net: if the authored deck kept the placeholder tab title, derive + // one from the current slide's heading so shared links look right. + useEffect(() => { + if (!document.title.startsWith('Replace')) return; + const h = document.querySelector( + '.slide-stage h1, .slide-stage h2' + ); + const t = h?.innerText.replace(/\s+/g, ' ').trim(); + if (t) document.title = t; + }, []); + + // URL hash sync (initial slide comes from the hash via useState above) + useEffect(() => { + const want = String(slide + 1); + if (window.location.hash.slice(1) !== want) + history.replaceState(null, '', '#' + want); + }, [slide]); + useEffect(() => { + const onHash = () => { + const h = parseInt(window.location.hash.slice(1), 10); + if (h >= 1 && h <= total && h - 1 !== slide) go(h - 1); + }; + window.addEventListener('hashchange', onHash); + return () => window.removeEventListener('hashchange', onHash); + }, [slide, total, go]); + + // cross-tab sync (audience ⇄ presenter), via BroadcastChannel + const chan = useRef(null); + const applyingRemote = useRef(false); + useEffect(() => { + const c = new BroadcastChannel('deck-sync'); + chan.current = c; + c.onmessage = (e) => { + if (e.data?.type === 'state') { + applyingRemote.current = true; + setSlide(e.data.slide); + setClicks(e.data.clicks); + } + }; + return () => c.close(); + }, []); + useEffect(() => { + if (applyingRemote.current) { + applyingRemote.current = false; + return; + } + chan.current?.postMessage({ type: 'state', slide, clicks }); + }, [slide, clicks]); + + // fullscreen flag, presenter timer, idle auto-hide + useEffect(() => { + const h = () => setFs(!!document.fullscreenElement); + document.addEventListener('fullscreenchange', h); + return () => document.removeEventListener('fullscreenchange', h); + }, []); + useEffect(() => { + if (!isPresenter) return; + setElapsed(0); + const t = setInterval(() => setElapsed((e) => e + 1), 1000); + return () => clearInterval(t); + }, [isPresenter]); + // mouse-driven only: keyboard nav keeps the UI hidden; the dock returns when + // the pointer nears the bottom (where it lives); the cursor hides on idle. + useEffect(() => { + let t = 0; + const onMove = (e: MouseEvent) => { + setCursorIdle(false); + setNearDock(e.clientY > window.innerHeight - 150); + clearTimeout(t); + t = window.setTimeout(() => setCursorIdle(true), 2600); + }; + window.addEventListener('mousemove', onMove); + return () => { + clearTimeout(t); + window.removeEventListener('mousemove', onMove); + }; + }, []); + + const liveCtx = useMemo( + () => ({ clicks, isStatic: false, registerMax }), + [clicks, registerMax] + ); + const hasPrev = slide > 0 || clicks > 0; + const hasNext = slide < total - 1 || clicks < curMax; + const notes = (slides[slide]?.props as { notes?: string } | undefined)?.notes; + const noteText = noteOverrides[slide] ?? notes ?? ''; + const nextSlide = slides[slide + 1]; + const hideUI = uiHidden || (fs && !nearDock); + const cursorHidden = fs && cursorIdle && !drawing; + const showAnnotator = drawing || (annStore.current[slide]?.length ?? 0) > 0; + + // prev / counter / next — rendered inside the pill on desktop, in its own + // pill above the tools on phones (only one is visible at a time). + const navCluster = ( + <> + +
+ {slide + 1} + / {total} +
+ + + ); + + return ( + +
+ +
+ {slides[slide]} +
+
+ + {showAnnotator && ( + + )} + + + + {gridOpen && ( +
+
+ All slides + +
+
+ {slides.map((s, i) => ( + + ))} +
+
+ )} + + {isPresenter && ( +
+
+ + Presenter · {slide + 1} / {total} + + {fmt(elapsed)} +
+ {nextSlide && ( +
+ {nextSlide} +
+ )} +