feat(#74): IMP-45 u1~u8 slide-level CSS override (frontmatter slide_overrides.css + --override-slide-css/--slide-css-file + idempotent Step 13 injector)
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 22s

u1 KNOWN_AXES tuple gains slide_css entry in src/user_overrides_io.py
(snake_case parity with image_overrides); round-trip test extends
to 6 axes.
u2 src/mdx_normalizer.py surfaces nested slide_overrides.css from the
MDX frontmatter into the normalize_mdx_content return dict; absent
key -> {}, non-string css drops. 4 unit cases in tests/test_mdx_normalizer.py
(present / absent / non-string / title-only).
u3 src/slide_css_injector.py NEW (88 lines) mirrors the
inject_image_overrides_style contract from src/image_id_stamper.py:
marker pair <!--IMP45-SLIDE-CSS:OPEN--> / <!--IMP45-SLIDE-CSS:CLOSE-->,
idempotent re-injection, </head> > <body> > document-start three-tier
fallback, empty/None -> unchanged. 8 fixtures in
tests/test_slide_css_injector.py mirror test_image_id_stamper.py.
u4 run_phase_z2_mvp1 accepts override_slide_css: Optional[str] = None;
None -> frontmatter slide_overrides.css fallback. Step 13 calls
inject_slide_css after image override injection and before the
final.html disk write, so CLI/CI/regression renders observe the same
backend artifact.
u5 argparse adds mutually-exclusive --override-slide-css TEXT (inline
CSS, <style> wrapper optional) and --slide-css-file PATH (UTF-8 read,
fail-closed sys.exit(2) on missing path / decode error / both flags
present). Resolved string is forwarded as override_slide_css kwarg.
6 cases in tests/test_phase_z2_cli_overrides.py (inline / file / both
/ missing / non-utf8 / neither).
u6 samples/mdx_batch/04.mdx frontmatter gains slide_overrides.css
block (verbatim of the former MDX04_DEFAULT_OVERRIDE_CSS constant,
no sample/frame gate). Subprocess smoke in
tests/test_phase_z2_slide_css_smoke.py verifies the marker pair and
CSS substring land in final.html.
u7 Front/client removes the sample/frame-gated frontend-only injection:
Home.tsx drops the MDX04_DEFAULT_OVERRIDE_CSS constant and the
sample==="04"+frame==="process_product_two_way" branch (-28 lines);
SlideCanvas.tsx drops the iframe contentDocument.head injection of
that prop (-14 lines). Live preview now reads backend final.html only.
u8 tests/regression/fixtures/89a_pre_baseline_sha.json 04.mdx entry
resyncs to the live SHA ddb6bf2f... / 28042 bytes (overwrites the
earlier 5-byte-drift d02c76fd... / 28047). Other entries untouched.
Note: 01.mdx baseline drift (ad6f16a3... / 29089 -> live f26a7fac...
/ 29084) predates this branch and is split to a follow-up issue per
the closed-issue fresh validation rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 03:26:03 +09:00
parent b4be6c1cd0
commit 9062931863
14 changed files with 740 additions and 55 deletions

View File

@@ -36,10 +36,6 @@ interface SlideCanvasProps {
userSelection: UserSelection;
/** Phase Z 가 만든 final.html URL (iframe 으로 표시). */
finalHtmlUrl?: string;
/** 슬라이드 단위 inline CSS override (catalog/template 무변, iframe contentDocument 에
* 동적 inject). Home 이 mdx 별 default visual 보완 등을 지정. 빈 문자열/undefined =
* inject 안 함. 사용자 lock 2026-05-14 — slide-level only. */
slideOverrideCss?: string;
/** 파이프라인 실행 중 표시 (loading state). */
isPipelineRunning?: boolean;
/** Phase 2 : pending layout 모드 — final.html iframe 숨기고 빈 layout zone 만 표시. */
@@ -86,7 +82,6 @@ export default function SlideCanvas({
slidePlan,
userSelection,
finalHtmlUrl,
slideOverrideCss,
isPipelineRunning,
isPendingLayout,
pendingLayoutId,
@@ -391,15 +386,6 @@ export default function SlideCanvas({
const doc = (e.currentTarget as HTMLIFrameElement).contentDocument;
if (!doc) return;
// 2026-05-14 — slide-level override CSS (catalog/template 무변).
// Home 이 mdx 별 default visual 보완 (bullet 간격 / zone 비율 등) 지정.
if (slideOverrideCss && slideOverrideCss.trim()) {
const overrideStyle = doc.createElement("style");
overrideStyle.setAttribute("data-purpose", "slide-level-override");
overrideStyle.textContent = slideOverrideCss;
doc.head.appendChild(overrideStyle);
}
// ── Zone DOM 측정 ──
// backend final.html 의 .zone[data-zone-position="..."] 요소를
// 찾아서 boundingClientRect 측정 → 1280×720 기준 정규화.

View File

@@ -242,22 +242,6 @@ export default function Home() {
// 호출되는 단일 callback. handleFileUpload 가 자동 분석 trigger.
const [selectedSample, setSelectedSample] = useState<"03" | "04" | "05" | null>(null);
// 2026-05-14 — mdx 별 slide-level CSS override (catalog/template 무변, frontend layer only).
// SlideCanvas 의 iframe onLoad 에서 동적 inject. 사용자 룰 : "보고용 슬라이드 결과물 단위"
// 변경. mdx04 의 default (rank 1 = process_product_two_way) 일 때만 적용 — 사용자 frame
// override 후 (rank 2 = bim_dx_comparison_table 등) 다른 frame 시 무적용.
const MDX04_DEFAULT_OVERRIDE_CSS = `
.slide-body {
grid-template-rows: 0.38fr 0.60fr !important;
gap: 1.5% !important;
}
.f29b__cell .text-line + .text-line { margin-top: 1px !important; }
.f29b__cell:nth-child(n+3) {
padding-top: 3px !important;
margin-top: 2px !important;
}
`.trim();
const handleSelectSample = useCallback(async (which: "03" | "04" | "05") => {
try {
const res = await fetch(`/api/sample-mdx?mdx=${encodeURIComponent(which)}`);
@@ -634,17 +618,6 @@ export default function Home() {
return state.slidePlan;
}, [pendingZones, state.slidePlan, pendingLayout]);
// 2026-05-14 — slide-level CSS override 계산. mdx04 default (rank 1 = process_product_two_way)
// 일 때만 적용 (catalog 무변, slide 결과물에만 inject). 사용자 frame override 후 다른
// frame 시 무적용 (rank 2 의 frame visual 유지).
const slideOverrideCss = useMemo<string | undefined>(() => {
if (selectedSample !== "04") return undefined;
const zone04_2 = state.slidePlan?.zones.find((z) => z.zone_id === "bottom");
const frameId = zone04_2?.internal_regions[0]?.frame_match_strategy.frame_id;
if (frameId !== "process_product_two_way") return undefined;
return MDX04_DEFAULT_OVERRIDE_CSS;
}, [selectedSample, state.slidePlan]);
// ── Frame 선택 ──
const handleFrameSelect = useCallback((frameId: string) => {
const zone = getSelectedZone(effectiveSlidePlan, state.userSelection);
@@ -805,7 +778,6 @@ export default function Home() {
normalizedContent={state.normalizedContent}
userSelection={state.userSelection}
finalHtmlUrl={runMeta?.final_html_url}
slideOverrideCss={slideOverrideCss}
isPipelineRunning={state.isLoading}
isPendingLayout={!!pendingLayout}
pendingLayoutId={pendingLayout}