@open-slide/core 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/{build-DSqSio-T.js → build-_276DMmJ.js} +2 -2
  2. package/dist/cli/bin.js +5 -5
  3. package/dist/{config-KdiYeWtK.js → config-BAwKWNtW.js} +888 -229
  4. package/dist/{config-C7vMYzFD.d.ts → config-D9cZ1A0X.d.ts} +2 -1
  5. package/dist/{dev-B_GVbr11.js → dev-BoqeVXVq.js} +2 -2
  6. package/dist/en-CDKzoZvf.js +351 -0
  7. package/dist/index.d.ts +4 -3
  8. package/dist/index.js +229 -39
  9. package/dist/locale/index.d.ts +1 -1
  10. package/dist/locale/index.js +166 -326
  11. package/dist/{preview-D_mxhj7w.js → preview-BLPxspc9.js} +2 -2
  12. package/dist/sync-j9_QPovT.js +3 -0
  13. package/dist/{types-DYgVpIGo.d.ts → types-JYG1cmwC.d.ts} +59 -5
  14. package/dist/vite/index.d.ts +2 -2
  15. package/dist/vite/index.js +2 -2
  16. package/package.json +9 -1
  17. package/skills/create-slide/SKILL.md +1 -1
  18. package/skills/create-theme/SKILL.md +60 -12
  19. package/skills/current-slide/SKILL.md +110 -0
  20. package/skills/slide-authoring/SKILL.md +59 -1
  21. package/src/app/app.tsx +11 -1
  22. package/src/app/components/asset-view.tsx +1 -13
  23. package/src/app/components/image-placeholder.tsx +123 -1
  24. package/src/app/components/inspector/image-crop-dialog.tsx +64 -20
  25. package/src/app/components/inspector/inspector-panel.tsx +163 -19
  26. package/src/app/components/inspector/inspector-provider.tsx +60 -7
  27. package/src/app/components/notes-drawer.tsx +117 -0
  28. package/src/app/components/player.tsx +11 -7
  29. package/src/app/components/present/overview-grid.tsx +2 -2
  30. package/src/app/components/sidebar/folder-item.tsx +16 -5
  31. package/src/app/components/sidebar/mobile-pill.tsx +34 -0
  32. package/src/app/components/sidebar/sidebar.tsx +10 -0
  33. package/src/app/components/themes/theme-detail.tsx +300 -0
  34. package/src/app/components/themes/themes-gallery.tsx +146 -0
  35. package/src/app/components/thumbnail-rail.tsx +136 -29
  36. package/src/app/components/ui/context-menu.tsx +237 -0
  37. package/src/app/lib/assets.ts +55 -2
  38. package/src/app/lib/inspector/use-notes.ts +134 -0
  39. package/src/app/lib/sdk.ts +1 -0
  40. package/src/app/lib/slides.ts +10 -1
  41. package/src/app/lib/themes.ts +22 -0
  42. package/src/app/lib/use-agent-socket.ts +18 -0
  43. package/src/app/routes/home-shell.tsx +173 -0
  44. package/src/app/routes/home.tsx +108 -204
  45. package/src/app/routes/slide.tsx +333 -68
  46. package/src/app/routes/themes.tsx +34 -0
  47. package/src/app/virtual.d.ts +20 -0
  48. package/src/locale/en.ts +61 -7
  49. package/src/locale/ja.ts +62 -7
  50. package/src/locale/types.ts +62 -5
  51. package/src/locale/zh-cn.ts +61 -7
  52. package/src/locale/zh-tw.ts +61 -7
  53. package/dist/sync-B4eLo2H6.js +0 -3
  54. /package/dist/{design-C13iz9_4.js → design-cpzS8aud.js} +0 -0
  55. /package/dist/{sync-3oqN1WyK.js → sync-BCJDRIqo.js} +0 -0
@@ -38,6 +38,7 @@ type Locale = {
38
38
  home: {
39
39
  appTitle: string;
40
40
  draft: string;
41
+ themes: string;
41
42
  folders: string;
42
43
  newFolder: string;
43
44
  folderName: string;
@@ -52,7 +53,6 @@ type Locale = {
52
53
  nothingMatchesSuffix: string;
53
54
  noSlidesYet: string;
54
55
  createSlideHintPrefix: string;
55
- createSlideHintMid: string;
56
56
  createSlideHintSuffix: string;
57
57
  folderEmptyTitle: string;
58
58
  folderEmptyHint: string;
@@ -85,6 +85,10 @@ type Locale = {
85
85
  slide: {
86
86
  home: string;
87
87
  backToHome: string;
88
+ agentConnected: string;
89
+ agentConnectedTooltip: string;
90
+ agentDisconnected: string;
91
+ agentDisconnectedTooltip: string;
88
92
  download: string;
89
93
  exportAsHtml: string;
90
94
  exportAsPdf: string;
@@ -157,6 +161,10 @@ type Locale = {
157
161
  inspector: {
158
162
  inspect: string;
159
163
  deselect: string;
164
+ agentWatching: string;
165
+ agentWatchingTooltip: string;
166
+ agentNotWatching: string;
167
+ agentNotWatchingTooltip: string;
160
168
  contentSection: string;
161
169
  typographySection: string;
162
170
  colorSection: string;
@@ -194,10 +202,10 @@ type Locale = {
194
202
  cropFitContain: string;
195
203
  cropApply: string;
196
204
  cropResetAria: string;
197
- noteForAgent: string;
198
- noteAgentPlaceholder: string;
199
- noteShortcutHint: string;
200
- addNote: string;
205
+ leaveComment: string;
206
+ commentPlaceholder: string;
207
+ commentShortcutHint: string;
208
+ addComment: string;
201
209
  /** templates: "{count} unsaved change" / "{count} unsaved changes" */
202
210
  unsavedChanges: Plural;
203
211
  /** templates: "{count} comment" / "{count} comments" */
@@ -300,6 +308,17 @@ type Locale = {
300
308
  pages: string;
301
309
  /** template: "Go to page {n}" */
302
310
  goToPageAria: string;
311
+ duplicatePage: string;
312
+ deletePage: string;
313
+ /** template: "Page {n} actions" */
314
+ pageActionsAria: string;
315
+ /** template: "Duplicated page {n}" */
316
+ toastDuplicated: string;
317
+ /** template: "Deleted page {n}" */
318
+ toastDeleted: string;
319
+ toastDuplicateFailed: string;
320
+ toastDeleteFailed: string;
321
+ resizeRail: string;
303
322
  };
304
323
  pdfToast: {
305
324
  title: string;
@@ -319,5 +338,40 @@ type Locale = {
319
338
  prevAria: string;
320
339
  nextAria: string;
321
340
  };
341
+ imagePlaceholder: {
342
+ dropOverlay: string;
343
+ uploading: string;
344
+ uploadFailed: string;
345
+ };
346
+ notesDrawer: {
347
+ toggle: string;
348
+ /** template: "page {n}/{total}" */
349
+ pageLabel: string;
350
+ placeholder: string;
351
+ statusSaving: string;
352
+ statusSaved: string;
353
+ /** template: "Save failed: {msg}" */
354
+ statusError: string;
355
+ };
356
+ themes: {
357
+ title: string;
358
+ noThemesTitle: string;
359
+ noThemesHintPrefix: string;
360
+ noThemesHintSuffix: string;
361
+ noDemoYet: string;
362
+ noDemoHintPrefix: string;
363
+ noDemoHintSuffix: string;
364
+ backToGallery: string;
365
+ /** template: "page {n}/{total}" */
366
+ pageOf: string;
367
+ nextPageAria: string;
368
+ prevPageAria: string;
369
+ /** template: "Open theme {name}" */
370
+ openThemeAria: string;
371
+ usedBy: string;
372
+ usedByEmpty: string;
373
+ expandPromptAria: string;
374
+ collapsePromptAria: string;
375
+ };
322
376
  }; //#endregion
323
377
  export { Locale, Plural };
@@ -1,5 +1,5 @@
1
- import "../types-DYgVpIGo.js";
2
- import { OpenSlideConfig } from "../config-C7vMYzFD.js";
1
+ import "../types-JYG1cmwC.js";
2
+ import { OpenSlideConfig } from "../config-D9cZ1A0X.js";
3
3
  import { InlineConfig } from "vite";
4
4
 
5
5
  //#region src/vite/config.d.ts
@@ -1,4 +1,4 @@
1
- import "../design-C13iz9_4.js";
2
- import { createViteConfig } from "../config-KdiYeWtK.js";
1
+ import "../design-cpzS8aud.js";
2
+ import { createViteConfig } from "../config-BAwKWNtW.js";
3
3
 
4
4
  export { createViteConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-slide/core",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Runtime and CLI for open-slide — write slides in slides/, we handle the rest.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -42,11 +42,19 @@
42
42
  "vite"
43
43
  ],
44
44
  "license": "MIT",
45
+ "author": {
46
+ "name": "1weiho",
47
+ "url": "https://github.com/1weiho"
48
+ },
49
+ "homepage": "https://open-slide.dev",
45
50
  "repository": {
46
51
  "type": "git",
47
52
  "url": "git+https://github.com/1weiho/open-slide.git",
48
53
  "directory": "packages/core"
49
54
  },
55
+ "bugs": {
56
+ "url": "https://github.com/1weiho/open-slide/issues"
57
+ },
50
58
  "publishConfig": {
51
59
  "access": "public"
52
60
  },
@@ -13,7 +13,7 @@ You only write files under `slides/<id>/`. Never modify `package.json`, `open-sl
13
13
 
14
14
  List files under `themes/`. If any theme markdown files exist (anything other than `README.md`), call `AskUserQuestion` with each theme id as an option plus a final **"no theme — design from scratch"** option.
15
15
 
16
- - If the user picks a theme: read `themes/<id>.md` end-to-end. The theme's palette, typography, layout, and Title/Footer components are now authoritative — copy them directly into the slide. In Step 2, skip the **aesthetic direction** question (the theme already commits to one direction); you still need the topic itself, so confirm it before moving on. Page count, text density, and motion are independent of theme — ask those normally.
16
+ - If the user picks a theme: read `themes/<id>.md` end-to-end. The theme's palette, typography, layout, and Title/Footer components are now authoritative — copy them directly into the slide. **Also set `theme: '<theme-id>'` on the `meta` export in `index.tsx`** (e.g. `export const meta: SlideMeta = { title: '…', theme: '<theme-id>' };`) so the slide back-links to the theme (chip on the slide card + listing on `/themes/<id>`). In Step 2, skip the **aesthetic direction** question (the theme already commits to one direction); you still need the topic itself, so confirm it before moving on. Page count, text density, and motion are independent of theme — ask those normally.
17
17
  - If the user picks "no theme", or `themes/` is empty (or contains only `README.md`): proceed to Step 2 unchanged.
18
18
 
19
19
  If you skip the aesthetic question because a theme was picked, restate the theme name in Step 2 so the user can correct course before you start writing.
@@ -1,15 +1,20 @@
1
1
  ---
2
2
  name: create-theme
3
- description: Use this skill when the user wants to create, draft, author, or extract a slide theme in this open-slide repo. Triggers on phrases like "create a theme", "make a theme called X", "extract a theme from <slide>", "build a theme from these images". Produces a single markdown file under `themes/<id>.md` describing palette, typography, layout, fixed Title/Footer components, and motion philosophy. Do NOT use for editing slides themselves — only for authoring `themes/<id>.md`.
3
+ description: Use this skill when the user wants to create, draft, author, or extract a slide theme in this open-slide repo. Triggers on phrases like "create a theme", "make a theme called X", "extract a theme from <slide>", "build a theme from these images". Produces two paired files under `themes/`: `<id>.md` (palette, typography, layout, fixed Title/Footer components, motion) and `<id>.demo.tsx` (a runnable demo slide that the dev-UI Themes panel previews). Do NOT use for editing real slides — only for authoring the theme bundle.
4
4
  ---
5
5
 
6
6
  # Create a slide theme
7
7
 
8
- This skill produces one markdown file under `themes/<id>.md` that describes a reusable visual identity for slides — palette, typography, layout, fixed Title/Footer/Eyebrow components, motion. Themes are agent-facing documentation, not executable runtime: a slide author reads the theme markdown and applies it when writing `slides/<id>/index.tsx`.
8
+ This skill produces a **theme bundle** under `themes/`: two paired files that together describe a reusable visual identity.
9
9
 
10
- A theme is **distinct from a slide's `design` const**. The theme markdown is authoring-time aesthetic direction (read by `create-slide`, copied into slide source). A per-slide `const design: DesignSystem = { … }` (declared at the top of `slides/<id>/index.tsx`) is the runtime tokens object the user can tweak from the Design panel in the dev UI. Both can coexist: the markdown theme commits the *direction*, the per-slide `design` const makes the slide *tweakable*. This skill only writes the markdown.
10
+ 1. `themes/<id>.md` agent-facing documentation: palette, typography, layout, fixed Title/Footer/Eyebrow components, motion. This is what `create-slide` reads when an author picks the theme.
11
+ 2. `themes/<id>.demo.tsx` — a runnable mini-slide (a normal slide module: `export default Page[]`) that demonstrates the theme on 2–3 pages. The dev UI's **Themes panel** loads this file and renders it as the theme's live preview.
11
12
 
12
- You only write a single file under `themes/`. Never modify slides or other configuration. The canvas / type-scale defaults that themes can override live in the **`slide-authoring`** skill read it before writing the theme so your overrides are stated explicitly.
13
+ Both files share the same stem so the runtime can pair them automatically.
14
+
15
+ A theme is **distinct from a slide's `design` const**. The theme markdown is authoring-time aesthetic direction (copied into a real slide's source by `create-slide`). The demo `.tsx` is a self-contained preview, not a real slide — it does not appear in the slides list. A per-slide `const design: DesignSystem = { … }` (declared at the top of `slides/<id>/index.tsx`) is the runtime tokens object the user can tweak from the Design panel. The markdown commits the *direction*; the per-slide `design` const makes the slide *tweakable*; the demo `.tsx` makes the theme *previewable*.
16
+
17
+ You only write files under `themes/<id>.md` and `themes/<id>.demo.tsx`. Never modify real slides or other configuration. The canvas / type-scale defaults that themes can override live in the **`slide-authoring`** skill — read it before writing the theme so your overrides are stated explicitly.
13
18
 
14
19
  ## Step 1 — Identify the input source
15
20
 
@@ -47,7 +52,6 @@ Produce a file with this exact section order. Section bodies adapt to the theme;
47
52
  ---
48
53
  name: <Human title, e.g. "Editorial Noir">
49
54
  description: <one-line elevator pitch>
50
- mode: light | dark | system
51
55
  ---
52
56
 
53
57
  # <Theme name>
@@ -163,6 +167,46 @@ const Cover: Page = () => (
163
167
  ```
164
168
  ````
165
169
 
170
+ ## Step 4b — Write `themes/<id>.demo.tsx`
171
+
172
+ The demo is a normal slide module — same shape as `slides/<id>/index.tsx`, just sitting under `themes/` so the runtime knows it's preview-only. The dev-UI Themes panel imports it and renders it inside `SlideCanvas` (1920×1080).
173
+
174
+ Contract:
175
+
176
+ - `import type { Page } from '@open-slide/core';`
177
+ - Inline the **same** `Title`, `Footer`, `Eyebrow` components defined in the theme markdown — verbatim, no abstractions, no imports from elsewhere. The demo and the markdown must stay in lockstep so what the user sees in the panel matches what `create-slide` will paste into a real slide.
178
+ - Export 2–3 `Page` components and a default array. Aim for: a Cover (Eyebrow + Title + subtitle), one Content page exercising body type + accent, and a Closer or "End" card. The "Example usage" block at the bottom of the markdown is a good starting point — extend it.
179
+ - If the theme has runtime-tweakable tokens worth surfacing in the Design panel later, also `export const design: DesignSystem = {...}`.
180
+ - No external assets, no `import` from `@/`, no slides-only helpers (e.g. `WindowShell` from a real slide). Demo files must be self-contained.
181
+
182
+ Skeleton:
183
+
184
+ ```tsx
185
+ import type { Page } from '@open-slide/core';
186
+
187
+ const Title = ({ children }: { children: React.ReactNode }) => (
188
+ // …same JSX as in themes/<id>.md
189
+ );
190
+ const Footer = ({ pageNum, total }: { pageNum: number; total: number }) => (
191
+ // …
192
+ );
193
+ const Eyebrow = ({ children }: { children: React.ReactNode }) => (
194
+ // …
195
+ );
196
+
197
+ const Cover: Page = () => (
198
+ // …
199
+ );
200
+ const Content: Page = () => (
201
+ // …
202
+ );
203
+ const Closer: Page = () => (
204
+ // …
205
+ );
206
+
207
+ export default [Cover, Content, Closer];
208
+ ```
209
+
166
210
  ## Step 5 — Self-review
167
211
 
168
212
  Run this checklist before finishing:
@@ -172,23 +216,27 @@ Run this checklist before finishing:
172
216
  - [ ] At least Title and Footer are defined as paste-ready React with concrete inline styles.
173
217
  - [ ] Motion section commits to one of static / subtle / rich.
174
218
  - [ ] Aesthetic paragraph names a single coherent direction.
175
- - [ ] File lives at `themes/<id>.md` and only that file was created — no slide changes, no config changes.
176
- - [ ] Frontmatter `mode` is one of `light`, `dark`, `system`.
219
+ - [ ] Both files written: `themes/<id>.md` and `themes/<id>.demo.tsx`. No slide changes, no config changes.
220
+ - [ ] Demo `.tsx` exports 2–3 pages and inlines the same Title/Footer/Eyebrow components defined in the markdown.
221
+ - [ ] Demo opens cleanly in the **Themes** panel of the dev UI — re-checked by you only by reading the file (do not start a server).
177
222
 
178
223
  ## Step 6 — Hand off
179
224
 
180
225
  Tell the user:
181
226
 
182
- - The theme id and file path.
183
- - That `/create-slide` will list it as a picker option on its next run.
227
+ - The theme id and the two file paths (`themes/<id>.md` + `themes/<id>.demo.tsx`).
228
+ - That the demo will appear in the dev UI's **Themes** panel as a live card and detail view (HMR — no restart needed).
229
+ - That `/create-slide` will list the theme as a picker option on its next run.
184
230
  - A one-line summary of the look (palette + aesthetic).
185
231
 
186
- Do not run the dev server. Do not modify slides — even to demonstrate the theme; that is the user's next move.
232
+ Do not run the dev server. Do not modify real slides — even to demonstrate the theme; the demo `.tsx` is the demonstration.
187
233
 
188
234
  ## Anti-patterns
189
235
 
190
- - ❌ Writing executable code in `themes/<id>.md` outside the labeled component snippets — the file is documentation.
191
- - ❌ Producing more than one file. One theme = one `themes/<id>.md`.
236
+ - ❌ Writing executable code in `themes/<id>.md` outside the labeled component snippets — the markdown is documentation.
237
+ - ❌ Producing only the markdown without the demo, or only the demo without the markdown. A theme is the **bundle** — both files, every time.
238
+ - ❌ Treating `themes/<id>.demo.tsx` as a real slide. It is preview-only and lives outside the slides list; never put it under `slides/`.
239
+ - ❌ Importing from `@/` or any slide-specific helper inside the demo. The demo is self-contained.
192
240
  - ❌ Inventing palette / fonts when the user supplied images or an existing slide. Extract, don't fabricate.
193
241
  - ❌ Editing `slides/`, `packages/`, `package.json`, or `open-slide.config.ts`.
194
242
  - ❌ Skipping the Fixed components section. Title and Footer are the most common reuse target — they must be paste-ready.
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: current-slide
3
+ description: Resolve which slide, page, and (optionally) selected element the user is currently viewing in the open-slide dev server. Consult this whenever the user references "this page", "this slide", "this element", "the slide I'm on", "the current page", or any deictic reference to slide content without naming it. Re-read `node_modules/.open-slide/current.json` at the start of every such turn — the user navigates between turns, so a value you read earlier in the conversation is almost certainly stale.
4
+ ---
5
+
6
+ # Where is the user right now?
7
+
8
+ When the user says "fix this page", "tweak this heading", or "the slide I'm looking at", they almost never name the slide id, page number, or element — they mean wherever they are in the dev viewer. Before asking "which slide?" or "which element?", check the file the dev server writes on every navigation and inspector pick.
9
+
10
+ ## Re-read on every deictic turn — never reuse a prior read
11
+
12
+ `current.json` is a live cursor, not a fact about the conversation. The user moves between slides, pages, and elements freely between your turns — including while you were doing other work. **Read the file fresh at the start of every new turn that uses a deictic reference**, even if:
13
+
14
+ - you already read it earlier in this same conversation,
15
+ - you just finished editing the slide it pointed to,
16
+ - the user's new message sounds like a continuation ("now make it bigger", "also fix this one", "keep going").
17
+
18
+ A "continue editing" follow-up is exactly the case where the user has likely just navigated to a different slide or picked a different element. Trusting your last read here will silently edit the wrong file. Re-read, compare `slideId` / `pageIndex` / `selection` against what you used last time, and act on the new values.
19
+
20
+ ## How to read it
21
+
22
+ ```
23
+ node_modules/.open-slide/current.json
24
+ ```
25
+
26
+ Path is relative to the project root (the user's `cwd`, the directory that contains `slides/` and `package.json`). Use the `Read` tool. The file is JSON.
27
+
28
+ ## What you get
29
+
30
+ ```json
31
+ {
32
+ "slideId": "q2-roadmap",
33
+ "pageIndex": 2,
34
+ "pageNumber": 3,
35
+ "totalPages": 8,
36
+ "slideTitle": "Q2 Roadmap",
37
+ "view": "slides",
38
+ "pagePath": "slides/q2-roadmap/index.tsx",
39
+ "selection": {
40
+ "line": 42,
41
+ "column": 6,
42
+ "tagName": "h1",
43
+ "text": "Q2 Roadmap"
44
+ },
45
+ "updatedAt": "2026-05-09T14:32:11.123Z"
46
+ }
47
+ ```
48
+
49
+ - `slideId` — folder name under `slides/`. Use as-is for any `/__slides/<id>/...` API or as the URL segment.
50
+ - `pageIndex` — 0-based, for use with the page array in `index.tsx` (`export default [Cover, Body, ...]`).
51
+ - `pageNumber` — 1-based, for use in messages to the user ("page 3 of 8") and for the URL `?p=N`.
52
+ - `pagePath` — relative path to the slide source. Hand straight to `Read` / `Edit`.
53
+ - `view` — `"slides"` (canvas view) or `"assets"` (asset manager). If `"assets"`, the user is browsing files for that slide rather than viewing a page.
54
+ - `selection` — `null` if nothing is selected. Otherwise, the JSX element the user picked in the inspector overlay:
55
+ - `line` (1-indexed) and `column` (0-indexed) point to the JSX opening tag inside `pagePath`. This is the canonical handle — match against the source line, not the rendered DOM.
56
+ - `tagName` is the rendered DOM tag, lowercased (`"h1"`, `"div"`, `"img"`).
57
+ - `text` is a trimmed text snippet (≤120 chars) from the element's `textContent`, useful as a sanity check that you're looking at the right node.
58
+ - Selection auto-clears whenever the user navigates to a different slide or page.
59
+ - `updatedAt` — ISO timestamp of the last navigation or selection change. Use it to detect staleness.
60
+
61
+ ## When to use this
62
+
63
+ - The user references the current slide/page deictically: "this", "here", "the page I'm on", "the slide I'm looking at", "what I'm working on".
64
+ - The user references a specific element: "this heading", "this image", "the button I just clicked", "tighten this", "change the color of this". If `selection` is non-null, that's the element they mean.
65
+ - Before asking "which slide?" or "which element?" as a clarifying question — check this file first.
66
+ - Before guessing from `git log`, recently-edited files, or the most recent slide folder.
67
+
68
+ ## When NOT to use this
69
+
70
+ - The user names a slide explicitly ("edit `q2-roadmap`") — use that name directly.
71
+ - The `apply-comments` workflow already finds the right file via `@slide-comment` markers; it doesn't need this skill.
72
+ - For listing or discovering slides — read `slides/` directly.
73
+
74
+ ## Staleness — verify before acting
75
+
76
+ `updatedAt` is the last time the user navigated. Treat it like a cache:
77
+
78
+ - **Fresh (under ~5 minutes old)**: trust it. Open `pagePath`, do the work.
79
+ - **Older than ~5 minutes, or older than your last interaction with the user**: confirm with the user before editing. The dev server may not be running; the user may have switched contexts.
80
+ - **Hours/days old**: ignore it. Ask the user which slide they mean.
81
+
82
+ A *newer* `updatedAt` than the one you saw last turn is the normal signal that the user has moved — switch to the new `slideId` / `pageIndex` / `selection` without asking.
83
+
84
+ ## When the file is missing
85
+
86
+ - The dev server hasn't been opened on a slide yet, or has never run.
87
+ - Don't create the file or guess. Ask the user which slide they mean, or suggest they open the slide in the dev server first.
88
+
89
+ ## Example — page-level reference
90
+
91
+ User: "tighten the spacing on this page"
92
+
93
+ 1. Read `node_modules/.open-slide/current.json`.
94
+ 2. Check `updatedAt` is recent.
95
+ 3. Read `pagePath` (e.g. `slides/q2-roadmap/index.tsx`).
96
+ 4. Identify the page at `pageIndex` in the default-exported array.
97
+ 5. Consult the `slide-authoring` skill for spacing rules, then edit that page in place.
98
+
99
+ If `current.json` is missing or stale, ask: "Which slide and page should I tighten? The dev server hasn't published a current page recently."
100
+
101
+ ## Example — element-level reference
102
+
103
+ User: "make this bigger"
104
+
105
+ 1. Read `node_modules/.open-slide/current.json`.
106
+ 2. If `selection` is non-null, the user means that element. Read `pagePath`, jump to `selection.line`, and find the JSX opening tag near that line/column. Confirm with the snippet in `selection.text` and the `tagName`.
107
+ 3. Consult `slide-authoring` for type-scale and layout rules before editing.
108
+ 4. Edit the JSX node in place.
109
+
110
+ If `selection` is null, fall back to the page-level flow above — and consider asking "which element?" since the user used a deictic but hasn't picked one in the inspector.
@@ -9,6 +9,7 @@ This skill is the **technical reference** for everything that happens inside `sl
9
9
 
10
10
  - `create-slide` owns "draft a new deck" — it asks the user scoping questions, then delegates the *how* to this skill.
11
11
  - `apply-comments` owns "process inspector markers" — it finds markers and applies edits, but the edits themselves follow the rules here.
12
+ - `current-slide` resolves deictic references ("this page", "the slide I'm on") to a concrete `slideId` + `pageIndex`. Consult it **first** when the user references the current slide without naming it, then come back here for how to edit it.
12
13
  - Any ad-hoc slide edit (manual tweak, one-off fix) should also consult this skill before touching the file.
13
14
 
14
15
  When any of those paths reach the point of *writing React code for a page*, this is the source of truth. Do not duplicate the knowledge below into other skills — link here instead.
@@ -19,7 +20,7 @@ When any of those paths reach the point of *writing React code for a page*, this
19
20
  - Entry is `slides/<id>/index.tsx`. Images/videos/fonts go under `slides/<id>/assets/`.
20
21
  - Do **not** touch `package.json`, `open-slide.config.ts`, or other slides.
21
22
  - Do not add dependencies. Only `react` and standard web APIs are available.
22
- - Do not create `README.md` or other prose files inside the slide folder — just `index.tsx` + `assets/`.
23
+ - A slide is **one `index.tsx` plus `assets/`** — nothing else. Do not create sibling `.tsx`/`.ts` files (`Card.tsx`, `components/`, `helpers.ts`, etc.); helper components and constants go inside `index.tsx`. Do not create `README.md` or other prose files either.
23
24
 
24
25
  ## File contract
25
26
 
@@ -37,6 +38,17 @@ export default [Cover, Body] satisfies Page[];
37
38
  - `export default` is a **non-empty array of zero-prop React components**, one per page, in order.
38
39
  - `meta.title` (optional) shows in the slide header. Default is the folder name.
39
40
  - The slide id is the kebab-case folder name. Pick something short and descriptive (`q2-roadmap`, `team-offsite-2026`).
41
+ - `meta.theme` (optional) marks the slide as built from a theme under `themes/`. The id must match a `<id>.md` basename. Surfaces a back-link chip on the slide card and lists the slide on `/themes/<id>`. Omit if the slide isn't derived from a registered theme.
42
+
43
+ ## Editing an existing slide
44
+
45
+ A finished slide commonly runs 1000–1800 lines. When you only need to touch one page, **don't read the whole file** — locate the page first, then read just that range:
46
+
47
+ ```bash
48
+ grep -n ": Page = " slides/<id>/index.tsx
49
+ ```
50
+
51
+ This lists every `const Foo: Page = …` declaration with its line number. Read the target page with `Read` using `offset` + `limit` (~150 lines is usually enough to capture one page plus its helper components). Read the whole file only when you need cross-page context (palette audit, reordering, design const tweaks).
40
52
 
41
53
  ## Canvas
42
54
 
@@ -264,6 +276,50 @@ The user uploads the real file via the Assets panel, then clicks the placeholder
264
276
 
265
277
  Size the placeholder to the slot it occupies. Pass `width`/`height` when the layout has a fixed image box; omit them when the placeholder fills a flex/grid cell. The `hint` should describe the *content* the user needs ("Q3 revenue chart") not the *role* ("hero image").
266
278
 
279
+ ## Repeated elements: component, not `map`
280
+
281
+ When a page has visually repeated items — cards, logo rows, gallery tiles, list rows, step indicators — **define a small component and instantiate it once per item**. Do **not** render the group with `array.map` over a data array.
282
+
283
+ Define the component **in the same `index.tsx`**, alongside the `Page` components. Never split it into a sibling file like `Card.tsx` — a slide is always a single `index.tsx` plus its `assets/`.
284
+
285
+ ```tsx
286
+ // ✅ Each card is its own JSX node — inspector edits one at a time.
287
+ const Card = ({ src, label }: { src: string; label: string }) => (
288
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
289
+ <img src={src} style={{ width: 320, height: 320, objectFit: 'cover', borderRadius: 12 }} />
290
+ <p style={{ fontSize: 32 }}>{label}</p>
291
+ </div>
292
+ );
293
+
294
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 64 }}>
295
+ <Card src={alpha} label="Alpha" />
296
+ <Card src={beta} label="Beta" />
297
+ <Card src={gamma} label="Gamma" />
298
+ </div>
299
+ ```
300
+
301
+ ```tsx
302
+ // ❌ One shared template — replacing the image or text in the inspector
303
+ // changes every rendered card at once.
304
+ const items = [
305
+ { src: alpha, label: 'Alpha' },
306
+ { src: beta, label: 'Beta' },
307
+ { src: gamma, label: 'Gamma' },
308
+ ];
309
+ items.map((item) => (
310
+ <div>
311
+ <img src={item.src} />
312
+ <p>{item.label}</p>
313
+ </div>
314
+ ));
315
+ ```
316
+
317
+ The inspector edits source JSX in place. A `map` body is **one source location** shared by every rendered instance, so when the user replaces an image or tweaks a label there, every card mutates together. Explicit instances give each card its own JSX node and its own props — the unit the inspector can target.
318
+
319
+ The component definition stays the single source of truth for layout/styling (change it once → all cards update). Only the per-instance data — `src`, `label`, accent color — lives at the call site.
320
+
321
+ This applies whenever the *visual element* repeats, not whenever the *data* does. Pure-text lists (`<ul><li>` bullets) are fine: each `<li>` is already its own JSX node, so plain literal markup is the correct shape — no need to wrap them in a component.
322
+
267
323
  ## Runtime behavior you get for free
268
324
 
269
325
  - Home page lists every folder under `slides/`.
@@ -282,6 +338,7 @@ Size the placeholder to the slot it occupies. Pass `width`/`height` when the lay
282
338
  - [ ] One coherent visual direction across every page (palette + type scale).
283
339
  - [ ] Slide declares a top-level `export const design: DesignSystem = { … }` and references the values via `var(--osd-X)` (use `design.X` only when you need a JS number for arithmetic). Only omit the `design` const for a one-off slide whose palette is intentionally locked.
284
340
  - [ ] One idea per page.
341
+ - [ ] Visually repeated elements (cards, tiles, logo rows) are rendered as explicit `<Component />` instances, not via `array.map` over a data list.
285
342
  - [ ] All imported assets exist on disk under `slides/<id>/assets/`.
286
343
  - [ ] Every `<ImagePlaceholder>` corresponds to a real image the user must supply — not decorative filler. If it could be replaced by typography or layout, it should be.
287
344
  - [ ] Nothing outside `slides/<id>/` was edited.
@@ -302,3 +359,4 @@ Size the placeholder to the slot it occupies. Pass `width`/`height` when the lay
302
359
  - ❌ Editing `package.json`, `open-slide.config.ts`, or other slides.
303
360
  - ❌ Sprinkling `<ImagePlaceholder>` across pages "for visual interest". Placeholders are for content the user owns; they're not stock-photo slots.
304
361
  - ❌ Using a placeholder for an icon or decorative shape — those are typography/SVG problems, not asset problems.
362
+ - ❌ Rendering visually repeated elements with `array.map(...)` over a data array. Define a component and instantiate it explicitly per item (`<Card />`, `<Card />`, `<Card />`) so the inspector can edit each independently — a shared `map` body mutates every instance at once.
package/src/app/app.tsx CHANGED
@@ -3,14 +3,24 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom';
3
3
  import { Toaster } from './components/ui/sonner';
4
4
  import { useLocale } from './lib/use-locale';
5
5
  import { Home } from './routes/home';
6
+ import { HomeShell } from './routes/home-shell';
6
7
  import { Presenter } from './routes/presenter';
7
8
  import { Slide } from './routes/slide';
9
+ import { ThemeDetailPage, ThemesGalleryPage } from './routes/themes';
8
10
 
9
11
  export function App() {
10
12
  return (
11
13
  <BrowserRouter>
12
14
  <Routes>
13
- <Route path="/" element={config.build.showSlideBrowser ? <Home /> : <NotFound />} />
15
+ {config.build.showSlideBrowser ? (
16
+ <Route element={<HomeShell />}>
17
+ <Route path="/" element={<Home />} />
18
+ <Route path="/themes" element={<ThemesGalleryPage />} />
19
+ <Route path="/themes/:themeId" element={<ThemeDetailPage />} />
20
+ </Route>
21
+ ) : (
22
+ <Route path="/" element={<NotFound />} />
23
+ )}
14
24
  <Route path="/s/:slideId" element={<Slide />} />
15
25
  <Route path="/s/:slideId/presenter" element={<Presenter />} />
16
26
  <Route path="*" element={<NotFound />} />
@@ -33,6 +33,7 @@ import {
33
33
  import {
34
34
  type AssetEntry,
35
35
  fetchSvgAsFile,
36
+ renamedCopy,
36
37
  type SvglItem,
37
38
  searchSvgl,
38
39
  useAssets,
@@ -315,19 +316,6 @@ function hasFiles(e: React.DragEvent): boolean {
315
316
  return false;
316
317
  }
317
318
 
318
- function renamedCopy(file: File, taken: Set<string>): File {
319
- const dot = file.name.lastIndexOf('.');
320
- const stem = dot > 0 ? file.name.slice(0, dot) : file.name;
321
- const ext = dot > 0 ? file.name.slice(dot) : '';
322
- let i = 1;
323
- let next = `${stem}-${i}${ext}`;
324
- while (taken.has(next)) {
325
- i += 1;
326
- next = `${stem}-${i}${ext}`;
327
- }
328
- return new File([file], next, { type: file.type, lastModified: file.lastModified });
329
- }
330
-
331
319
  function AssetCard({
332
320
  asset,
333
321
  onPreview,