feat: port TechLog shells and styles

This commit is contained in:
DongHyeonka
2026-08-15 21:20:47 +09:00
parent 01316763e3
commit 627df884dd
14 changed files with 4124 additions and 8 deletions
@@ -0,0 +1,119 @@
import { useId, useRef, useState } from "react";
import { Link } from "react-router-dom";
import { TECH_LOG_FEATURE_ID } from "../../../application/tech-log-feature-input.ts";
import { useApplication } from "../../../../../presentation/providers/application-provider.tsx";
type SearchDialogProps = {
className?: string;
onBeforeOpen?: () => void;
};
export function SearchDialog({
className = "",
onBeforeOpen,
}: SearchDialogProps) {
const dialogId = useId();
const dialogRef = useRef<HTMLDialogElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const [query, setQuery] = useState("");
const normalizedQuery = query.trim().toLocaleLowerCase("ko-KR");
const { publicContent } = useApplication().features.get(TECH_LOG_FEATURE_ID);
const results = publicContent.searchPublicContent(normalizedQuery);
function open() {
onBeforeOpen?.();
dialogRef.current?.showModal();
requestAnimationFrame(() => inputRef.current?.focus());
}
function close() {
dialogRef.current?.close();
}
return (
<>
<button
ref={triggerRef}
className={className}
type="button"
aria-haspopup="dialog"
aria-controls={dialogId}
aria-label="TechLog 검색 열기"
onClick={open}
>
</button>
<dialog
ref={dialogRef}
className="search-dialog"
id={dialogId}
aria-labelledby={dialogId + "-title"}
onClose={() => triggerRef.current?.focus()}
onKeyDown={(event) => {
if (event.key === "Escape") {
event.preventDefault();
close();
}
}}
onClick={(event) => {
if (event.target === event.currentTarget) close();
}}
>
<div className="search-dialog-inner">
<header>
<div>
<p className="context-label">Public records</p>
<h2 id={dialogId + "-title"}>TechLog </h2>
</div>
<button type="button" className="dialog-close" onClick={close}>
</button>
</header>
<label className="search-field">
<span className="visually-hidden"></span>
<span aria-hidden="true"></span>
<input
ref={inputRef}
type="search"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="제목, 요약, 주제, 프로젝트 검색"
autoComplete="off"
/>
</label>
<p className="search-count" aria-live="polite">
{results.length}
</p>
{results.length > 0 ? (
<>
<ul className="search-results">
{results.map((entry) => (
<li key={entry.path}>
<Link to={entry.path} onClick={close}>
<span>{entry.contentType}</span>
<strong>{entry.title}</strong>
<p>{entry.summary}</p>
</Link>
</li>
))}
</ul>
{normalizedQuery ? (
<Link
className="search-all-link"
to={`/search?q=${encodeURIComponent(query.trim())}`}
onClick={close}
>
<span aria-hidden="true"></span>
</Link>
) : null}
</>
) : (
<p className="search-empty"> .</p>
)}
</div>
</dialog>
</>
);
}