Refactor project name from "Open Claude Design" to "Open Design" (#1)
* Refactor project name from "Open Claude Design" to "Open Design" - Updated project name in package.json, package-lock.json, and README files. - Changed CLI commands and references from "ocd" to "od". - Adjusted file structure references in documentation and code to reflect new naming conventions. - Enhanced .gitignore to include new runtime data files. - Updated metadata in LICENSE file to match new project name. * Add contributing guidelines in English and Chinese - Introduced CONTRIBUTING.md and CONTRIBUTING.zh-CN.md to provide clear instructions for contributors. - Outlined contribution types, local setup instructions, and merging criteria for skills and design systems. - Enhanced README files to reference the new contributing guidelines.
This commit is contained in:
@@ -76,8 +76,12 @@ export const ChatComposer = forwardRef<ChatComposerHandle, Props>(
|
||||
const importTriggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
// initialDraft is only honored on the first non-empty value the parent
|
||||
// hands us. After we seed once, the composer is fully under user control
|
||||
// — re-renders that pass the same prompt back must not reseed.
|
||||
const seededRef = useRef(false);
|
||||
// — re-renders that pass the same prompt back must not reseed. If the
|
||||
// initial useState above already consumed a non-empty initialDraft we
|
||||
// mark it seeded immediately, so an early clear by the user (typing or
|
||||
// backspace before the parent stops passing initialDraft) does not get
|
||||
// overwritten by the effect.
|
||||
const seededRef = useRef(Boolean(initialDraft));
|
||||
|
||||
useEffect(() => {
|
||||
if (seededRef.current) return;
|
||||
|
||||
@@ -31,7 +31,7 @@ const SECTION_LABEL_KEY: Record<Section, keyof Dict> = {
|
||||
const SECTION_ORDER: Section[] = ['pages', 'sketches', 'scripts', 'images', 'other'];
|
||||
|
||||
/**
|
||||
* Full-panel browser for a project's `.ocd/projects/<id>/` folder. Mirrors
|
||||
* Full-panel browser for a project's `.od/projects/<id>/` folder. Mirrors
|
||||
* Claude Design's "Design Files" surface: grouped sections, hover-revealed
|
||||
* row menu, drop-files footer, and (when a row is selected) a right-side
|
||||
* preview pane. Triggered as a sticky first tab in FileWorkspace.
|
||||
|
||||
@@ -5,6 +5,8 @@ import type {
|
||||
AppConfig,
|
||||
DesignSystemSummary,
|
||||
Project,
|
||||
ProjectKind,
|
||||
ProjectMetadata,
|
||||
ProjectTemplate,
|
||||
SkillSummary,
|
||||
} from '../types';
|
||||
@@ -15,7 +17,7 @@ import { ExamplesTab } from './ExamplesTab';
|
||||
import { Icon } from './Icon';
|
||||
import { LanguageMenu } from './LanguageMenu';
|
||||
import { CenteredLoader } from './Loading';
|
||||
import { NewProjectPanel, type CreateInput, type CreateTab } from './NewProjectPanel';
|
||||
import { NewProjectPanel, type CreateInput } from './NewProjectPanel';
|
||||
|
||||
type TopTab = 'designs' | 'examples' | 'design-systems';
|
||||
|
||||
@@ -38,7 +40,7 @@ interface Props {
|
||||
const SIDEBAR_MIN = 320;
|
||||
const SIDEBAR_MAX = 560;
|
||||
const SIDEBAR_DEFAULT = 380;
|
||||
const SIDEBAR_STORAGE_KEY = 'ocd-entry-sidebar-width';
|
||||
const SIDEBAR_STORAGE_KEY = 'od-entry-sidebar-width';
|
||||
|
||||
function loadSidebarWidth(): number {
|
||||
try {
|
||||
@@ -70,13 +72,6 @@ export function EntryView({
|
||||
const t = useT();
|
||||
const [topTab, setTopTab] = useState<TopTab>('designs');
|
||||
const [previewSystemId, setPreviewSystemId] = useState<string | null>(null);
|
||||
const [panelPreset, setPanelPreset] = useState<{
|
||||
tab: CreateTab;
|
||||
skillId: string | null;
|
||||
name: string;
|
||||
pendingPrompt?: string;
|
||||
nonce: number;
|
||||
} | null>(null);
|
||||
const [sidebarWidth, setSidebarWidth] = useState<number>(() => loadSidebarWidth());
|
||||
const [resizing, setResizing] = useState(false);
|
||||
|
||||
@@ -98,13 +93,16 @@ export function EntryView({
|
||||
: t('settings.noAgentSelected');
|
||||
}, [config.mode, config.model, config.baseUrl, currentAgent, t]);
|
||||
|
||||
// 'Use this prompt' on an example card is a fast path — skip the form and
|
||||
// create the project immediately with sane defaults derived from the skill,
|
||||
// seeding the chat composer with the example prompt via pendingPrompt.
|
||||
function usePromptFromSkill(skill: SkillSummary) {
|
||||
setPanelPreset({
|
||||
tab: tabForSkill(skill),
|
||||
skillId: skill.id,
|
||||
onCreateProject({
|
||||
name: skill.name,
|
||||
skillId: skill.id,
|
||||
designSystemId: null,
|
||||
metadata: metadataForSkill(skill),
|
||||
pendingPrompt: skill.examplePrompt || skill.description,
|
||||
nonce: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -118,10 +116,7 @@ export function EntryView({
|
||||
);
|
||||
|
||||
function handleCreate(input: CreateInput) {
|
||||
onCreateProject({
|
||||
...input,
|
||||
pendingPrompt: panelPreset?.pendingPrompt,
|
||||
});
|
||||
onCreateProject(input);
|
||||
}
|
||||
|
||||
const startWidthRef = useRef(0);
|
||||
@@ -177,15 +172,11 @@ export function EntryView({
|
||||
</div>
|
||||
</div>
|
||||
<NewProjectPanel
|
||||
key={panelPreset?.nonce ?? 'default'}
|
||||
skills={skills}
|
||||
designSystems={designSystems}
|
||||
defaultDesignSystemId={defaultDesignSystemId}
|
||||
templates={templates}
|
||||
onCreate={handleCreate}
|
||||
presetTab={panelPreset?.tab}
|
||||
presetSkillId={panelPreset?.skillId ?? null}
|
||||
presetName={panelPreset?.name}
|
||||
loading={loading}
|
||||
/>
|
||||
<div className="entry-side-foot">
|
||||
@@ -313,8 +304,38 @@ function TopTabButton({
|
||||
);
|
||||
}
|
||||
|
||||
function tabForSkill(skill: SkillSummary): CreateTab {
|
||||
// Map a skill's declared mode to project metadata. Falls back to the same
|
||||
// defaults the new-project form would apply (high-fidelity prototype, no
|
||||
// speaker notes on decks, no template animations) so 'Use this prompt'
|
||||
// produces a project indistinguishable from one created via the form. Per-
|
||||
// skill hints in SKILL.md frontmatter (od.fidelity, od.speaker_notes,
|
||||
// od.animations) override the defaults so each example reproduces the
|
||||
// shipped example.html — e.g. wireframe-sketch declares fidelity:wireframe.
|
||||
function metadataForSkill(skill: SkillSummary): ProjectMetadata {
|
||||
const kind = kindForSkill(skill);
|
||||
if (kind === 'prototype') {
|
||||
return { kind, fidelity: skill.fidelity ?? 'high-fidelity' };
|
||||
}
|
||||
if (kind === 'deck') {
|
||||
return {
|
||||
kind,
|
||||
speakerNotes:
|
||||
typeof skill.speakerNotes === 'boolean' ? skill.speakerNotes : false,
|
||||
};
|
||||
}
|
||||
if (kind === 'template') {
|
||||
return {
|
||||
kind,
|
||||
animations:
|
||||
typeof skill.animations === 'boolean' ? skill.animations : false,
|
||||
};
|
||||
}
|
||||
return { kind: 'other' };
|
||||
}
|
||||
|
||||
function kindForSkill(skill: SkillSummary): ProjectKind {
|
||||
if (skill.mode === 'deck') return 'deck';
|
||||
if (skill.mode === 'prototype') return 'prototype';
|
||||
return 'template';
|
||||
if (skill.mode === 'template') return 'template';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
@@ -141,13 +141,23 @@ function HtmlViewer({
|
||||
};
|
||||
}, [projectId, file.name, file.mtime, liveHtml, reloadKey]);
|
||||
|
||||
// Detect deck-shaped HTML even when the project's skill didn't declare
|
||||
// `mode: deck`. Freeform projects often produce a deck because the user
|
||||
// asked for one in plain prose; without this, prev/next and Present
|
||||
// never surface and the deck becomes a static, unnavigable preview.
|
||||
const looksLikeDeck = useMemo(() => {
|
||||
if (!source) return false;
|
||||
return /class\s*=\s*['"][^'"]*\bslide\b/i.test(source);
|
||||
}, [source]);
|
||||
const effectiveDeck = isDeck || looksLikeDeck;
|
||||
|
||||
const srcDoc = useMemo(
|
||||
() => (source ? buildSrcdoc(source, { deck: isDeck }) : ''),
|
||||
[source, isDeck],
|
||||
() => (source ? buildSrcdoc(source, { deck: effectiveDeck }) : ''),
|
||||
[source, effectiveDeck],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDeck) {
|
||||
if (!effectiveDeck) {
|
||||
setSlideState(null);
|
||||
return;
|
||||
}
|
||||
@@ -155,24 +165,24 @@ function HtmlViewer({
|
||||
const data = ev?.data as
|
||||
| { type?: string; active?: number; count?: number }
|
||||
| null;
|
||||
if (!data || data.type !== 'ocd:slide-state') return;
|
||||
if (!data || data.type !== 'od:slide-state') return;
|
||||
if (typeof data.active !== 'number' || typeof data.count !== 'number') return;
|
||||
setSlideState({ active: data.active, count: data.count });
|
||||
}
|
||||
window.addEventListener('message', onMessage);
|
||||
return () => window.removeEventListener('message', onMessage);
|
||||
}, [isDeck]);
|
||||
}, [effectiveDeck]);
|
||||
|
||||
function postSlide(action: 'next' | 'prev' | 'first' | 'last') {
|
||||
const win = iframeRef.current?.contentWindow;
|
||||
if (!win) return;
|
||||
win.postMessage({ type: 'ocd:slide', action }, '*');
|
||||
win.postMessage({ type: 'od:slide', action }, '*');
|
||||
}
|
||||
|
||||
// Keyboard nav on the host, so the user can press ←/→ even when focus
|
||||
// is on the chat composer or any other host control.
|
||||
useEffect(() => {
|
||||
if (!isDeck || mode !== 'preview') return;
|
||||
if (!effectiveDeck || mode !== 'preview') return;
|
||||
function onKey(e: KeyboardEvent) {
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (target) {
|
||||
@@ -195,7 +205,7 @@ function HtmlViewer({
|
||||
}
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [isDeck, mode]);
|
||||
}, [effectiveDeck, mode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!presentMenuOpen) return;
|
||||
@@ -309,7 +319,7 @@ function HtmlViewer({
|
||||
setZoom((z) => Math.max(25, Math.min(200, z + delta)));
|
||||
}
|
||||
|
||||
const showPresent = isDeck && source !== null;
|
||||
const showPresent = effectiveDeck && source !== null;
|
||||
const canShare = source !== null;
|
||||
const exportTitle = file.name.replace(/\.html?$/i, '') || file.name;
|
||||
const canPptx = canShare && Boolean(onExportAsPptx) && !streaming;
|
||||
@@ -328,7 +338,7 @@ function HtmlViewer({
|
||||
>
|
||||
<Icon name="reload" size={14} />
|
||||
</button>
|
||||
{isDeck ? (
|
||||
{effectiveDeck ? (
|
||||
<span
|
||||
className="deck-nav"
|
||||
role="group"
|
||||
@@ -502,12 +512,12 @@ function HtmlViewer({
|
||||
role="menuitem"
|
||||
onClick={() => {
|
||||
setShareMenuOpen(false);
|
||||
exportAsPdf(source ?? '', exportTitle, { deck: isDeck });
|
||||
exportAsPdf(source ?? '', exportTitle, { deck: effectiveDeck });
|
||||
}}
|
||||
>
|
||||
<span className="share-menu-icon"><Icon name="file" size={14} /></span>
|
||||
<span>
|
||||
{isDeck
|
||||
{effectiveDeck
|
||||
? t('fileViewer.exportPdfAllSlides')
|
||||
: t('fileViewer.exportPdf')}
|
||||
</span>
|
||||
|
||||
@@ -28,9 +28,6 @@ interface Props {
|
||||
defaultDesignSystemId: string | null;
|
||||
templates: ProjectTemplate[];
|
||||
onCreate: (input: CreateInput) => void;
|
||||
presetTab?: CreateTab;
|
||||
presetSkillId?: string | null;
|
||||
presetName?: string;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
@@ -47,14 +44,11 @@ export function NewProjectPanel({
|
||||
defaultDesignSystemId,
|
||||
templates,
|
||||
onCreate,
|
||||
presetTab,
|
||||
presetSkillId,
|
||||
presetName,
|
||||
loading = false,
|
||||
}: Props) {
|
||||
const t = useT();
|
||||
const [tab, setTab] = useState<CreateTab>(presetTab ?? 'prototype');
|
||||
const [name, setName] = useState(presetName ?? '');
|
||||
const [tab, setTab] = useState<CreateTab>('prototype');
|
||||
const [name, setName] = useState('');
|
||||
// Design-system selection is now an *array* internally so the same
|
||||
// component can drive both single-select and multi-select modes without
|
||||
// duplicating state. Single-select coerces to length 0/1.
|
||||
@@ -89,7 +83,6 @@ export function NewProjectPanel({
|
||||
// pick a default-rendered skill (so the agent gets the right SKILL.md
|
||||
// body) without requiring the user to choose one explicitly.
|
||||
const skillIdForTab = useMemo(() => {
|
||||
if (presetSkillId !== undefined) return presetSkillId;
|
||||
if (tab === 'other') return null;
|
||||
if (tab === 'prototype') {
|
||||
const list = skills.filter((s) => s.mode === 'prototype');
|
||||
@@ -104,7 +97,7 @@ export function NewProjectPanel({
|
||||
?? null;
|
||||
}
|
||||
return null;
|
||||
}, [tab, skills, presetSkillId]);
|
||||
}, [tab, skills]);
|
||||
|
||||
const canCreate =
|
||||
!loading && (tab !== 'template' || templateId != null);
|
||||
|
||||
@@ -666,9 +666,22 @@ export function ProjectView({
|
||||
[skills, project.skillId],
|
||||
);
|
||||
|
||||
// Hand the pending prompt to ChatPane exactly once. After the first render
|
||||
// we tell App to clear it so re-entering the project later doesn't reseed.
|
||||
const initialDraft = project.pendingPrompt;
|
||||
// Hand the pending prompt to ChatPane exactly once. We snapshot the value
|
||||
// into local state on mount so it survives the ChatPane remount triggered
|
||||
// when `activeConversationId` resolves from `null` to a real id (the
|
||||
// `key={activeConversationId}` on ChatPane otherwise wipes the freshly
|
||||
// seeded composer draft). Once the conversation id is in place — meaning
|
||||
// ChatPane has remounted with the seed still available — we clear both
|
||||
// the local snapshot and the persisted pendingPrompt so future
|
||||
// conversation switches don't keep re-seeding the composer.
|
||||
const [initialDraft, setInitialDraft] = useState<string | undefined>(
|
||||
project.pendingPrompt,
|
||||
);
|
||||
useEffect(() => {
|
||||
if (initialDraft && activeConversationId) {
|
||||
setInitialDraft(undefined);
|
||||
}
|
||||
}, [initialDraft, activeConversationId]);
|
||||
useEffect(() => {
|
||||
if (project.pendingPrompt) onClearPendingPrompt();
|
||||
}, [project.pendingPrompt, onClearPendingPrompt]);
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ const DICTS: Record<Locale, Dict> = {
|
||||
'zh-CN': zhCN,
|
||||
};
|
||||
|
||||
const LS_KEY = 'open-claude-design:locale';
|
||||
const LS_KEY = 'open-design:locale';
|
||||
|
||||
// First-run default is English. We honor an explicit user pick saved to
|
||||
// localStorage but never auto-detect from `navigator.language`, so the
|
||||
|
||||
@@ -37,13 +37,13 @@ export const en: Dict = {
|
||||
'common.daysShort': '{n}d',
|
||||
'common.untitled': 'Untitled',
|
||||
|
||||
'app.brand': 'Open Claude Design',
|
||||
'app.brand': 'Open Design',
|
||||
'app.brandPill': 'Research Preview',
|
||||
'app.brandSubtitle': 'by Nexu Labs',
|
||||
'app.welcomeLoading': 'Loading workspace…',
|
||||
|
||||
'settings.welcomeKicker': 'Welcome',
|
||||
'settings.welcomeTitle': 'Set up Open Claude Design',
|
||||
'settings.welcomeTitle': 'Set up Open Design',
|
||||
'settings.welcomeSubtitle':
|
||||
"Pick how you'd like to run generations. You can change this any time from the Settings button in the top bar.",
|
||||
'settings.kicker': 'Settings',
|
||||
|
||||
@@ -37,13 +37,13 @@ export const zhCN: Dict = {
|
||||
'common.daysShort': '{n}天',
|
||||
'common.untitled': '未命名',
|
||||
|
||||
'app.brand': 'Open Claude Design',
|
||||
'app.brand': 'Open Design',
|
||||
'app.brandPill': '研究预览版',
|
||||
'app.brandSubtitle': '由 Nexu Labs 出品',
|
||||
'app.welcomeLoading': '正在加载工作区…',
|
||||
|
||||
'settings.welcomeKicker': '欢迎',
|
||||
'settings.welcomeTitle': '初始化 Open Claude Design',
|
||||
'settings.welcomeTitle': '初始化 Open Design',
|
||||
'settings.welcomeSubtitle':
|
||||
'选择你希望使用的执行方式。后续可以随时从顶部「设置」按钮中修改。',
|
||||
'settings.kicker': '设置',
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/* ============================================================
|
||||
Open Claude Design — visual language modeled on claude.ai/design
|
||||
Open Design — visual language modeled on claude.ai/design
|
||||
============================================================ */
|
||||
:root {
|
||||
/* Surface palette — warmer paper, hairline borders, soft shadows.
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
* Drift fixes baked in:
|
||||
* - `transform-origin: top left` and the stage is positioned by grid +
|
||||
* place-items, so scaling never shifts content sideways inside the
|
||||
* OCD viewer's nested transform wrapper.
|
||||
* OD viewer's nested transform wrapper.
|
||||
* - Capture-phase keydown on BOTH window and document so iframe focus
|
||||
* quirks can't swallow arrow keys.
|
||||
* - Auto-focus body on load and on every click.
|
||||
@@ -242,7 +242,7 @@ export const DECK_SKELETON_HTML = `<!doctype html>
|
||||
// The stage is 1920×1080 and positioned by .deck-shell's
|
||||
// \`display:grid;place-items:center\`. We scale via transform with
|
||||
// transform-origin:top-left, then re-center by translating to the
|
||||
// remainder. This survives nested transforms (e.g. when the OCD viewer
|
||||
// remainder. This survives nested transforms (e.g. when the OD viewer
|
||||
// wraps the iframe in its own scale wrapper at zoom != 100%).
|
||||
function fit() {
|
||||
var sw = window.innerWidth;
|
||||
@@ -277,7 +277,7 @@ export const DECK_SKELETON_HTML = `<!doctype html>
|
||||
else if (e.key === 'Home') { e.preventDefault(); go(0); }
|
||||
else if (e.key === 'End') { e.preventDefault(); go(slides.length - 1); }
|
||||
}
|
||||
// Capture phase + listen on both targets — inside the OCD iframe,
|
||||
// Capture phase + listen on both targets — inside the OD iframe,
|
||||
// focus may be on window OR document; a single non-capture listener
|
||||
// silently misses presses.
|
||||
window.addEventListener('keydown', onKey, true);
|
||||
@@ -328,7 +328,7 @@ You may edit only inside slots marked \`SLOT:\`:
|
||||
|
||||
These are the failure patterns we just spent days debugging. Each one looks "equivalent" but breaks something specific:
|
||||
|
||||
- ❌ Don't write your own \`fit()\` function or \`transform: scale()\` script. The framework already does it, and ad-hoc versions drift inside the OCD viewer's nested transform wrapper.
|
||||
- ❌ Don't write your own \`fit()\` function or \`transform: scale()\` script. The framework already does it, and ad-hoc versions drift inside the OD viewer's nested transform wrapper.
|
||||
- ❌ Don't use \`transform-origin: center center\` on the stage. The framework uses \`top left\` plus an explicit translate so scaled content lands at the same place every render.
|
||||
- ❌ Don't use \`document.addEventListener('keydown', …)\` alone. Inside an iframe, focus is sometimes on window. The framework adds capture-phase listeners on **both** targets — replacing this with a single listener silently swallows arrow keys.
|
||||
- ❌ Don't replace the localStorage key, the slide-visibility toggle (\`.slide.active\`), or the counter element IDs (\`#deck-cur\`, \`#deck-total\`, \`#deck-prev\`, \`#deck-next\`). The framework reads them by ID.
|
||||
@@ -338,7 +338,7 @@ These are the failure patterns we just spent days debugging. Each one looks "equ
|
||||
|
||||
## Why this matters (so you can judge edge cases)
|
||||
|
||||
The framework is a contract with the host viewer. The OCD iframe sits inside a transformed wrapper (the zoom control); the keyboard handler needs capture phase + dual targets; "Share → PDF" reads the print stylesheet; the position survives reloads via localStorage. If a turn rewrites any of these — even with "equivalent" code — the next turn diverges, and three turns in the deck has subtly broken nav and a one-page PDF. Treat the framework as load-bearing infrastructure.
|
||||
The framework is a contract with the host viewer. The OD iframe sits inside a transformed wrapper (the zoom control); the keyboard handler needs capture phase + dual targets; "Share → PDF" reads the print stylesheet; the position survives reloads via localStorage. If a turn rewrites any of these — even with "equivalent" code — the next turn diverges, and three turns in the deck has subtly broken nav and a one-page PDF. Treat the framework as load-bearing infrastructure.
|
||||
|
||||
If the user asks for something the framework genuinely doesn't support (vertical decks, custom slide transitions, multi-column simultaneous slides), say so and ask before forking. **Default answer: keep the framework, change the slide content.**
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Discovery + planning + huashu-philosophy directives.
|
||||
*
|
||||
* This is the dominant layer of the composed system prompt. It stacks
|
||||
* BEFORE the official OCD designer prompt so the hard rules below — emit
|
||||
* BEFORE the official OD designer prompt so the hard rules below — emit
|
||||
* a discovery form on turn 1, branch into a direction picker / brand
|
||||
* extraction on turn 2, plan with TodoWrite on turn 3 — beat the softer
|
||||
* "skip questions for small tweaks" wording in the base prompt.
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
import { renderDirectionFormBody, renderDirectionSpecBlock } from './directions';
|
||||
|
||||
export const DISCOVERY_AND_PHILOSOPHY = `# OCD core directives (read first — these override anything later in this prompt)
|
||||
export const DISCOVERY_AND_PHILOSOPHY = `# OD core directives (read first — these override anything later in this prompt)
|
||||
|
||||
You are an expert designer working with the user as your manager. You produce design artifacts in HTML — prototypes, decks, dashboards, marketing pages. **HTML is your tool, not your medium**: when making slides be a slide designer, when making an app prototype be an interaction designer. Don't write a web page when the brief is a deck.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* The base system prompt for Open Claude Design.
|
||||
* The base system prompt for Open Design.
|
||||
*
|
||||
* Adapted from claude.ai/design's "expert designer" prompt — same identity,
|
||||
* workflow, and content philosophy, retargeted to the tools an OCD-managed
|
||||
* workflow, and content philosophy, retargeted to the tools an OD-managed
|
||||
* agent actually has (Claude Code's Read / Edit / Write / Bash / Glob / Grep
|
||||
* / TodoWrite, plus the project folder as cwd).
|
||||
*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Prompt composer. The base is the OCD-adapted "expert designer" system
|
||||
* Prompt composer. The base is the OD-adapted "expert designer" system
|
||||
* prompt (see ./official-system.ts) — a full identity, workflow, and
|
||||
* content-philosophy charter. Stacked on top:
|
||||
*
|
||||
|
||||
@@ -80,7 +80,7 @@ export async function fetchSkillExample(id: string): Promise<string | null> {
|
||||
}
|
||||
}
|
||||
|
||||
// Project files — all paths are scoped under .ocd/projects/<id>/ on disk.
|
||||
// Project files — all paths are scoped under .od/projects/<id>/ on disk.
|
||||
|
||||
export async function fetchProjectFiles(projectId: string): Promise<ProjectFile[]> {
|
||||
try {
|
||||
|
||||
@@ -44,7 +44,7 @@ export function exportAsZip(html: string, title: string): void {
|
||||
{ path: `${slug}/index.html`, content: doc },
|
||||
{
|
||||
path: `${slug}/README.md`,
|
||||
content: `# ${title || slug}\n\nGenerated by Open Claude Design.\nOpen index.html in a browser to view.\n`,
|
||||
content: `# ${title || slug}\n\nGenerated by Open Design.\nOpen index.html in a browser to view.\n`,
|
||||
},
|
||||
]);
|
||||
triggerDownload(blob, `${slug}.zip`);
|
||||
|
||||
+210
-21
@@ -9,9 +9,9 @@
|
||||
* When `options.deck` is set we also inject a `postMessage` listener that
|
||||
* lets the host advance / rewind slides without relying on the iframe
|
||||
* having keyboard focus. The host posts:
|
||||
* { type: 'ocd:slide', action: 'next' | 'prev' | 'first' | 'last' | 'go', index?: number }
|
||||
* { type: 'od:slide', action: 'next' | 'prev' | 'first' | 'last' | 'go', index?: number }
|
||||
* and the iframe responds with:
|
||||
* { type: 'ocd:slide-state', active: number, count: number }
|
||||
* { type: 'od:slide-state', active: number, count: number }
|
||||
* after every navigation so the host can render its own counter / dots.
|
||||
*/
|
||||
export function buildSrcdoc(
|
||||
@@ -30,51 +30,240 @@ export function buildSrcdoc(
|
||||
</head>
|
||||
<body>${html}</body>
|
||||
</html>`;
|
||||
if (!options.deck) return wrapped;
|
||||
return injectDeckBridge(wrapped);
|
||||
const withShim = injectSandboxShim(wrapped);
|
||||
if (!options.deck) return withShim;
|
||||
return injectDeckBridge(withShim);
|
||||
}
|
||||
|
||||
// Sandboxed iframes (we use `sandbox="allow-scripts"`) without
|
||||
// `allow-same-origin` raise a SecurityError on first `localStorage` /
|
||||
// `sessionStorage` access. Many freeform-generated decks call
|
||||
// `localStorage.getItem(...)` at the top of their IIFE without a
|
||||
// try/catch — when it throws, the whole script aborts and the deck
|
||||
// becomes a static, unnavigable preview. We install a same-origin
|
||||
// in-memory shim BEFORE any user script runs so those decks degrade
|
||||
// gracefully (position just doesn't persist across reloads).
|
||||
function injectSandboxShim(doc: string): string {
|
||||
const shim = `<script>(function(){
|
||||
function makeStore(){
|
||||
var data = {};
|
||||
var api = {
|
||||
getItem: function(k){ return Object.prototype.hasOwnProperty.call(data, k) ? data[k] : null; },
|
||||
setItem: function(k, v){ data[k] = String(v); },
|
||||
removeItem: function(k){ delete data[k]; },
|
||||
clear: function(){ data = {}; },
|
||||
key: function(i){ return Object.keys(data)[i] || null; }
|
||||
};
|
||||
Object.defineProperty(api, 'length', { get: function(){ return Object.keys(data).length; } });
|
||||
return api;
|
||||
}
|
||||
function tryShim(name){
|
||||
var works = false;
|
||||
try { works = !!window[name] && typeof window[name].getItem === 'function'; void window[name].length; }
|
||||
catch (_) { works = false; }
|
||||
if (works) return;
|
||||
try { Object.defineProperty(window, name, { configurable: true, value: makeStore() }); }
|
||||
catch (_) { try { window[name] = makeStore(); } catch (__) {} }
|
||||
}
|
||||
tryShim('localStorage');
|
||||
tryShim('sessionStorage');
|
||||
})();</script>`;
|
||||
if (/<head[^>]*>/i.test(doc)) return doc.replace(/<head[^>]*>/i, (m) => `${m}${shim}`);
|
||||
if (/<body[^>]*>/i.test(doc)) return doc.replace(/<body[^>]*>/i, (m) => `${m}${shim}`);
|
||||
return shim + doc;
|
||||
}
|
||||
|
||||
// The deck bridge supports three deck conventions found across our skills
|
||||
// and freeform-generated artifacts:
|
||||
// 1. Horizontal scroll decks (simple-deck, guizang-ppt) — slides laid out
|
||||
// side-by-side, navigation = scrollTo({ left }).
|
||||
// 2. Class-toggle decks (deck-framework, freeform pitches) — one slide
|
||||
// carries `.active` or `.is-active`; siblings are display:none. Their
|
||||
// own JS listens for ArrowRight/Left, so we drive them by dispatching
|
||||
// synthetic KeyboardEvents.
|
||||
// 3. Visibility-only decks — no class toggle, slides hidden via inline
|
||||
// style. We fall back to keyboard dispatch + visibility detection.
|
||||
//
|
||||
// All three report `{ active, count }` back to the host so the toolbar can
|
||||
// render a unified counter. A MutationObserver on each `.slide` lets us
|
||||
// catch class changes from the deck's own keyboard handler.
|
||||
//
|
||||
// We also inject a small CSS override that fixes a common authoring
|
||||
// mistake in fixed-canvas decks: a `.stage { display: grid; place-items:
|
||||
// center }` only centers items within their grid cells, but the track
|
||||
// itself stays `start`-aligned, so the 1920x1080 canvas top-lefts at
|
||||
// (0,0) of the stage. Combined with `transform-origin: center center`,
|
||||
// the scaled canvas ends up offset toward the bottom-right of any
|
||||
// preview that's smaller than 1920x1080 — exactly what users see in the
|
||||
// sandbox iframe. `place-content: center` centers the track itself.
|
||||
function injectDeckBridge(doc: string): string {
|
||||
const styleFix = `<style data-od-deck-fix>
|
||||
.stage, .deck-stage, .deck-shell { place-content: center !important; }
|
||||
</style>`;
|
||||
const docWithStyle = /<\/head>/i.test(doc)
|
||||
? doc.replace(/<\/head>/i, styleFix + '</head>')
|
||||
: /<head[^>]*>/i.test(doc)
|
||||
? doc.replace(/<head[^>]*>/i, (m) => m + styleFix)
|
||||
: styleFix + doc;
|
||||
doc = docWithStyle;
|
||||
const script = `<script>(function(){
|
||||
function slides(){ return document.querySelectorAll('.slide'); }
|
||||
function scroller(){
|
||||
if (document.body.scrollWidth > document.body.clientWidth + 1) return document.body;
|
||||
if (document.body && document.body.scrollWidth > document.body.clientWidth + 1) return document.body;
|
||||
return document.scrollingElement || document.documentElement;
|
||||
}
|
||||
function activeIndex(){
|
||||
return Math.round(scroller().scrollLeft / window.innerWidth);
|
||||
function isScrollDeck(){
|
||||
var sc = scroller();
|
||||
return !!(sc && sc.scrollWidth > sc.clientWidth + 1);
|
||||
}
|
||||
function go(i){
|
||||
function findActiveByClass(list){
|
||||
for (var i=0; i<list.length; i++) {
|
||||
var cl = list[i].classList;
|
||||
if (cl && (cl.contains('is-active') || cl.contains('active') || cl.contains('current'))) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
function findActiveByVisibility(list){
|
||||
for (var i=0; i<list.length; i++) {
|
||||
try {
|
||||
var cs = window.getComputedStyle(list[i]);
|
||||
if (cs.display !== 'none' && cs.visibility !== 'hidden' && cs.opacity !== '0') return i;
|
||||
} catch (_) {}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
function activeIndex(list){
|
||||
if (!list || !list.length) return 0;
|
||||
if (isScrollDeck()) {
|
||||
var w = Math.max(1, window.innerWidth);
|
||||
return Math.max(0, Math.min(list.length - 1, Math.round(scroller().scrollLeft / w)));
|
||||
}
|
||||
var byClass = findActiveByClass(list);
|
||||
if (byClass >= 0) return byClass;
|
||||
var byVis = findActiveByVisibility(list);
|
||||
if (byVis >= 0) return byVis;
|
||||
return 0;
|
||||
}
|
||||
function dispatchKey(key){
|
||||
// Bubbles so any listener on window picks it up too. We dispatch on
|
||||
// document only — dispatching on window/body in addition would cause
|
||||
// bubbling to fire the same document-level listener twice.
|
||||
var init = { key: key, code: key, bubbles: true, cancelable: true, composed: true };
|
||||
try {
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', init));
|
||||
document.dispatchEvent(new KeyboardEvent('keyup', init));
|
||||
} catch (_) {}
|
||||
}
|
||||
function scrollGo(i){
|
||||
var list = slides();
|
||||
if (!list.length) return;
|
||||
var next = Math.max(0, Math.min(list.length - 1, i));
|
||||
scroller().scrollTo({ left: next * window.innerWidth, behavior: 'smooth' });
|
||||
setTimeout(report, 360);
|
||||
setTimeout(report, 380);
|
||||
}
|
||||
function go(action){
|
||||
var list = slides();
|
||||
if (!list.length) return;
|
||||
if (isScrollDeck()) {
|
||||
var i = activeIndex(list);
|
||||
if (action === 'next') scrollGo(i + 1);
|
||||
else if (action === 'prev') scrollGo(i - 1);
|
||||
else if (action === 'first') scrollGo(0);
|
||||
else if (action === 'last') scrollGo(list.length - 1);
|
||||
return;
|
||||
}
|
||||
if (action === 'next') dispatchKey('ArrowRight');
|
||||
else if (action === 'prev') dispatchKey('ArrowLeft');
|
||||
else if (action === 'first') dispatchKey('Home');
|
||||
else if (action === 'last') dispatchKey('End');
|
||||
setTimeout(report, 280);
|
||||
}
|
||||
function gotoIndex(i){
|
||||
var list = slides();
|
||||
if (!list.length) return;
|
||||
var target = Math.max(0, Math.min(list.length - 1, i));
|
||||
if (isScrollDeck()) { scrollGo(target); return; }
|
||||
var current = activeIndex(list);
|
||||
var diff = target - current;
|
||||
if (!diff) { report(); return; }
|
||||
var key = diff > 0 ? 'ArrowRight' : 'ArrowLeft';
|
||||
var n = Math.abs(diff);
|
||||
for (var k = 0; k < n; k++) dispatchKey(key);
|
||||
setTimeout(report, 320);
|
||||
}
|
||||
function report(){
|
||||
try {
|
||||
var list = slides();
|
||||
window.parent.postMessage({
|
||||
type: 'ocd:slide-state',
|
||||
active: activeIndex(),
|
||||
type: 'od:slide-state',
|
||||
active: activeIndex(list),
|
||||
count: list.length,
|
||||
}, '*');
|
||||
} catch (e) {}
|
||||
}
|
||||
window.addEventListener('message', function(ev){
|
||||
var data = ev && ev.data;
|
||||
if (!data || data.type !== 'ocd:slide') return;
|
||||
var list = slides();
|
||||
var i = activeIndex();
|
||||
if (data.action === 'next') go(i + 1);
|
||||
else if (data.action === 'prev') go(i - 1);
|
||||
else if (data.action === 'first') go(0);
|
||||
else if (data.action === 'last') go(list.length - 1);
|
||||
else if (data.action === 'go' && typeof data.index === 'number') go(data.index);
|
||||
if (!data || data.type !== 'od:slide') return;
|
||||
if (data.action === 'go' && typeof data.index === 'number') gotoIndex(data.index);
|
||||
else go(data.action);
|
||||
});
|
||||
// Report once on load and on every scroll-end so the host stays in sync.
|
||||
window.addEventListener('load', function(){ setTimeout(report, 200); });
|
||||
document.addEventListener('scroll', function(){ clearTimeout(window.__ocdReportT); window.__ocdReportT = setTimeout(report, 120); }, { passive: true, capture: true });
|
||||
document.addEventListener('scroll', function(){
|
||||
clearTimeout(window.__odReportT);
|
||||
window.__odReportT = setTimeout(report, 120);
|
||||
}, { passive: true, capture: true });
|
||||
// Nudge the deck's own fit/resize listener after layout settles. Fixed-canvas
|
||||
// decks (e.g. ".canvas { width: 1920px }" + "transform: scale(...)") compute
|
||||
// their scale on first run, which fires when the iframe is still 0x0 in
|
||||
// sandboxed previews — the deck's fit() then resolves to scale(0) / scale(1)
|
||||
// and never recovers. Re-firing 'resize' lets the deck recompute, and a
|
||||
// ResizeObserver picks up later layout settles (zoom toggle, sidebar drag).
|
||||
function nudgeResize(){
|
||||
try { window.dispatchEvent(new Event('resize')); }
|
||||
catch (_) {}
|
||||
}
|
||||
// Aggressively nudge during the first second so the deck catches the
|
||||
// iframe's first non-zero size; bail out early once the iframe reports a
|
||||
// real width. Without this loop, fixed-canvas decks render at scale(0).
|
||||
function chaseFirstLayout(){
|
||||
var attempts = 0;
|
||||
function tick(){
|
||||
attempts += 1;
|
||||
var w = window.innerWidth;
|
||||
nudgeResize();
|
||||
if (w > 0 && attempts >= 2) return; // one extra nudge after first non-zero
|
||||
if (attempts < 30) setTimeout(tick, 50);
|
||||
}
|
||||
tick();
|
||||
}
|
||||
if (document.readyState === 'complete') chaseFirstLayout();
|
||||
else window.addEventListener('load', chaseFirstLayout);
|
||||
// Re-nudge whenever the iframe itself is resized by the host (e.g.
|
||||
// user toggles zoom, resizes the chat sidebar, exits Present).
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
try {
|
||||
var ro = new ResizeObserver(function(){ nudgeResize(); });
|
||||
ro.observe(document.documentElement);
|
||||
} catch (_) {}
|
||||
}
|
||||
// For class-toggle decks the deck's own keyboard handler updates classes
|
||||
// on the slide elements; an attribute observer translates that into the
|
||||
// host counter without depending on scroll events.
|
||||
function observeSlides(){
|
||||
var list = slides();
|
||||
if (!list.length) { setTimeout(observeSlides, 150); return; }
|
||||
try {
|
||||
var mo = new MutationObserver(function(){
|
||||
clearTimeout(window.__odReportT2);
|
||||
window.__odReportT2 = setTimeout(report, 60);
|
||||
});
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
mo.observe(list[i], { attributes: true, attributeFilter: ['class', 'style', 'hidden', 'aria-hidden'] });
|
||||
}
|
||||
} catch (e) {}
|
||||
setTimeout(report, 100);
|
||||
}
|
||||
observeSlides();
|
||||
})();</script>`;
|
||||
if (/<\/body>/i.test(doc)) return doc.replace(/<\/body>/i, `${script}</body>`);
|
||||
return doc + script;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import type { AppConfig } from '../types';
|
||||
|
||||
const STORAGE_KEY = 'open-claude-design:config';
|
||||
const STORAGE_KEY = 'open-design:config';
|
||||
|
||||
export const DEFAULT_CONFIG: AppConfig = {
|
||||
mode: 'daemon',
|
||||
|
||||
+10
-1
@@ -86,8 +86,17 @@ export interface SkillSummary {
|
||||
upstream: string | null;
|
||||
/** Lower number = higher priority in the Examples gallery. `null` keeps
|
||||
* the skill in its natural alphabetical position below all featured
|
||||
* entries. Set via `ocd.featured` in the SKILL.md frontmatter. */
|
||||
* entries. Set via `od.featured` in the SKILL.md frontmatter. */
|
||||
featured?: number | null;
|
||||
/** Optional metadata hints, parsed from `od.fidelity`,
|
||||
* `od.speaker_notes`, and `od.animations` in SKILL.md. Used by the
|
||||
* Examples gallery's "Use this prompt" fast-create path to mirror the
|
||||
* shipped `example.html` (e.g. wireframe-sketch declares
|
||||
* `fidelity: wireframe`). Missing hints fall back to the same defaults
|
||||
* the new-project form would apply. */
|
||||
fidelity?: 'wireframe' | 'high-fidelity' | null;
|
||||
speakerNotes?: boolean | null;
|
||||
animations?: boolean | null;
|
||||
hasBody: boolean;
|
||||
examplePrompt: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user