diff --git a/TODO.md b/TODO.md
index 1f03251..115dabb 100644
--- a/TODO.md
+++ b/TODO.md
@@ -213,8 +213,8 @@ Pure tab-policy helpers that decide:
- `titleStrategy`: shared title resolver hooks
## Phase 4 — Editor Routing Registry
-- Replace long `if/else` tab-type rendering chain with declarative tab->component mapping.
-- Keep special cases explicit (e.g., tab ID transforms such as git-diff commit/file parsing).
+- [x] Replace long `if/else` tab-type rendering chain with declarative tab->component mapping.
+- [x] Keep special cases explicit (e.g., tab ID transforms such as git-diff commit/file parsing).
## Phase 5 — Menu/Event Harmonization
- Route menu-driven view/tab actions through the same behavior layer where applicable.
@@ -258,4 +258,5 @@ Pure tab-policy helpers that decide:
- [x] Phase 3 continuation slice complete: migrated post/media open paths in Sidebar, Editor, LinkedMediaPanel, and Panel to shared entity-tab policy.
- [x] Phase 3 completion slice: centralized `chat` and `import` tab-open specs and migrated sidebar call sites.
- [x] Phase 3 completion slice: centralized `git-diff` file/commit ID/spec/open helpers and reused shared parsing in `TabBar`.
-- [ ] Next implementation slice: Phase 4 (editor routing registry).
+- [x] Phase 4 complete: introduced `editorRouting` registry/resolver and migrated `Editor` to declarative route->view rendering.
+- [ ] Next implementation slice: Phase 5 (menu/event harmonization).
diff --git a/src/renderer/components/Editor/Editor.tsx b/src/renderer/components/Editor/Editor.tsx
index aeb91c7..9bc0c0f 100644
--- a/src/renderer/components/Editor/Editor.tsx
+++ b/src/renderer/components/Editor/Editor.tsx
@@ -22,6 +22,7 @@ import { AutoSaveManager, getContrastColor } from '../../utils';
import { InsertModal } from '../InsertModal';
import { AISuggestionsModal, AISuggestions } from '../AISuggestionsModal/AISuggestionsModal';
import { openEntityTab } from '../../navigation/tabPolicy';
+import { EditorRoute, resolveEditorRoute } from '../../navigation/editorRouting';
import { useI18n } from '../../i18n';
import './Editor.css';
@@ -1724,20 +1725,7 @@ export const Editor: React.FC = () => {
// Get the active tab
const activeTab = tabs.find(t => t.id === activeTabId);
- // Determine what to show based on active tab
- // Settings and tags should only show when their tab is active, not based on activeView
- // (activeView controls the sidebar, not the main content area)
- const showPost = activeTab?.type === 'post';
- const showMedia = activeTab?.type === 'media';
- const showSettings = activeTab?.type === 'settings';
- const showStyle = activeTab?.type === 'style';
- const showTags = activeTab?.type === 'tags';
- const showChat = activeTab?.type === 'chat';
- const showImport = activeTab?.type === 'import';
- const showMetadataDiff = activeTab?.type === 'metadata-diff';
- const showGitDiff = activeTab?.type === 'git-diff';
- const showDocumentation = activeTab?.type === 'documentation';
- const showSiteValidation = activeTab?.type === 'site-validation';
+ const editorRoute = resolveEditorRoute(activeTab);
useEffect(() => {
const activePostId = activeTab?.type === 'post' ? activeTab.id : null;
@@ -1789,129 +1777,30 @@ export const Editor: React.FC = () => {
);
- // Show settings only if settings tab is active
- if (showSettings) {
- return (
-
-
- {renderErrorModal()}
- {renderConfirmDeleteModal()}
-
- );
- }
+ const editorViewRenderers: Record React.ReactNode> = {
+ settings: () => ,
+ style: () => ,
+ tags: () => ,
+ chat: () => (editorRoute.tabId ? : ),
+ import: () =>
+ editorRoute.tabId ? : ,
+ 'metadata-diff': () => ,
+ 'git-diff': () =>
+ editorRoute.tabId && editorRoute.gitDiffResource
+ ?
+ : ,
+ documentation: () => ,
+ 'site-validation': () => ,
+ post: () => (editorRoute.tabId ? : ),
+ media: () => (editorRoute.tabId ? : ),
+ dashboard: () => ,
+ };
- if (showStyle) {
- return (
-
-
- {renderErrorModal()}
- {renderConfirmDeleteModal()}
-
- );
- }
+ const editorContent = editorViewRenderers[editorRoute.route]();
- // Show tags if tags tab is active
- if (showTags) {
- return (
-
-
- {renderErrorModal()}
- {renderConfirmDeleteModal()}
-
- );
- }
-
- // Show chat if chat tab is active
- if (showChat && activeTabId) {
- return (
-
-
- {renderErrorModal()}
- {renderConfirmDeleteModal()}
-
- );
- }
-
- // Show import analysis if import tab is active
- if (showImport && activeTabId) {
- return (
-
-
- {renderErrorModal()}
- {renderConfirmDeleteModal()}
-
- );
- }
-
- // Show metadata diff if metadata-diff tab is active
- if (showMetadataDiff) {
- return (
-
-
- {renderErrorModal()}
- {renderConfirmDeleteModal()}
-
- );
- }
-
- // Show git diff view if git-diff tab is active
- if (showGitDiff && activeTabId) {
- const filePath = activeTabId.startsWith('git-diff:') ? activeTabId.slice('git-diff:'.length) : activeTabId;
- return (
-
-
- {renderErrorModal()}
- {renderConfirmDeleteModal()}
-
- );
- }
-
- if (showDocumentation) {
- return (
-
-
- {renderErrorModal()}
- {renderConfirmDeleteModal()}
-
- );
- }
-
- if (showSiteValidation) {
- return (
-
-
- {renderErrorModal()}
- {renderConfirmDeleteModal()}
-
- );
- }
-
- // Show post editor if a post tab is active
- if (showPost && activeTabId) {
- return (
-
-
- {renderErrorModal()}
- {renderConfirmDeleteModal()}
-
- );
- }
-
- // Show media editor if a media tab is active
- if (showMedia && activeTabId) {
- return (
-
-
- {renderErrorModal()}
- {renderConfirmDeleteModal()}
-
- );
- }
-
- // No tab active - show dashboard
return (
-
+ {editorContent}
{renderErrorModal()}
{renderConfirmDeleteModal()}
diff --git a/src/renderer/navigation/editorRouting.ts b/src/renderer/navigation/editorRouting.ts
new file mode 100644
index 0000000..5812ef4
--- /dev/null
+++ b/src/renderer/navigation/editorRouting.ts
@@ -0,0 +1,60 @@
+import type { Tab, TabType } from '../store/appStore';
+import { parseGitDiffTabId } from './tabPolicy';
+
+export type EditorRoute =
+ | 'dashboard'
+ | 'post'
+ | 'media'
+ | 'settings'
+ | 'style'
+ | 'tags'
+ | 'chat'
+ | 'import'
+ | 'metadata-diff'
+ | 'git-diff'
+ | 'documentation'
+ | 'site-validation';
+
+export const EDITOR_TAB_ROUTE_REGISTRY: Record> = {
+ post: 'post',
+ media: 'media',
+ settings: 'settings',
+ style: 'style',
+ tags: 'tags',
+ chat: 'chat',
+ import: 'import',
+ 'metadata-diff': 'metadata-diff',
+ 'git-diff': 'git-diff',
+ documentation: 'documentation',
+ 'site-validation': 'site-validation',
+};
+
+export interface EditorRouteResolution {
+ route: EditorRoute;
+ tabId: string | null;
+ gitDiffResource: string | null;
+}
+
+export function resolveEditorRoute(activeTab: Tab | undefined): EditorRouteResolution {
+ if (!activeTab) {
+ return {
+ route: 'dashboard',
+ tabId: null,
+ gitDiffResource: null,
+ };
+ }
+
+ if (activeTab.type === 'git-diff') {
+ return {
+ route: 'git-diff',
+ tabId: activeTab.id,
+ gitDiffResource: parseGitDiffTabId(activeTab.id).resource,
+ };
+ }
+
+ return {
+ route: EDITOR_TAB_ROUTE_REGISTRY[activeTab.type],
+ tabId: activeTab.id,
+ gitDiffResource: null,
+ };
+}
diff --git a/tests/renderer/navigation/editorRouting.test.ts b/tests/renderer/navigation/editorRouting.test.ts
new file mode 100644
index 0000000..86c85d8
--- /dev/null
+++ b/tests/renderer/navigation/editorRouting.test.ts
@@ -0,0 +1,63 @@
+import { describe, expect, it } from 'vitest';
+import type { Tab } from '../../../src/renderer/store/appStore';
+import {
+ EDITOR_TAB_ROUTE_REGISTRY,
+ resolveEditorRoute,
+} from '../../../src/renderer/navigation/editorRouting';
+
+describe('editorRouting', () => {
+ it('defines canonical tab-type to editor-route mapping', () => {
+ expect(EDITOR_TAB_ROUTE_REGISTRY).toEqual({
+ post: 'post',
+ media: 'media',
+ settings: 'settings',
+ style: 'style',
+ tags: 'tags',
+ chat: 'chat',
+ import: 'import',
+ 'metadata-diff': 'metadata-diff',
+ 'git-diff': 'git-diff',
+ documentation: 'documentation',
+ 'site-validation': 'site-validation',
+ });
+ });
+
+ it('resolves dashboard route when no active tab is present', () => {
+ expect(resolveEditorRoute(undefined)).toEqual({
+ route: 'dashboard',
+ tabId: null,
+ gitDiffResource: null,
+ });
+ });
+
+ it('resolves post and media routes with active tab id', () => {
+ const postTab: Tab = { type: 'post', id: 'post-1', isTransient: true };
+ const mediaTab: Tab = { type: 'media', id: 'media-1', isTransient: false };
+
+ expect(resolveEditorRoute(postTab)).toEqual({
+ route: 'post',
+ tabId: 'post-1',
+ gitDiffResource: null,
+ });
+
+ expect(resolveEditorRoute(mediaTab)).toEqual({
+ route: 'media',
+ tabId: 'media-1',
+ gitDiffResource: null,
+ });
+ });
+
+ it('resolves git-diff route and extracts resource from tab id', () => {
+ const tab: Tab = {
+ type: 'git-diff',
+ id: 'git-diff:commit:abc123',
+ isTransient: true,
+ };
+
+ expect(resolveEditorRoute(tab)).toEqual({
+ route: 'git-diff',
+ tabId: 'git-diff:commit:abc123',
+ gitDiffResource: 'commit:abc123',
+ });
+ });
+});