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]);
|
||||
|
||||
Reference in New Issue
Block a user