refactor: derive TechLog navigation from the route contract

Both headers carried their own literal list of {label, path}. That made the
route contract and the header two sources for the same three facts — which
routes are navigable, what they are called, and in what order — with nothing
keeping them in step: a renamed route or a reordered menu could be right in one
place and stale in the other.

techLogNavigation(layoutGroup) derives the menu from TECH_LOG_ROUTE_REGISTRY,
where navigationOrder is what makes a route navigable. Output is byte-identical
to the previous literal lists, pinned by a new test.

Derived from TechLog's own route contract rather than from the composed
registries: .dependency-cruiser.json freezes an exact allowlist of files that
may read src/features/installed-*, explicitly so that coupling cannot spread,
and a feature header is not on it.

The studio header keeps its active-state rules — "작업본" stays highlighted
across /studio/documents/* except on the new-document screen — because that is
presentation behaviour the contract has no opinion about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-17 20:34:36 +09:00
co-authored by Claude Opus 5
parent 93ce86eef4
commit 9e5fbd1384
4 changed files with 129 additions and 23 deletions
@@ -1,25 +1,28 @@
import { useId, useState } from "react";
import { techLogNavigation } from "../../tech-log-navigation.ts";
import { GuardedStudioLink } from "./guarded-studio-link.tsx";
const navigation = [
{
href: "/studio/documents",
label: "작업본",
active: (path: string) =>
/**
* Which entries exist, what they are called and in what order comes from the
* route contract. Only the active-state rule stays here: "작업본" stays
* highlighted across `/studio/documents/*` except on the new-document screen,
* which is presentation behaviour the contract has no opinion about.
*/
const activeRules: Readonly<Record<string, (path: string) => boolean>> =
Object.freeze({
TECH_LOG_STUDIO_DOCUMENTS: (path) =>
path.startsWith("/studio/documents") && path !== "/studio/documents/new",
},
{
href: "/studio/publications",
label: "게시 기록",
active: (path: string) => path.startsWith("/studio/publications"),
},
{
href: "/studio/documents/new",
label: "새 문서",
active: (path: string) => path === "/studio/documents/new",
},
] as const;
TECH_LOG_STUDIO_PUBLICATIONS: (path) =>
path.startsWith("/studio/publications"),
TECH_LOG_STUDIO_DOCUMENT_NEW: (path) => path === "/studio/documents/new",
});
const navigation = techLogNavigation("STUDIO").map((entry) => ({
href: entry.path,
label: entry.label,
active: activeRules[entry.routeId] ?? ((path: string) => path === entry.path),
}));
function StudioNavigation({
currentPath,