test: prove TechLog UI migration parity

This commit is contained in:
DongHyeonka
2026-08-16 02:48:26 +09:00
parent c5c8b9423c
commit 6c2780b7a7
176 changed files with 2393 additions and 616 deletions
-1
View File
@@ -2,7 +2,6 @@ import { createRoot } from "react-dom/client";
import { recordBootFailure } from "../adapters/diagnostics/bounded-diagnostics.ts";
import { BootErrorShell } from "../presentation/boundaries/boot-error-shell.tsx";
import "../presentation/styles/theme.css";
import "pretendard/dist/web/variable/pretendardvariable.css";
import "@fontsource/ibm-plex-mono/400.css";
import "@fontsource/ibm-plex-mono/500.css";
@@ -13,7 +13,9 @@ const navigation = [
export function SiteHeader({ currentPath }: { currentPath?: string }) {
const location = useLocation();
const activePath = currentPath ?? location.pathname;
const activePath =
currentPath ??
(location.pathname.startsWith("/topics/") ? "/explore" : location.pathname);
const menuRef = useRef<HTMLDetailsElement>(null);
const summaryRef = useRef<HTMLElement>(null);
@@ -1,53 +1,34 @@
import type { CSSProperties } from "react";
const styles = {
error: {
fontFamily:
'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
height: "100vh",
textAlign: "center",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
},
desc: {
display: "inline-block",
},
h1: {
display: "inline-block",
margin: "0 20px 0 0",
padding: "0 23px 0 0",
fontSize: 24,
fontWeight: 500,
verticalAlign: "top",
lineHeight: "49px",
},
h2: {
fontSize: 14,
fontWeight: 400,
lineHeight: "49px",
margin: 0,
},
} as const satisfies Record<string, CSSProperties>;
import { useSourceCompatiblePublicNotFound } from "../public-not-found-layout.tsx";
export function PublicNotFoundPage() {
useSourceCompatiblePublicNotFound();
return (
<>
<title>404: This page could not be found.</title>
<div style={styles.error}>
<div>
<style>{
"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"
}</style>
<h1 className="next-error-h1" style={styles.h1}>
404
</h1>
<div style={styles.desc}>
<h2 style={styles.h2}>This page could not be found.</h2>
</div>
</div>
</div>
<style>{`
html { background: white; color: black; }
body {
min-width: 0;
margin: 8px;
overflow: visible;
background: transparent;
color: black;
font-family: "Times New Roman";
font-size: 16px;
line-height: normal;
text-rendering: auto;
word-break: normal;
}
pre.tech-log-plain-not-found {
margin: 13px 0;
color: black;
font-family: monospace;
font-size: 13px;
line-height: normal;
white-space: pre-wrap;
overflow-wrap: break-word;
}
`}</style>
<pre className="tech-log-plain-not-found">Not Found</pre>
</>
);
}
@@ -0,0 +1,18 @@
import {
createContext,
useContext,
useLayoutEffect,
} from "react";
type SetNestedNotFound = (active: boolean) => void;
export const PublicNotFoundLayoutContext =
createContext<SetNestedNotFound | null>(null);
export function useSourceCompatiblePublicNotFound(): void {
const setNestedNotFound = useContext(PublicNotFoundLayoutContext);
useLayoutEffect(() => {
setNestedNotFound?.(true);
return () => setNestedNotFound?.(false);
}, [setNestedNotFound]);
}
@@ -1,8 +1,9 @@
import type { ReactNode } from "react";
import { type ReactNode, useCallback, useState } from "react";
import { Link } from "react-router-dom";
import { publicSiteConfig } from "../../contracts/public-site-config.ts";
import { SiteHeader } from "./components/site-header.tsx";
import { PublicNotFoundLayoutContext } from "./public-not-found-layout.tsx";
type PublicShellProps = {
children: ReactNode;
@@ -10,29 +11,37 @@ type PublicShellProps = {
};
export function PublicShell({ children, currentPath }: PublicShellProps) {
const [nestedNotFound, setNestedNotFound] = useState(false);
const updateNestedNotFound = useCallback((active: boolean) => {
setNestedNotFound(active);
}, []);
return (
<div className="site-frame">
<a className="skip-link" href="#main-content">
</a>
<SiteHeader currentPath={currentPath} />
<PublicNotFoundLayoutContext.Provider value={updateNestedNotFound}>
{nestedNotFound ? children : (
<div className="site-frame">
<a className="skip-link" href="#main-content">
</a>
<SiteHeader currentPath={currentPath} />
{children}
{children}
<footer className="site-footer">
<div className="shell footer-inner">
<div>
<p className="footer-name">{publicSiteConfig.operator}</p>
<p>{publicSiteConfig.identityStatement}</p>
</div>
<div className="footer-links">
<Link to={publicSiteConfig.contactPath}>
{publicSiteConfig.contactLabel}
</Link>
<Link to={publicSiteConfig.latestRelease}> Release</Link>
</div>
<footer className="site-footer">
<div className="shell footer-inner">
<div>
<p className="footer-name">{publicSiteConfig.operator}</p>
<p>{publicSiteConfig.identityStatement}</p>
</div>
<div className="footer-links">
<Link to={publicSiteConfig.contactPath}>
{publicSiteConfig.contactLabel}
</Link>
<Link to={publicSiteConfig.latestRelease}> Release</Link>
</div>
</div>
</footer>
</div>
</footer>
</div>
)}
</PublicNotFoundLayoutContext.Provider>
);
}
@@ -3,6 +3,7 @@ import type { WorkingCopyInput } from "../../../contracts/studio/contract.ts";
import { ContentFormatError } from "../../../domain/content-format/parse-case-content.ts";
import { projectWorkingCopy } from "../../../domain/content-format/project-public-render-model.ts";
import { PublicRecordRenderer } from "../../shared/public-render/public-record-renderer.tsx";
import { useStudio } from "../use-studio.ts";
type CatalogEntry = components["schemas"]["CatalogEntry"];
@@ -24,6 +25,7 @@ function resolvePreviewEvidenceAsset(key: string) {
}
export function InstantPreview({ draft, catalog }: { draft: WorkingCopyInput; catalog: CatalogEntry[] }) {
const studio = useStudio();
let model: ReturnType<typeof projectWorkingCopy> | null = null;
let issues: string[] | null = null;
try {
@@ -45,7 +47,7 @@ export function InstantPreview({ draft, catalog }: { draft: WorkingCopyInput; ca
model={model!}
embedded
resolveEvidenceAsset={resolvePreviewEvidenceAsset}
resolvePublishedLabel={() => undefined}
resolvePublishedLabel={studio.resolvePublishedLabel}
/>
</div>
);
@@ -288,7 +288,7 @@ export function PublicPreviewScreen({ documentId }: { documentId: string }) {
model={previewDetail.preview.renderModel}
embedded
resolveEvidenceAsset={resolvePreviewEvidenceAsset}
resolvePublishedLabel={() => undefined}
resolvePublishedLabel={studio.resolvePublishedLabel}
/>
</div>
</>
@@ -1,5 +1,3 @@
/* eslint-disable react-hooks/set-state-in-effect -- a changed event or retry intentionally enters a fresh loading state. */
import { useEffect, useState } from "react";
import {
@@ -50,7 +48,8 @@ export function PublicationEventPreviewScreen({
publicationEventId: string;
classes?: PublicationFlowClasses;
}) {
const { gateway } = useStudio();
const studio = useStudio();
const { gateway } = studio;
const [retry, setRetry] = useState(0);
const [state, setState] = useState<
| { status: "LOADING" }
@@ -168,7 +167,7 @@ export function PublicationEventPreviewScreen({
model={renderModel}
embedded
resolveEvidenceAsset={resolveEvidenceAsset}
resolvePublishedLabel={() => undefined}
resolvePublishedLabel={studio.resolvePublishedLabel}
/>
</div>
</div>
@@ -1,4 +1,6 @@
export const defaultPublicationFlowClasses = {
import styles from "../../styles/publication-flow.module.css";
const publicationFlowClassContract = {
page: "studio-publication-page",
snapshotPage: "studio-snapshot-page",
heading: "studio-publication-heading",
@@ -35,4 +37,11 @@ export const defaultPublicationFlowClasses = {
snapshotDocument: "studio-publication-snapshot-document",
} as const;
export type PublicationFlowClasses = Record<keyof typeof defaultPublicationFlowClasses, string>;
export type PublicationFlowClasses = Record<
keyof typeof publicationFlowClassContract,
string
>;
export const defaultPublicationFlowClasses = Object.fromEntries(
Object.keys(publicationFlowClassContract).map((key) => [key, styles[key]]),
) as PublicationFlowClasses;
@@ -1,5 +1,3 @@
/* eslint-disable react-hooks/set-state-in-effect -- a changed history query intentionally enters a fresh loading state. */
import { useEffect, useRef, useState, type FormEvent } from "react";
import {
@@ -1,5 +1,3 @@
/* eslint-disable react-hooks/set-state-in-effect -- a changed document or retry intentionally enters a fresh loading state. */
import { useEffect, useMemo, useState } from "react";
import {
@@ -8,6 +8,7 @@ import {
import { isStudioGatewayError } from "../../application/ports/studio-gateway-error.ts";
import type { StudioGateway } from "../../application/ports/studio-gateway.ts";
import type { ResolvePublishedLabel } from "../../domain/public-render-content.ts";
import type {
WorkingCopy,
WorkingCopyInput,
@@ -24,6 +25,7 @@ import { useBeforeUnload } from "./components/use-before-unload.ts";
type StudioProviderProps = Readonly<{
children: ReactNode;
createGateway: () => StudioGateway;
resolvePublishedLabel?: ResolvePublishedLabel;
now?: () => Date;
navigate?: (href: string) => void;
}>;
@@ -41,9 +43,12 @@ function defaultNavigate(href: string): void {
window.dispatchEvent(new PopStateEvent("popstate"));
}
const missingPublishedLabel: ResolvePublishedLabel = () => undefined;
export function StudioProvider({
children,
createGateway,
resolvePublishedLabel = missingPublishedLabel,
now = () => new Date("2026-08-14T01:00:00.000Z"),
navigate = defaultNavigate,
}: StudioProviderProps) {
@@ -143,6 +148,7 @@ export function StudioProvider({
const value = useMemo<StudioContextValue>(
() => ({
gateway,
resolvePublishedLabel,
now,
editor,
requestAnnouncement,
@@ -161,6 +167,7 @@ export function StudioProvider({
navigateInternal,
now,
requestAnnouncement,
resolvePublishedLabel,
setEditorStatus,
updateEditorDraft,
],
@@ -47,6 +47,14 @@ export function StudioShell({ children }: StudioShellProps) {
() => application.features.get(TECH_LOG_FEATURE_ID).createStudioGateway(),
[application],
);
const resolvePublishedLabel = useCallback(
(path: string) =>
application.features
.get(TECH_LOG_FEATURE_ID)
.publicContent.listRecords()
.find((record) => record.path === path)?.publishedLabel,
[application],
);
useEffect(() => {
const resetPersistedSession = (event: PageTransitionEvent) => {
@@ -61,6 +69,7 @@ export function StudioShell({ children }: StudioShellProps) {
<StudioProvider
key={generation}
createGateway={createGateway}
resolvePublishedLabel={resolvePublishedLabel}
navigate={navigateInternal}
>
<StudioFrame>{children}</StudioFrame>
@@ -1,6 +1,7 @@
import { createContext, useContext, useMemo } from "react";
import type { StudioGateway } from "../../application/ports/studio-gateway.ts";
import type { ResolvePublishedLabel } from "../../domain/public-render-content.ts";
import type {
WorkingCopy,
WorkingCopyInput,
@@ -17,6 +18,7 @@ export type StudioEditorState = Readonly<{
export type StudioContextValue = Readonly<{
gateway: StudioGateway;
resolvePublishedLabel: ResolvePublishedLabel;
now(): Date;
editor: StudioEditorState | null;
requestAnnouncement: string;
@@ -1,3 +1,4 @@
@import "tailwindcss";
:root {
--canvas: #f7f7f5;
-2
View File
@@ -119,8 +119,6 @@ function RouteLifecycle({
title: resolve(`route.${definition.routeId}.title`),
appName: message("common.appName"),
});
const main = document.getElementById("main-content");
main?.focus({ preventScroll: true });
try {
if (!navigator.userAgent.toLowerCase().includes("jsdom")) {
window.scrollTo({ top: 0, left: 0, behavior: "auto" });