---
name: create-ryos-app
description: Create new applications for ryOS following established patterns and conventions. Use when building a new app, adding an application to the desktop, creating app components, or scaffolding app structures.
---
# Creating ryOS Applications
## Quick Start Checklist
```
- [ ] 1. Create app directory: src/apps/[app-name]/
- [ ] 2. Create main component: components/[AppName]AppComponent.tsx
- [ ] 3. Create menu bar: components/[AppName]MenuBar.tsx
- [ ] 4. Create logic hook: hooks/use[AppName]Logic.ts
- [ ] 5. Create app definition: index.tsx (include 6 help items)
- [ ] 6. Add icon: public/icons/default/[app-name].png
- [ ] 7. Register in src/config/appRegistry.tsx
- [ ] 8. Add translation keys to src/lib/locales/en/translation.json
- [ ] 9. Localize (last): add en strings, sync locales; use the localize skill to finish
```
## Directory Structure
```
src/apps/[app-name]/
├── components/
│ ├── [AppName]AppComponent.tsx # Main component (required)
│ └── [AppName]MenuBar.tsx # Menu bar (required)
├── hooks/
│ └── use[AppName]Logic.ts # Logic hook (recommended)
└── index.tsx # App definition (required)
```
## 1. App Definition (`index.tsx`)
```tsx
export const appMetadata = {
name: "[App Name]",
version: "1.0.0",
creator: { name: "Ryo Lu", url: "https://ryo.lu" },
github: "https://github.com/ryokun6/ryos",
icon: "/icons/default/[app-name].png",
};
// Always include exactly 6 help items (icon, title, description each).
export const helpItems = [
{ icon: "🚀", title: "Getting Started", description: "How to use this app" },
{ icon: "📂", title: "Open & Save", description: "Open and save files from the File menu" },
{ icon: "✏️", title: "Editing", description: "Use the Edit menu for cut, copy, paste" },
{ icon: "👁️", title: "View Options", description: "Adjust view and layout from the View menu" },
{ icon: "⌨️", title: "Shortcuts", description: "Use keyboard shortcuts for faster workflows" },
{ icon: "❓", title: "Help & About", description: "Open Help from the Help menu for more info" },
];
```
## 2. Main Component (`[AppName]AppComponent.tsx`)
```tsx
import { WindowFrame } from "@/components/layout/WindowFrame";
import { [AppName]MenuBar } from "./[AppName]MenuBar";
import { AppProps } from "@/apps/base/types";
import { use[AppName]Logic } from "../hooks/use[AppName]Logic";
import { HelpDialog } from "@/components/dialogs/HelpDialog";
import { AboutDialog } from "@/components/dialogs/AboutDialog";
import { appMetadata } from "..";
export function [AppName]AppComponent({
isWindowOpen,
onClose,
isForeground,
skipInitialSound,
instanceId,
}: AppProps) {
const {
t,
translatedHelpItems,
isHelpDialogOpen,
setIsHelpDialogOpen,
isAboutDialogOpen,
setIsAboutDialogOpen,
isXpTheme,
} = use[AppName]Logic({ isWindowOpen, isForeground, instanceId });
const menuBar = (
<[AppName]MenuBar
onClose={onClose}
onShowHelp={() => setIsHelpDialogOpen(true)}
onShowAbout={() => setIsAboutDialogOpen(true)}
/>
);
if (!isWindowOpen) return null;
return (
<>
{!isXpTheme && isForeground && menuBar}
{/* App content */}
>
);
}
```
## 3. Logic Hook (`use[AppName]Logic.ts`)
```tsx
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useTranslatedHelpItems } from "@/hooks/useTranslatedHelpItems";
import { useThemeStore } from "@/stores/useThemeStore";
import { helpItems } from "..";
export function use[AppName]Logic({ instanceId }: { instanceId: string }) {
const { t } = useTranslation();
const translatedHelpItems = useTranslatedHelpItems("[app-name]", helpItems);
const currentTheme = useThemeStore((state) => state.current);
const isXpTheme = currentTheme === "xp" || currentTheme === "win98";
const [isHelpDialogOpen, setIsHelpDialogOpen] = useState(false);
const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false);
return {
t,
translatedHelpItems,
isXpTheme,
isHelpDialogOpen,
setIsHelpDialogOpen,
isAboutDialogOpen,
setIsAboutDialogOpen,
};
}
```
## 4. Menu Bar (`[AppName]MenuBar.tsx`)
Match existing app menubars: structure, classes, and spacing.
- **Wrapper**: `` — no extra gap between menus (layout uses `space-x-0`).
- **Trigger**: `MenubarTrigger className="text-md px-2 py-1 border-none focus-visible:ring-0"`.
- **Content**: `MenubarContent align="start" sideOffset={1} className="px-0"`.
- **Items**: `MenubarItem className="text-md h-6 px-3"`.
- **Separators**: `MenubarSeparator className="h-[2px] bg-black my-1"`.
```tsx
import { MenuBar } from "@/components/layout/MenuBar";
import {
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
} from "@/components/ui/menubar";
import { useThemeStore } from "@/stores/useThemeStore";
import { useTranslation } from "react-i18next";
interface [AppName]MenuBarProps {
onClose: () => void;
onShowHelp: () => void;
onShowAbout: () => void;
}
export function [AppName]MenuBar({ onClose, onShowHelp, onShowAbout }: [AppName]MenuBarProps) {
const { t } = useTranslation();
const currentTheme = useThemeStore((state) => state.current);
const isXpTheme = currentTheme === "xp" || currentTheme === "win98";
const isMacOsxTheme = currentTheme === "macosx";
return (
{t("common.menu.file")}
{t("common.menu.close")}
{t("common.menu.help")}
{t("apps.[app-name].menu.help")}
{!isMacOsxTheme && (
<>
{t("apps.[app-name].menu.about")}
>
)}
);
}
```
## 5. Register in `appRegistry.tsx`
```tsx
// Import
import { appMetadata as [appName]Metadata, helpItems as [appName]HelpItems } from "@/apps/[app-name]";
// Lazy component
const Lazy[AppName]App = createLazyComponent(
() => import("@/apps/[app-name]/components/[AppName]AppComponent")
.then(m => ({ default: m.[AppName]AppComponent })),
"[app-name]"
);
// Add to registry
["[app-name]"]: {
id: "[app-name]",
name: "[App Name]",
icon: { type: "image", src: [appName]Metadata.icon },
description: "App description",
component: Lazy[AppName]App,
helpItems: [appName]HelpItems,
metadata: [appName]Metadata,
windowConfig: {
defaultSize: { width: 650, height: 475 },
minSize: { width: 400, height: 300 },
} as WindowConstraints,
},
```
## AppProps Interface
| Prop | Type | Description |
|------|------|-------------|
| `isWindowOpen` | `boolean` | Window visibility |
| `onClose` | `() => void` | Close handler |
| `isForeground` | `boolean` | Window is active |
| `instanceId` | `string` | Unique instance ID |
| `skipInitialSound` | `boolean` | Skip open sound |
| `initialData` | `TInitialData` | Optional startup data |
## Menu Bar Placement
- **macOS/System7**: Render outside WindowFrame when `isForeground`
- **XP/Win98**: Pass via `menuBar` prop to WindowFrame
```tsx
const isXpTheme = currentTheme === "xp" || currentTheme === "win98";
return (
<>
{!isXpTheme && isForeground && menuBar}
```
## WindowFrame Options
| Prop | Values | Use |
|------|--------|-----|
| `material` | `"default"`, `"transparent"`, `"notitlebar"` | Window style |
| `interceptClose` | `boolean` | Show save dialog before close |
| `keepMountedWhenMinimized` | `boolean` | Preserve state when minimized |
## Common Patterns
### Initial Data
```tsx
interface ViewerInitialData { filePath: string; }
export function ViewerAppComponent({ initialData }: AppProps) {
const filePath = initialData?.filePath ?? "";
}
```
### Launch Other Apps
```tsx
import { useLaunchApp } from "@/hooks/useLaunchApp";
const launchApp = useLaunchApp();
launchApp("photos", { path: "/image.png" });
```
### Global Store (Zustand)
```tsx
// src/stores/use[AppName]Store.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
export const use[AppName]Store = create()(
persist((set) => ({ /* state and actions */ }), { name: "[app-name]-storage" })
);
```
## 9. Localize (Do Last)
After the app is built and wired up, finish by localizing:
1. **Add translation keys** for all user-facing strings (menu labels, dialogs, status, help).
2. **Add English entries** under `apps.[app-name].*` in `src/lib/locales/en/translation.json`.
3. **Sync other locales** (e.g. `bun run scripts/sync-translations.ts --mark-untranslated`).
Use the **localize** skill for the full workflow: extract strings → `t()` calls → en keys → sync. Do this step last so all UI copy is stable before extracting and syncing.