open-slide Best Practices for Israeli Developers
Adapted from 1weiho/open-slide (MIT). Hebrew and RTL adaptations by skills-il.
Problem
open-slide ships an opinionated authoring system: a fixed 1920×1080 React canvas, a typed DesignSystem const, a themes folder, an inspector that drops @slide-comment markers in source, and five built-in skills (slide-authoring, create-slide, create-theme, apply-comments, current-slide). None of those touch Hebrew or RTL. Out of the box, a deck written by a coding agent comes out left-aligned, system-font only, with paddingLeft that fights the Hebrew reading order, and no guidance for mixed Hebrew+English text. Israeli developers waste hours retrofitting RTL, loading Hebrew Google Fonts, and discovering that the type scale (140-200px hero) which works in Inter looks cramped in Heebo at the same weight.
Instructions
When to use
Use this skill whenever you are writing or editing code under slides/<id>/ in an open-slide project, creating a theme under themes/, or processing inspector comments, especially when any of that content is in Hebrew or bilingual.
New project setup
When in an empty folder or workspace with no existing open-slide project, scaffold one using:
npx @open-slide/cli init my-slide
cd my-slide
npm run dev
Replace my-slide with a suitable project name. The scaffolder installs dependencies with npm and prints npm run dev as the next step; pnpm dev works too if you re-install with pnpm.
Node version. @open-slide/cli depends on commander@15, which declares "node": ">=22.12.0", and on chalk@6 (">=22"). On Node 20 the install still completes but npm prints EBADENGINE Unsupported engine for both. Use Node 22.12 or newer.
What the scaffold gives you:
| Path | What it is |
|---|---|
slides/<id>/index.tsx |
One folder per slide. See the id rule below. |
themes/ |
Theme markdown files |
open-slide.config.ts |
Typed OpenSlideConfig export, empty by default. Framework-level options live here: base, slidesDir, themesDir, assetsDir, port, allowedHosts, build, and a deprecated locale. |
.agents/skills/ and .claude/skills/ |
The five upstream skills, copied in at init time |
npm run sync:skills |
Re-copies the bundled skills out of @open-slide/core after a core upgrade |
Run npm run sync:skills after upgrading @open-slide/core, otherwise the agent keeps reading the skill copies from the version you scaffolded with.
Which version you are on
Target @open-slide/core 1.19.x. That is what npx @open-slide/cli init installs today, and everything in this skill is verified against 1.19.1.
A 2.0.0 beta is published (React 19, Vite 8, TypeScript 7). It is a pre-release: npm's latest tag still resolves to 1.19.x and the beta sits behind its own beta dist-tag, so you only reach v2 by asking for it (@open-slide/cli@2.0.0-beta.1 or @beta). Do not scaffold production work onto it, and do not assume any v2 API detail is stable.
Your slide source is not the problem. The upstream migration guide is explicit that v1 slides need no changes and that the migration is confined to package.json, your Node version, and your deploy config. Everything this skill teaches about the file contract, the canvas, type scale, themes and Hebrew/RTL carries over unchanged.
The one thing that breaks on upgrade. A v1 scaffold puts vite in your devDependencies; core 2.x ships its own Vite 8 internally. Leave the old entry in place and the dev server and build both refuse to start:
✖ @tailwindcss/vite resolves vite@5.4.21 (node_modules/vite), but @open-slide/core ships
vite@8.2.2 (node_modules/@open-slide/core/node_modules/vite).
A `vite` entry in your package.json (v1 workspaces had one) shadows the copy core depends on.
Remove it and reinstall.
Delete the vite devDependency and reinstall. Fresh v2 scaffolds omit it and are unaffected. Migration guide: https://open-slide.dev/docs/migrate-to-v2.
Slide folder ids must be ASCII (Hebrew folder names are silently dropped)
Do not name a slide folder in Hebrew. Discovery filters every folder under slides/ through SLIDE_ID_RE = /^[a-z0-9_-]+$/i and skips the rest, so a folder like slides/פתיחה/ never reaches the sidebar, the browser, or the build. Re-verified on @open-slide/core@1.19.1 (the current stable release) by scaffolding a project and running npm run build:
[plugin:open-slide] [plugin open-slide] Ignoring slide folder "פתיחה": slide ids must match
/^[a-z0-9_-]+$/i (lowercase/uppercase letters, digits, "-", "_"). Rename the folder under
"slides/" to a kebab-case id so it appears in the browser and can be moved into folders.
It is only a warning, not an error, so npm run build exits 0 and the missing slide is easy to miss. Use a Latin kebab-case id (peticha, cover, q2-roadmap) and put the Hebrew in meta.title and in the page content, where it belongs.
Do not over-apply this rule. It is specific to slide folder ids. Verified as NOT affected:
| Surface | Hebrew allowed? | Why |
|---|---|---|
slides/<id>/ folder name |
No | Filtered by SLIDE_ID_RE at discovery |
meta.title |
Yes | Free text, never used as an id |
themes/<id>.md file name |
Yes | The theme id is path.basename(file, '.md') with no ASCII filter, and theme routes are built with encodeURIComponent(id) |
Theme frontmatter values (name:, description:) |
Yes | Only the frontmatter KEY is matched against /^([A-Za-z0-9_-]+)\s*:/; the value is free text |
| Slide body copy | Yes | It is just JSX text |
Hebrew and RTL
For any Hebrew or bilingual deck, load ./rules/hebrew-rtl.md first. It covers:
- Hebrew Google Fonts (Heebo, Rubik, Assistant, Noto Sans Hebrew) loaded by head injection
- The
<html dir="rtl">set-up vs per-pagedirattribute (the canvas root is the page component, not the document) - CSS logical properties (
paddingInlineStart,marginInlineStart,insetInlineStart) instead ofpaddingLeft/marginLeft/left flexDirection: 'row-reverse'vs settingdiron the flex container- Bidirectional text with
<bdi>for mixed Hebrew + Latin (brand names, code, URLs) - Hebrew-aware type scale tuning (Hebrew renders ~10-15% wider per character at the same px size)
- Hebrew
DesignSystem.fonts.displayandfonts.bodychoices that work at hero size - RTL-aware theme markdown patterns under
themes/ - Hebrew typography pitfalls: nikkud rendering, sofit letter spacing, hyphen vs maqaf
Slide authoring (file contract, canvas, type scale, themes, design tokens)
For the technical reference on what goes inside slides/<id>/index.tsx, load ./rules/slide-authoring.md. This is the same content the upstream slide-authoring skill exposes, with Hebrew/RTL pointers inline. It covers the file contract, the 1920×1080 canvas math, the type scale, spacing, visual direction, themes, the DesignSystem design-tokens object, the starter template, assets, image placeholders, and the repeated-elements rule.
Upstream has since split its own slide-authoring skill into per-primitive reference files. Three of them cover primitives this skill does not restate, so read them upstream rather than reimplementing by hand:
| Upstream reference | Primitive | Why it matters in a Hebrew deck |
|---|---|---|
references/steps.md |
<Steps> / <Step> stepped reveals |
Staged reveals let you keep each Hebrew beat short instead of overflowing one dense page |
references/page-numbers.md |
useSlidePageNumber() |
Read { current, total } from the hook instead of hardcoding a footer counter; wrap the Latin digits in <bdi> inside an RTL footer |
references/morph.md |
MorphElement shared-element ("magic move") transitions |
Morph interpolates position and size across the cut, so an element that moves between an LTR and an RTL page needs the same id on both |
Drafting a new deck
When the user asks for "make slides about X" / "create a presentation" / "draft a deck", load ./rules/create-slide.md for the workflow: theme picker, the four scoping questions (aesthetic direction, page count, text density, motion), slide id selection, structure planning, visual commitment, write, self-review, hand off.
Creating a theme
When the user asks to "create a theme" / "make a theme called X" / "extract a theme from this slide", load ./rules/create-theme.md. A theme is a two-file bundle: themes/<id>.md (Palette / Typography / Layout / Fixed components / Motion / Aesthetic) plus a paired demo module themes/<id>.demo.tsx (.jsx, .ts and .js are also accepted), a runnable 2-3 page mini-slide that the dev UI's Themes panel renders as the live preview. Writing only the markdown leaves the theme with a dead preview card.
Applying inspector comments
When the user has clicked on a rendered page in the dev server and added comments (via the in-browser inspector tool), and asks to "apply comments" / "process slide comments", load ./rules/apply-comments.md. It covers the {/* @slide-comment */} marker format, the detection regex, the base64url-decoded payload, and the apply-in-reverse-line-order procedure.
Resolving deictic references ("this page", "the slide I'm on")
When the user references the current slide without naming it, load ./rules/current-slide.md first. It explains how to read the live cursor at node_modules/.open-slide/current.json, what fields are in it, and the staleness rules. Re-read it on every deictic turn, the user navigates between turns, so a value you read earlier is almost certainly stale.
Speaker notes go in the notes export, never in a markdown file
open-slide has built-in speaker notes, and coding agents routinely miss them: asked for a talk track, an agent writes script.md or notes.md next to the slide, which the runtime never reads. The presenter view stays empty and the user assumes the feature is missing.
Notes are an optional notes export in slides/<id>/index.tsx, index-aligned with the default page array. SlideModule declares it as notes?: (string | undefined)[]. Use undefined to skip a page rather than shifting the array:
const Cover: Page = () => <div dir="rtl">שלום</div>;
const Agenda: Page = () => <div dir="rtl">סדר היום</div>;
export const notes: (string | undefined)[] = [
'פתחו בציטוט של הלקוח, ואז הציגו את עצמכם.',
undefined,
`שלושה עמודי תווך, פסקה לכל אחד.
עצרו לשאלות לפני המעבר.`,
];
export default [Cover, Agenda, Pillars];
Rules:
- One entry per page, in the same order as the default export. A missing entry silently misaligns every note after it.
- Multi-line notes work; use a template literal.
notesmust be an ARRAY LITERAL. The dev server writes notes back into your source (PUT /__notes), and its parser rejects anything it cannot edit in place: a computed array, a spread, an imported constant, or a.map()all return422 \notes` export is not an array literal` and permanently break editing notes from the drawer. Write the array out by hand.- Hebrew notes render LTR-aligned and you cannot fix it from the slide. The notes drawer and presenter pane set no
dir, so Hebrew notes inherit the viewer chrome's direction. There is no author-side hook for this. Keep notes short and hard-wrap them yourself so the ragged alignment stays readable. - Never deliver a speech script as a separate file. If the user asks for "תסריט" / "a script" / "speaker notes", it goes in this export.
Stable ships the feature without documentation. The notes runtime is present in 1.19.1, but none of the five skills bundled with that release documents it. The word "notes" does occur in them, in a table column header, a grid-overlay bullet and a line about inspector markers, but never in reference to this export. So sync:skills will not teach an agent the feature exists. That gap is the reason this section exists.
Dev-server surfaces worth knowing
These are viewer features, not authoring APIs, but users ask about them and an agent that knows they exist avoids rebuilding them by hand:
| Surface | What it does |
|---|---|
| Notes drawer / presenter view | Renders the notes export above. Present mode shows notes beside the timer. |
| Command menu | Keyboard-driven palette for jumping between slides and actions. |
| Presenter-window deck switching | The presenter window can switch decks without dropping the presenter session. |
| Themes panel | Renders each theme's <id>.demo.tsx. A theme with no demo file shows an empty card. |
allowedHosts |
open-slide.config.ts accepts allowedHosts?: string[] | true, needed when serving the dev server through a tunnel or a non-localhost host. |
Vertical budget reminder (the #1 source of broken slides)
Anything below 1080px on the canvas is silently cropped. Before writing any page, sum:
(font_size × line_height × number_of_lines) + gaps + (2 × padding) ≤ 1080
If you're tight, split into two pages. Never use overflow: auto/scroll/hidden, negative margins, or transforms to hide overflow. The canvas does not scroll; cropped content is gone. See ./rules/slide-authoring.md section "Vertical budget" for the full worked example.
For Hebrew text the budget is tighter than it looks: Hebrew renders ~10-15% wider per character than Latin at the same px size, so a heading that fits English on one line at 80px may wrap to two in Heebo. Either drop the size or shorten the copy. See hebrew-rtl.md for the calibration table.
Reference Links
| Source | URL | What to Check |
|---|---|---|
| open-slide upstream repo | https://github.com/1weiho/open-slide | Latest API, breaking changes, version |
| open-slide docs / homepage | https://open-slide.dev | Install, demos |
| @open-slide/cli on npm | https://www.npmjs.com/package/@open-slide/cli | Current CLI version |
| @open-slide/core on npm | https://www.npmjs.com/package/@open-slide/core | Current runtime version, exports |
| MDN: CSS logical properties | https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_logical_properties_and_values | RTL-aware CSS, inset-inline-start, padding-inline |
MDN: <bdi> element |
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/bdi | Bidi isolation for mixed Hebrew+Latin |
| Google Fonts (Hebrew subset) | https://fonts.google.com/?subset=hebrew | Hebrew web fonts: Heebo, Rubik, Assistant, Noto Sans Hebrew |
Upstream slide-id regex (slide-ops.ts) |
https://raw.githubusercontent.com/1weiho/open-slide/7fbd1ea84cf1ee7cd701cc9fe59bff3e4e0148c5/packages/core/src/editing/slide-ops.ts | SLIDE_ID_RE, whether non-ASCII slide folder ids are still filtered out |
| Upstream webfont loading rules | https://raw.githubusercontent.com/1weiho/open-slide/7fbd1ea84cf1ee7cd701cc9fe59bff3e4e0148c5/packages/core/skills/slide-authoring/references/webfonts.md | The supported way to load a webfont (head injection, slide-keyed id) |
Recommended MCP Servers
None directly, since open-slide has no API surface. The skill is purely client-side authoring guidance.
Gotchas
These are the failure modes a coding agent hits when authoring an open-slide deck in Hebrew without this skill. They are NOT user errors.
Using
paddingLeftandmarginLefteverywhere. In LTR these read as "from the start of the line". In RTL they're literally on the wrong side of the canvas. UsepaddingInlineStart/paddingInlineEnd(or shorthandpaddingInline) so the layout flips automatically whendir="rtl"is set on the canvas root. The same applies toleft/right(useinsetInlineStart/insetInlineEnd) andtext-align: left/right(usetext-align: start/end).Setting
direction: 'rtl'on<html>or globally. open-slide's canvas root is the page component, not the HTML document. The dev server chrome (file rail, navigation arrows) is upstream UI in English; flipping the document direction breaks it. Setdir="rtl"on the page component's root<div>only. The Inspector and presenter mode keep their LTR layout; only your slide content flips. There is no Hebrew option to switch them to either:OpenSlideConfig.localeis deprecated in favour of the slide UI's language switcher, and the shipped UI locales areen,zh-TW,zh-CNandjaonly. Tell the user the authoring chrome stays English; only the deck is Hebrew.Naming the slide folder in Hebrew. It is the natural thing to do on a Hebrew deck and it silently loses the slide: discovery filters folder names through
/^[a-z0-9_-]+$/iand only warns. Latin kebab-case id, Hebrew inmeta.title. See "Slide folder ids must be ASCII" above.Loading the Hebrew webfont from inside the page component. A
<style>@import ...</style>or a<link>rendered inside aPagere-registers the whole@font-faceset once per mounted page, and the home page, the thumbnail rail, the overview grid and the PDF print root all mount every page at the same time. Upstream's rule is to inject the stylesheet once into<head>from module top level ofindex.tsx, with an element id keyed to the slide (osd-webfont-<slide-id>) so one slide's fonts cannot suppress another's. Seehebrew-rtl.mdsection 3.The default display font is a serif with no Hebrew glyphs.
defaultDesign.fonts.displayisGeorgia, "Times New Roman", serifandfonts.bodyis-apple-system, BlinkMacSystemFont, "Inter", system-ui, sans-serif. Georgia carries no Hebrew, so an unstyled Hebrew hero silently falls back to whatever system serif the OS picks (David / Times-like). That, not the body stack, is the real cause of the "my Hebrew deck renders in a dated serif" symptom. For decks that ship beyond a dev machine, load Heebo or Rubik via Google Fonts (css2, nosubsetparameter, it is ignored) and put them first in the stack with the system stack as fallback. Seehebrew-rtl.md.Same type scale as English. 140-200px hero in Inter looks tight at the lower bound (140) for Hebrew, because Hebrew uppercase-equivalent (Hebrew has no case) sits at the cap-height-equivalent of large-x-height Latin fonts. Hebrew at 140px reads like Latin at 120px. Either bump the hero to 160-220px for Hebrew, or drop the line count.
Mixing Hebrew and English without
<bdi>. A bullet like "השתמשו ב-React Router" with mixed Hebrew + Latin tokens often renders the punctuation in the wrong place because the browser's bidi algorithm doesn't always isolate the Latin span. Wrap mixed runs in<bdi>:השתמשו ב-<bdi>React Router</bdi>. Critical for code identifiers, brand names, and URLs inside Hebrew copy.array.mapfor visually repeated cards. The upstream rule (one component instantiated per item, NOTmapover a data array) applies in Hebrew too. The reason is the inspector:mapshares one source location per item, so editing one Hebrew label changes all rendered cards. Define<Card />once, instantiate it three times explicitly. Seeslide-authoring.md.Ignoring
node_modules/.open-slide/current.json. When the user says "תקן את הכותרת בעמוד הזה" ("fix the heading on this page"), the deictic "הזה" maps exactly the same way the English "this" does, you must readcurrent.jsonto find which slide they're on. Re-read it on every turn. Seecurrent-slide.md.Same vertical budget at the bumped Hebrew type scale. A cover that fits a 3-line LTR hero at 200px (3 × 200 × 0.95 = 570px) will overflow if you keep 3 lines AND bump to 232px for Hebrew (3 × 232 × 0.95 = 661px), once padding and subtitle are added you blow past 1080. Either stay at 1-2 lines for the Hebrew hero, or recompute the full budget with the bumped values before committing. Splitting is always the right answer.
Mis-translated technical terms inside the slide copy. Translation services and well-meaning agents render English dev terminology into Hebrew that means something else. Common traps:
- "themes" → "ערכאות" (literally "court instances", a legal term). Correct: "ערכות נושא" (theme sets) or "תמות" (transliteration).
- "resolution" (as in "resolving a reference") → "רזולוציית" (means screen/image resolution). Correct: "פיענוח" (decoding) or "זיהוי" (identification).
- "authoring" → "אוטרינג" (English transliteration, sounds techy and wrong). Correct: "כתיבה" or "יצירה".
- "tuned scale" → "סקאלה מכוונת" (literal but robotic). Better: "סקאלה מותאמת" (adapted scale). Always verify Hebrew technical terms with a native speaker or a Hebrew dev community reference before shipping.
Pairing brand colors as a gradient can produce an off-brand intermediate hue. A linear gradient from Israeli Blue (
#003286) to YooTech Magenta (#bc46a2) blends through saturated purple, which is NOT in the agentskills.co.il palette. If a project has a brand palette, audit each combination: hold solid bold marks for primary brand colors, and only gradient between adjacent hues (e.g., Israeli Blue → Israeli Blue Light) or fade to neutral (cream / navy). One confident solid usually beats a contrived gradient.Audit your own copy for bidi traps. Recursively. A skill that warns about missing
<bdi>will frequently MISS its own<bdi>calls inside Hebrew sentences ("השתמשו ב-RTL" needs<bdi>RTL</bdi>). When writing prose, slides, or examples, manually scan every Latin token embedded in Hebrew runs and wrap it. Code identifiers, brand names, version numbers, file extensions, all need isolation. The author writing the rule is not exempt from the rule.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Hebrew text renders left-aligned | Page root is missing dir="rtl" |
Add dir="rtl" to the page's outer <div>, or set direction: 'rtl' in its inline style |
| Hebrew heading renders in a serif when you wanted sans | Verified cause: defaultDesign.fonts.display is Georgia, "Times New Roman", serif, and Georgia has no Hebrew glyphs, so the OS substitutes a system Hebrew serif |
Set fonts.display (not just fonts.body) to a Hebrew family and load it via head injection |
| Editing a note from the drawer fails, or the drawer stops saving | The notes export is not an array literal, so the dev server's write-back returns 422 \notes` export is not an array literal. A computed array, a spread, an import or a .map()` all trigger it |
Write notes out as a plain array literal |
| Hebrew speaker notes are left-aligned in the presenter pane | The notes drawer and presenter set no dir, so notes inherit the viewer chrome's direction. There is no author-side fix |
Keep notes short and hard-wrap them; this is viewer behaviour, not a bug in your slide |
A signed number reads backwards on a Hebrew slide (-5 shows as 5-) |
The sign has no digit on its other side, so UAX#9 W4 cannot fold it into the number; it stays a neutral and jumps to the far side | Wrap the signed number in <bdi>. Negative KPIs are the usual miss, they look like a single number |
| Brand name like "skills-il" appears with hyphen on the wrong side | Mixed Hebrew+Latin without bidi isolation | Wrap the Latin run in <bdi> |
| Hero heading wraps to a second line in Hebrew but not English | Hebrew is wider per character | Either bump hero size by ~15% or shorten the title |
paddingLeft: 160 puts content against the right edge of an RTL slide |
Physical CSS in an RTL container | Replace with paddingInline: 160 or paddingInlineStart: 160 |
| Inspector arrows / file rail flipped | You set direction: 'rtl' globally on <html> |
Move dir="rtl" to the page component's root only |
| A slide you created never appears in the sidebar or the build, but nothing errors | The folder id under slides/ is not ASCII (a Hebrew name), so discovery skipped it with a warning only |
Rename the folder to Latin kebab-case; keep the Hebrew in meta.title. Grep the dev/build output for Ignoring slide folder |
| Hebrew webfont loads but the deck feels heavy on the home page or PDF export | The @font-face set is registered once per mounted page because the stylesheet is injected inside a Page component |
Inject it once into <head> from module top level with an id keyed to the slide (osd-webfont-<slide-id>) |
npm warn EBADENGINE Unsupported engine on npx @open-slide/cli init |
Node older than 22.12.0 (commander@15 requires >=22.12.0) |
Upgrade Node to 22.12 or newer |
current.json does not exist |
Dev server has not been opened on a slide yet | Run pnpm dev and open any slide in the browser, then re-read |
| Comment markers missing after applying | Did not delete the marker line, only the text | Re-run the regex; remove the entire line including trailing \n |
@open-slide/cli init fails with "package not found" |
Network or registry issue | Verify npm view @open-slide/cli version returns a version; clear pnpm/npm cache |
| Type imports fail | Wrong package or stale install | Imports come from @open-slide/core: import type { Page, SlideMeta, DesignSystem } from '@open-slide/core' |
Bundled Resources
rules/
├── slide-authoring.md # File contract, canvas, type scale, themes, design tokens, assets
├── create-slide.md # New-deck workflow: theme picker, scoping questions, structure
├── create-theme.md # Theme markdown authoring under themes/<id>.md
├── apply-comments.md # @slide-comment marker handling
├── current-slide.md # Resolving "this page" via current.json
└── hebrew-rtl.md # Hebrew fonts, bidi, logical CSS, RTL DesignSystem (the new content)
Load on demand based on the task. Always start with hebrew-rtl.md for Hebrew or bilingual content.