Skip to main content

create-ryos-app

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.

Source facts

Repository
ryokun6/ryos
Last source activity
July 8, 2026 at 22:33
Detected SKILL.md language
English
Stars
1,280
Forks
185

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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 metadata: metadata.ts (appMetadata + exactly 6 help items) - [ ] 6. Create app definition: index.tsx (re-export metadata, declare initialData type) - [ ] 7. Add icon: choose from existing icons or the resource catalogs, then place active assets under `public/icons/<theme>/[app-name].png` - [ ] 8. Register the app id: add to appIds + appNames in src/config/appRegistryData.ts - [ ] 9. Register the app: lazy component + registry entry in src/config/appRegistry.tsx - [ ] 10. Register help key order in src/hooks/useTranslatedHelpItems.ts - [ ] 11. Add translation keys to src/lib/locales/en/translation.json - [ ] 12. 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) ├── metadata.ts # appMetadata + helpItems (required) └── index.tsx # App definition: re-export metadata, initialData types (required) ``` ### Why metadata lives in its own file `appRegistry.tsx` imports `appMetadata`/`helpItems` **eagerly** so the dock, About/Help dialogs, and search can show app info without loading the (lazy) component bundle. Keep these in a tiny `metadata.ts` that imports nothing heavy. Most current apps follow this split (`@/apps/<id>/metadata`). `index.tsx` then re-exports from `metadata.ts` and is the home for `initialData` types and any app-specific exported types. ## 1. Metadata (`metadata.ts`) Keep app metadata and help items in `metadata.ts` so the registry can load them eagerly without pulling in the component. ```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" }, ]; ``` ### App icon sourcing Before creating a new app icon from scratch, check the active icon themes and the historical icon resource catalogs: 1. Look for an existing logical icon in `public/icons/default`, `public/icons/macosx`, `public/icons/win98`, and `public/icons/xp`. 2. Search the resource catalogs for historically appropriate source art: - Mac OS X: `public/resources/macos-icon-catalogs/{panther,tiger}/catalog.md` - Windows: `public/resources/windows-icon-catalogs/{win98,xp}/catalog.md` 3. If a catalog asset is the right source, copy or adapt it into the active icon tree (`public/icons/<theme>/...`) instead of referencing `public/resources/...` directly from app metadata. 4. Add at least `public/icons/default/[app-name].png`; add theme-specific variants when the catalog has a better era-matched asset. 5. Run `bun run generate:icons` after adding or moving active files under `public/icons`. Keep `public/resources/*-icon-catalogs` as source libraries. Do not replace unrelated active icons just because a catalog contains a historical equivalent. ### App Definition (`index.tsx`) Re-export the metadata and declare any `initialData` type. This is what other files import as `@/apps/[app-name]`. ```tsx export { appMetadata, helpItems } from "./metadata"; // Optional: typed startup payload for launchApp("[app-name]", { ... }) export interface [AppName]InitialData { // e.g. filePath?: string; } ``` ## 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, isWindowsTheme, } = use[AppName]Logic({ isWindowOpen, isForeground, instanceId }); const menuBar = ( <[AppName]MenuBar onClose={onClose} onShowHelp={() => setIsHelpDialogOpen(true)} onShowAbout={() => setIsAboutDialogOpen(true)} /> ); if (!isWindowOpen) return null; return ( <> {!isWindowsTheme && isForeground && menuBar} <WindowFrame title={t("apps.[app-name].title")} onClose={onClose} isForeground={isForeground} appId="[app-name]" skipInitialSound={skipInitialSound} instanceId={instanceId} menuBar={isWindowsTheme ? menuBar : undefined} > <div className="flex flex-col h-full bg-os-window-bg font-os-ui"> {/* App content */} </div> </WindowFrame> <HelpDialog isOpen={isHelpDialogOpen} onOpenChange={setIsHelpDialogOpen} appId="[app-name]" helpItems={translatedHelpItems} /> <AboutDialog isOpen={isAboutDialogOpen} onOpenChange={setIsAboutDialogOpen} metadata={appMetadata} appId="[app-name]" /> </> ); } ``` ## 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 isWindowsTheme = currentTheme === "xp" || currentTheme === "win98"; const [isHelpDialogOpen, setIsHelpDialogOpen] = useState(false); const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false); return { t, translatedHelpItems, isWindowsTheme, isHelpDialogOpen, setIsHelpDialogOpen, isAboutDialogOpen, setIsAboutDialogOpen, }; } ``` ## 4. Menu Bar (`[AppName]MenuBar.tsx`) Match existing app menubars: structure, classes, and spacing. - **Wrapper**: `<MenuBar inWindowFrame={isWindowsTheme}>` — 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 isWindowsTheme = currentTheme === "xp" || currentTheme === "win98"; const isMacOSTheme = currentTheme === "macosx"; return ( <MenuBar inWindowFrame={isWindowsTheme}> <MenubarMenu> <MenubarTrigger className="text-md px-2 py-1 border-none focus-visible:ring-0"> {t("common.menu.file")} </MenubarTrigger> <MenubarContent align="start" sideOffset={1} className="px-0"> <MenubarSeparator className="h-[2px] bg-black my-1" /> <MenubarItem onClick={onClose} className="text-md h-6 px-3"> {t("common.menu.close")} </MenubarItem> </MenubarContent> </MenubarMenu> <MenubarMenu> <MenubarTrigger className="text-md px-2 py-1 border-none focus-visible:ring-0"> {t("common.menu.help")} </MenubarTrigger> <MenubarContent align="start" sideOffset={1} className="px-0"> <MenubarItem onClick={onShowHelp} className="text-md h-6 px-3"> {t("apps.[app-name].menu.help")} </MenubarItem> {!isMacOSTheme && ( <> <MenubarSeparator className="h-[2px] bg-black my-1" /> <MenubarItem onClick={onShowAbout} className="text-md h-6 px-3"> {t("apps.[app-name].menu.about")} </MenubarItem> </> )} </MenubarContent> </MenubarMenu> </MenuBar> ); } ``` ## 5. Register the App ID (`appRegistryData.ts`) The `AppId` union type, dock/search ordering, and store lookups all derive from `src/config/appRegistryData.ts`. **Add the id here first** — otherwise `appRegistry.tsx` (and everything typed against `AppId`) will not compile. ```tsx // src/config/appRegistryData.ts export const appIds = [ // ...existing ids... "[app-name]", ] as const; export const appNames: Record<AppId, string> = { // ...existing names... "[app-name]": "[App Name]", }; ``` If you ever rename an app's id, add the old id to `LEGACY_APP_ID_ALIASES` so persisted/bookmarked references still resolve. ## 6. Register in `appRegistry.tsx` `createLazyComponent` lives in `src/config/lazyAppComponent.tsx` (already imported at the top of `appRegistry.tsx`) and registers the chunk for intent-based prefetch. Import metadata from the lightweight `metadata.ts`, not the component. ```tsx // Metadata import (eager, lightweight) — note the /metadata path import { appMetadata as [appName]Metadata, helpItems as [appName]HelpItems } from "@/apps/[app-name]/metadata"; // Lazy component (loaded on open). Use your initialData type instead of unknown if you declared one. const Lazy[AppName]App = createLazyComponent<unknown>( () => import("@/apps/[app-name]/components/[AppName]AppComponent") .then(m => ({ default: m.[AppName]AppComponent })), "[app-name]" // cache key = app id, keeps refs stable across HMR + enables prefetch ); // Add to the appRegistry object ["[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, }, ``` ## 7. Register help keys
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub