一键导入
frontend-naming-conventions
Use when creating or naming pages, components, stores, hooks, routes, or utilities
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when creating or naming pages, components, stores, hooks, routes, or utilities
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | frontend-naming-conventions |
| description | Use when creating or naming pages, components, stores, hooks, routes, or utilities |
Strict naming conventions for all frontend code entities.
Pattern: Page_[Name]
// Folder: src/pages/Page_Organization/
// File: Page_Organization.tsx
export const Page_Organization = () => <div>Organization page</div>;
Rules: Folder Page_[Name], File Page_[Name].tsx, ONLY export const (NO default exports)
Examples: Page_Root, Page_Login, Page_Organization, Page_AccessDenied
Pattern: Page[Name]_[ComponentName] (remove underscore after "Page")
// ✅ Correct
(PageProjectShot_Files, PageProjectShot_Canvas, PageOrganization_Header);
// ❌ Wrong
ProjectShot_Files; // Missing "Page" prefix
Page_ProjectShot_Files; // Extra underscore after Page
Folder: src/pages/Page_ProjectShot/PageProjectShot_Files/PageProjectShot_Files.tsx
When a subcomponent is ONLY used by another subcomponent:
Pattern: Page[Name]_[ParentComponent]_[ChildComponent]
PageOrganization_WorkspaceProjects_ProjectCard; // Used only by WorkspaceProjects
PageProject_SetCard_ActionsMenu; // Used only by SetCard
Folder: Page_Organization/PageOrganization_WorkspaceProjects/PageOrganization_WorkspaceProjects_ProjectCard/
Pattern: Store_[Name]
// File: src/stores/Store_App.ts
class Store_App_Default {
/* ... */
}
export const Store_App = new Store_App_Default();
export const useStore_App = () => useStore(Store_App);
Use for: Platform APIs (AudioContext, WASM), performance-critical caches, RAF loop data
Pattern: service_[Scope]_[Name] | Variable: s[Name]
| Entity | Pattern | Example |
|---|---|---|
| File | service_[Scope]_[Name].ts | service_PageScene_WebAudio.ts |
| Class | Service_[Scope]_[Name]_Class | Service_PageScene_WebAudio_Class |
| Singleton | service_[Scope]_[Name] | service_PageScene_WebAudio |
| Types | Service_[Scope]_[Name]_[TypeName] | Service_PageScene_WebAudio_ClipData |
// File: pages/Page_Scene/service_PageScene_WebAudio.ts
// Types - exported with full prefix
export interface Service_PageScene_WebAudio_ClipData {
clipId: string;
fileId: string;
}
// Class - internal naming with full prefix
class Service_PageScene_WebAudio_Class {
private buffers = new Map<string, AudioBuffer>();
syncPlayback(clips: Service_PageScene_WebAudio_ClipData[], time: number): void {
// Called from RAF loop
}
}
// Singleton export - matches file name
export const service_PageScene_WebAudio = new Service_PageScene_WebAudio_Class();
// Usage in components
const sWebAudio = service_PageScene_WebAudio;
sWebAudio.syncPlayback(clips, time);
Same as hooks/utils: colocate at closest common parent
Page_Scene children use it → pages/Page_Scene/src/services/ (global)| Use Service | Use Hook |
|---|---|
| Platform APIs (AudioContext, WASM) | React state management |
| Binary data caching (blobs, buffers) | Data fetching (TanStack Query) |
| RAF loop access (60fps reads) | UI state subscriptions |
| Imperative operations | Declarative patterns |
Hook: useQ_[Scope]_[Entity] | Variable: q[Entity] (drop scope prefix)
| Location | Scope Pattern | Example |
|---|---|---|
| Page root | Page[Name] | useQ_PageScene_Scene |
| Subcomponent | Page[Name]_[Subcomponent] | useQ_PageScene_Script_Script |
| Nested subcomponent | Page[Name]_[Parent]_[Child] | useQ_PageScene_Timeline_Track_Clips |
CRITICAL: Scope must match the folder/subcomponent hierarchy. A hook in PageScene_Decks/ folder MUST use scope PageScene_Decks:
// ✅ Correct - scope matches folder location
// File: Page_Scene/PageScene_Decks/useQ_PageScene_Decks_Decks.ts
export const useQ_PageScene_Decks_Decks = () => { ... };
// ❌ Wrong - scope doesn't include subcomponent
// File: Page_Scene/PageScene_Decks/useQ_PageScene_Decks.ts
export const useQ_PageScene_Decks = () => { ... }; // Missing "_Decks" entity!
// ✅ Correct - short variable name
const qOrganization = useQ_PageOrganization_Organization({ organizationId });
qOrganization.query.isLoading;
qOrganization.organization?.projects;
// ❌ Wrong - destructuring (NEVER do this)
const { query, organization } = useQ_PageOrganization_Organization();
// ❌ Wrong - verbose variable name
const qPageOrganization_Organization = useQ_PageOrganization_Organization();
| Hook Location | Folder |
|---|---|
| Page-level | Page_[Name]/useQ_Page[Name]_[Entity].ts |
| Subcomponent | Page_[Name]/Page[Name]_[Subcomp]/useQ_Page[Name]_[Subcomp]_[Entity].ts |
Hook: useM_[Scope]_[EntityAction] | Variable: m[EntityAction]
Scope follows the same hierarchy rules as Query Hooks (see above).
// ✅ Correct - subcomponent scope
// File: Page_Scene/PageScene_Decks/useM_PageScene_Decks_DeckShotCreate.ts
const mDeckShotCreate = useM_PageScene_Decks_DeckShotCreate();
mDeckShotCreate.mutation.mutate({ deck_id: "..." });
// ❌ Wrong - missing subcomponent in scope
// File: Page_Scene/PageScene_Decks/useM_PageScene_DeckShotCreate.ts
const mDeckShotCreate = useM_PageScene_DeckShotCreate(); // Should be _Decks_DeckShotCreate
For hooks that query or mutate junction tables (many-to-many relationships):
Pattern: $Table1$Table2$Relation (use actual table names, NOT invented entity names)
Query: useQ_[Scope]_$Table1$Table2$Relation
Mutation: useM_[Scope]_$Table1$Table2$Relation[Action]
Variable: qRelations / mRelationCreate
// ✅ Correct - uses actual table names
useQ_PageScene_Timeline_SceneAudioTracks$SceneFiles$Relation;
useM_PageScene_Timeline_SceneAudioTracks$SceneFiles$RelationCreate;
// ❌ Wrong - invented entity name
useQ_PageScene_Timeline_AudioClips; // "AudioClip" is not a table
useM_PageScene_Timeline_AudioClipCreate;
Why: Don't invent entity names. Use actual table names to maintain traceability between hooks and database schema.
Hook: useProvider_[Name] | Variable: p[Name]
const pAssetManager = useProvider_Spark_AssetManagerModal();
pAssetManager.state.projectId;
pAssetManager.setState({ projectId: "123" });
const pTheme = useProvider_Theme();
pTheme.toggleTheme();
Pattern: useHotkeys_[ComponentName] - standalone files, colocated with component
// File: useHotkeys_PageScene_Timeline.ts
import { useHotkeys } from "react-hotkeys-hook";
export const useHotkeys_PageScene_Timeline = () => {
const pPageScene = useProvider_Page_Scene();
useHotkeys(
"space",
() => {
pPageScene.setState((prev) => ({
timeline_isPlaying: !prev.timeline_isPlaying,
}));
},
{ preventDefault: true }
);
useHotkeys(
"escape",
() => {
pPageScene.setState({ nodeSelected: null });
},
{ enableOnFormTags: true }
);
};
// Usage - no return, just call it
useHotkeys_PageScene_Timeline();
Rules: Standalone file, use react-hotkeys-hook, no returns (side-effect only)
Implementation details: See frontend-hotkeys for patterns, options, and anti-patterns.
src/routes/__root.tsx // Root layout (double underscore)
src/routes/_protected.tsx // Protected layout (single underscore)
src/routes/_protected/$organizationId.tsx // Dynamic param
src/routes/login.tsx // Public route
Pattern: UI_[ComponentName]
// Folder: src/components/UI_HorizontalNav/UI_HorizontalNav.tsx
export const UI_HorizontalNav = () => <nav>...</nav>;
Requirements: Pure (no external context), reusable, ANTD-based
| Scope | When | Pattern | Location |
|---|---|---|---|
| Global | Different domains (auth + data, infra + UI) | Utils_[Category]_[Name] | src/utils/ |
| Domain | 2+ consumers under common parent | utils_[Scope]_[Name].ts | Closest common parent |
| Single-use | One consumer | const camelCase | Inline in consumer file |
// Global - used across domains
export const Utils_Files_FormatSize = (bytes: number): string => { ... };
// Domain - used by multiple Timeline components
// File: Page_Scene/PageScene_Timeline/utils_PageScene_Timeline_SnapToGrid.ts
export const utils_PageScene_Timeline_SnapToGrid = (time: number): number => { ... };
// Single-use - inline before component
const calculateTickInterval = (pps: number) => { ... };
export const PageScene_Timeline_Ruler = () => { /* uses calculateTickInterval */ };
src/utils/Utils_Category_NameUse for: Static values that don't change - dimensions, px values, text, arrays, options, object maps, configs
Pattern: const_[Scope] | Export: const_[Scope]_[Name]
| Entity | Pattern | Example |
|---|---|---|
| File | const_[Scope].ts | const_PageTestScriptEditor.ts |
| Export | const_[Scope]_[Name] | const_PageTestScriptEditor_HeaderHeight |
// File: pages/Page_TestScriptEditor/const_PageTestScriptEditor.ts
// Dimensions
export const const_PageTestScriptEditor_HeaderHeight = 48;
export const const_PageTestScriptEditor_SidebarWidth = 240;
// Options (for selects/dropdowns)
export const const_PageTestScriptEditor_ZoomLevels = [0.5, 1, 2, 4] as const;
// Text
export const const_PageTestScriptEditor_EmptyStateText = "No scripts found";
// Object maps
export const const_PageTestScriptEditor_BlockTypeLabels = {
scene: "Scene Heading",
action: "Action",
dialogue: "Dialogue",
} as const;
For subcomponent-scoped constants, follow the subcomponent naming pattern:
// File: pages/Page_Scene/PageScene_Timeline/const_PageScene_Timeline.ts
export const const_PageScene_Timeline_TrackHeight = 32;
export const const_PageScene_Timeline_RulerHeight = 24;
export const const_PageScene_Timeline_MinZoom = 0.1;
export const const_PageScene_Timeline_MaxZoom = 10;
| Condition | Action |
|---|---|
| Used in ONE file + small | Inline const in consumer file |
| Used in MULTIPLE files | Extract to const_ file |
| Large value (even if single-use) | Extract to const_ file |
"Large value" = long text strings, big arrays (5+ items), objects with multiple keys
// ✅ Inline - small, single-use
const PADDING = 8;
export const PageScene_Timeline_Ruler = () => {
/* uses PADDING */
};
// ✅ Extract - used by multiple files
// const_PageScene_Timeline.ts
export const const_PageScene_Timeline_TrackHeight = 32;
// ✅ Extract - large object, keeps component file clean
// const_PageScene_Timeline.ts
export const const_PageScene_Timeline_InterpolationOptions = [
{ value: "hold", label: "Hold" },
{ value: "linear", label: "Linear" },
{ value: "ease", label: "Ease In/Out" },
{ value: "bezier", label: "Bezier" },
] as const;
// Import with namespace for clarity
import {
const_PageScene_Timeline_TrackHeight,
const_PageScene_Timeline_RulerHeight,
} from "./const_PageScene_Timeline";
// Use directly (already namespaced via prefix)
const style = { height: const_PageScene_Timeline_TrackHeight };
// ❌ Wrong - no scope prefix
export const HEADER_HEIGHT = 48;
// ❌ Wrong - SCREAMING_SNAKE_CASE (use PascalCase for suffix)
export const const_PageScene_Timeline_TRACK_HEIGHT = 32;
// ❌ Wrong - large object inlined in component file
const PageScene_Timeline = () => {
const INTERPOLATION_OPTIONS = [
/* 10 items */
]; // Should be in const_ file
};
// ❌ Wrong - shared constant not extracted
// File A uses TRACK_HEIGHT, File B uses TRACK_HEIGHT
// Both define it locally instead of sharing via const_ file
Pattern: Modal suffix appended directly (NO underscore before Modal)
// ✅ Correct
(Spark_AssetManagerModal, PageRoot_OrganizationComposerModal);
// ❌ Wrong
Spark_AssetManager_Modal; // Extra underscore
Exception: Spark_Modal (utility wrapper)
CRITICAL: All exports must use export const - NO default exports
// ✅ Correct
export const Page_Organization = () => { ... };
export const useQ_Me = () => { ... };
// ❌ Wrong
function Page_Organization() { ... }
export { Page_Organization };
export default Page_Organization;
| Wrong | Correct |
|---|---|
export const Organization = () | export const Page_Organization = () |
const { query } = useQ_Me() | const qMe = useQ_Me() |
const qPageOrganization_Organization = ... | const qOrganization = ... |
useOrganizations() | useQ_Me_Organizations() |
export default | export const only |
| Single-use util in standalone file | Inline in consumer |
Domain util in global src/utils/ | Place at common parent |
webAudioManager (generic name) | service_PageScene_WebAudio |
Service in features/shared/services/ | Service at closest common parent |
AudioClipPlaybackData (generic type) | Service_PageScene_WebAudio_ClipData |
export const HEADER_HEIGHT = 48 | const_[Scope]_HeaderHeight |
| Large inline arrays/objects in component | Extract to const_ file |
| Same constant defined in multiple files | Share via const_ file |
| Hook in subcomponent missing scope segment | useQ_PageScene_Decks_Decks not useQ_PageScene_Decks |
Never create a hook that only returns one function:
// ❌ Wrong - hook returns single function
const useSomething_Zoom = () => {
const handleZoomWheel = () => { ... };
return { handleZoomWheel };
};
// ✅ Correct - define inline where used
const Component = () => {
useEffect(() => {
const handleZoomWheel = (e: WheelEvent) => { ... };
el.addEventListener("wheel", handleZoomWheel);
return () => el.removeEventListener("wheel", handleZoomWheel);
}, []);
};
Use when deploying Cloudflare Workers, managing R2 storage, or working with Cloudflare infrastructure
Use when working with ANTD components, theme tokens, icons, forms, or feedback components (message/notification/modal)
Use when adding, referencing, or serving static assets (images, fonts, videos, 3D models) through the R2 CDN pipeline with type-safe imports
Use when writing or reviewing JavaScript/TypeScript code for style patterns like concise arrows, inline handlers, expression formatting, or when tempted to use eslint-disable
Use when working with environment variables in frontend code
Use when creating or modifying keyboard shortcuts/hotkeys in frontend code