test: prove TechLog UI migration parity
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
|
||||
+3
-4
@@ -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;
|
||||
|
||||
@@ -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" });
|
||||
|
||||
Reference in New Issue
Block a user