Files
C.E.L_Slide_test2/templates/phase_z2/slide_base.html
kyeongmin b9747c2f4a
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 21s
feat(#84): IMP-84 u1~u3 silent automation policy enforcement (FramePanel reject confirm + slide_base provisional badge/outline + IMP-30 visual assertions inverted)
- u1 FramePanel.tsx: extract `applyFrameSelection(candidate, onFrameSelect)`
  pure helper; collapse `handleFrameSelect` to direct onFrameSelect for every
  V4 label; drop `window.confirm` reject popup (IMP-47B u11 regression noise
  per `feedback_auto_pipeline_first`). New vitest pin `imp84_framepanel_reject_silent.test.ts`
  covers helper invocation across all 4 V4 labels + source-presence pins.
- u2 templates/phase_z2/slide_base.html: delete `.zone--provisional` CSS,
  `.zone__needs-adaptation-badge` CSS, the zone--provisional class fragment
  in the zone div, and the badge `<span>` render at the provisional zone.
  Preserve `data-provisional="1"` attribute as silent telemetry. New pytest
  `tests/phase_z2/test_imp84_provisional_silent_render.py` pins the silent
  contract independently of the IMP-30 first-render file.
- u3 tests/test_phase_z2_imp30_first_render.py: invert the three IMP-30 u5
  positive provisional-visual assertions to IMP-84 silent-contract negatives
  (no class, no badge, no CSS selectors); preserve positive `data-provisional`
  telemetry assertions. Docstrings updated to IMP-84 silent contract.

Out of scope (Round #4 + #92 contract): Home.tsx `toast.error(aiReviewMsg)`
call line, designAgentApi.ts `api_error_kinds`/`api_error_kind` schema and
operational-only formatter, FramePanel reject badge/tooltip read-only labels
(L102/L147/L156), and backend `zone.provisional` flag emission.

Stage 4 PASS: u1 vitest 10/10, u2 pytest 5/5, u3 pytest 29/29 (incl. 3
IMP-84 inverted assertions: `test_imp84_provisional_zone_silent_no_class_no_badge`,
`test_imp84_provisional_badge_never_rendered_in_mixed_zones`,
`test_imp84_slide_base_css_strips_provisional_visual_selectors`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:15:02 +09:00

410 lines
14 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- Phase Z-2 MVP-1.5b — single slide + Type B layout (top/bottom zones).
원래 Phase Z 설계 복귀: MDX 1 = slide 1, layout preset = zone 분할, frame-derived block ⊂ zone. -->
<!DOCTYPE html>
<html lang="ko"{% if embedded_mode == "embedded" %} class="embedded"{% endif %}>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280">
<title>{{ slide_title }}</title>
{% if embedded_mode == "auto" %}
<script>
(function(){
try {
var params = new URLSearchParams(window.location.search);
if (params.get('embedded') === '1' || window.self !== window.top) {
document.documentElement.classList.add('embedded');
}
} catch (e) {}
})();
</script>
{% endif %}
<style>
/* ── existing tokens (inlined) ── */
{{ token_css | safe }}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
background: #e8ecf0;
display: flex; justify-content: center; align-items: center;
min-height: 100vh;
word-break: keep-all;
padding: 20px 0;
}
/* ── IMP-14 A-4: embedded mode reset (iframe consumer) ──
standalone-only body centering/min-height/padding undone so the .slide
(1280×720) sits at origin without vertical shift or clipping. */
html.embedded body {
background: transparent;
display: block;
min-height: 0;
padding: 0;
}
html.embedded .slide {
box-shadow: none;
}
/* ── 16:9 슬라이드 (single, 1280×720) ── */
.slide {
width: 1280px; height: 720px;
position: relative;
overflow: hidden;
background: #fff;
box-shadow: 0 4px 20px rgba(0,0,0,.15);
flex-shrink: 0;
}
.slide-bg {
position: absolute; inset: 0;
background: linear-gradient(180deg, #f0f0f0 0%, #ffffff 100%);
z-index: 0;
}
.slide-title {
position: absolute;
left: 52px; top: 22px;
width: calc(100% - 104px);
font-weight: 700;
font-size: var(--font-slide-title);
line-height: var(--lh-slide-title);
background-image: linear-gradient(180deg, #296b55 0%, #123328 100%);
-webkit-background-clip: text; background-clip: text;
color: transparent;
text-shadow: 0 0 2px #322c1e;
z-index: 2;
}
.slide-divider {
position: absolute;
left: 50px; top: 58px;
width: calc(100% - 100px);
height: 2px;
background: #cbd5e1;
z-index: 2;
}
.slide-body {
position: absolute;
left: 50px; top: 76px;
width: calc(100% - 100px); /* 1180 */
height: 585px;
z-index: 1;
overflow: hidden;
}
/* ── Layout preset (composition planner v0 — 8 vocabulary) ──
단일 .layout-{preset} 클래스에 grid CSS 주입.
layout_css = pipeline build_layout_css() 결과 :
- horizontal-2 : rows = dynamic px (content_weight + frame_min_height)
- 그 외 : rows / cols = fr default (LAYOUT_PRESETS)
positions (single / top / bottom / left / right / top-left / ... / grid-2x2 9개)
은 .zone 의 inline grid-area 로 지정 — preset CSS 는 areas 만 정의. */
.layout-{{ layout_preset }} {
display: grid;
grid-template-columns: {{ layout_css.cols }};
grid-template-rows: {{ layout_css.rows }};
grid-template-areas: {{ layout_css.areas | safe }};
gap: {{ gap_px }}px;
height: 100%;
}
/* ── Zone — block 가용 공간 최대화. 프레임 자체 styling 이 boundary 역할 ── */
.zone {
position: relative;
overflow: hidden;
padding: 0;
min-width: 0;
min-height: 0;
}
/* IMP-84: provisional zone visual treatment removed (silent-automation
policy). `data-provisional="1"` attribute is still emitted on the
zone div as silent telemetry for downstream selectors / inspection;
no user-visible outline, wash, or badge. */
/* ── Frame-family text layout contract (shared, reusable) ──
feedback-1 (mvp1.5b_test7): visible improvement 강화.
Stronger hanging indent + breathing line spacing + visible hierarchy. */
.text-line {
font-size: var(--font-body); /* 11px */
font-weight: var(--weight-body);
line-height: 1.6; /* breathing room */
margin: 0;
}
.text-line + .text-line {
margin-top: 4px; /* visible gap between lines (was 2px) */
}
/* bullet line — hanging indent via padding + absolute marker (안정적) */
.text-line--bullet {
padding-left: 14px;
position: relative;
}
.text-line--bullet::before {
content: "•"; /* visible bullet 마커 */
position: absolute;
left: 2px;
top: 0;
font-weight: 700;
line-height: inherit;
color: inherit;
}
/* indent levels — visible nested hierarchy */
.text-line--indent-1 {
padding-left: 30px;
}
.text-line--indent-1::before {
content: "◦"; /* sub-bullet 다른 마커 */
left: 18px;
}
.text-line--indent-2 {
padding-left: 46px;
}
.text-line--indent-2::before {
content: "▪";
left: 34px;
}
/* body line — 마커 없음 */
.text-line--body {
padding-left: 0;
}
.text-line strong { color: #000; font-weight: 700; }
/* ── Transform-block family (frame 29 AS-IS/TO-BE dedicated comparison component) ──
feedback-3: bullet rows 가 아닌 *전용 비교 component*. Header (AS-IS / TO-BE) + paired rows.
color-coded cells, left-border accent → 단번에 *비교 구조* 인지. */
.transform-block {
margin-top: 4px;
padding: 6px 8px 6px;
background: rgba(255, 255, 255, 0.4);
border-radius: 3px;
border: 1px solid rgba(0,0,0,0.05);
}
.transform-block__header {
display: grid;
grid-template-columns: 1fr 22px 1fr;
gap: 6px;
margin-bottom: 4px;
padding-bottom: 3px;
border-bottom: 1px dashed rgba(0,0,0,0.12);
font-size: 9px;
font-weight: 900;
letter-spacing: 0.5px;
}
.transform-block__label--from { color: #6b5444; }
.transform-block__label--to { color: #2563eb; text-align: left; }
.transform-rows {
display: flex; flex-direction: column;
gap: 4px;
}
.transform-row {
display: grid;
grid-template-columns: 1fr 22px 1fr;
gap: 6px;
align-items: stretch;
font-size: var(--font-body);
line-height: 1.45;
}
.transform-row__from {
padding: 3px 8px;
background: linear-gradient(90deg, rgba(140,120,90,0.16), rgba(140,120,90,0.08));
border-left: 2px solid #b8a98a;
border-radius: 2px;
color: #4b3f30;
font-weight: 500;
}
.transform-row__arrow {
text-align: center;
color: #2563eb;
font-weight: 900;
font-size: 14px;
align-self: center;
}
.transform-row__to {
padding: 3px 8px;
background: linear-gradient(90deg, rgba(37,99,235,0.10), rgba(37,99,235,0.18));
border-left: 2px solid #2563eb;
border-radius: 2px;
color: #1e40af;
font-weight: 700;
}
/* ── Footer pill — feedback-3: body 와 weight balance, 살짝 가벼워짐 ── */
.slide-footer {
position: absolute;
left: 50px; bottom: 8px;
width: calc(100% - 100px); /* front 기준 (body 와 정렬) */
height: 41px; /* front 기준 */
border-radius: 999px;
overflow: hidden;
z-index: 2;
display: flex; align-items: center; justify-content: center;
background: linear-gradient(90deg, #3b3523 5%, #263a2a 50%, #113f31 95%);
opacity: 0.92; /* slightly lighter */
}
.slide-footer-text {
position: relative; z-index: 1;
font-size: 15px; /* was 20px (--font-footer) */
font-weight: 700;
line-height: 1.2;
color: #fff;
text-align: center;
text-shadow: 0 0 3px rgba(0,0,0,.4);
letter-spacing: -0.01em;
}
.slide-footer-text em { color: #fe3; font-style: normal; }
.phase-z2-marker {
position: absolute;
top: 4px; right: 8px;
font-size: 9px; color: #94a3b8;
font-family: monospace;
z-index: 3;
}
/* ── IMP-35 u8 : popup details/summary (Step 17 POPUP gate escalation) ──
When the Step 17 POPUP gate escalates a unit (zone.has_popup=True),
slide_base renders a JS-free <details>/<summary> wrapper in the zone.
The body of the frame stays as zone.partial_html (the FIT-version of
content); the popup body holds the FULL original raw_content (MDX 원문
무손실 보존 — 오답노트 #5 / IMPROVEMENT-REDESIGN.md §3.6 line 110).
Placement (default top-right) is read from
zone.popup_binding.detail_trigger.placement
(templates/phase_z2/regions/display_strategies.yaml). HTML-native
<details> per CLAUDE.md 자세히보기 contract — no JavaScript. */
.zone__popup-details {
position: absolute;
z-index: 5;
font-family: 'Pretendard', sans-serif;
}
.zone__popup-details--top-right {
top: 4px;
right: 4px;
}
.zone__popup-details--top-left {
top: 4px;
left: 4px;
}
.zone__popup-details--bottom-right {
bottom: 4px;
right: 4px;
}
.zone__popup-details--bottom-left {
bottom: 4px;
left: 4px;
}
.zone__popup-summary {
list-style: none;
cursor: pointer;
padding: 2px 6px;
background: rgba(30, 41, 59, 0.85);
color: #fff;
border-radius: 2px;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.04em;
line-height: 1.2;
user-select: none;
}
.zone__popup-summary::-webkit-details-marker { display: none; }
.zone__popup-summary::marker { content: ""; }
.zone__popup-body {
position: absolute;
top: 22px;
right: 0;
width: 360px;
max-height: 280px;
overflow: auto;
padding: 8px 10px;
background: #fff;
border: 1px solid var(--color-border, #e2e8f0);
border-radius: 3px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
white-space: pre-wrap;
word-break: keep-all;
font-size: 10px;
line-height: 1.5;
color: #1e293b;
}
/* ── IMP-90 u17 : print mode (Step 22 user-edit + Export).
Companion JS at body end opens popups so FULL raw_content prints. */
@media print {
@page { size: 1280px 720px; margin: 0; }
html, body {
background: #fff !important;
padding: 0 !important;
margin: 0 !important;
min-height: 0 !important;
display: block !important;
}
.slide {
box-shadow: none !important;
page-break-inside: avoid;
break-inside: avoid;
}
.zone__popup-summary { display: none !important; }
.zone__popup-details,
.zone__popup-details[open] { position: static !important; }
.zone__popup-body {
position: static !important;
top: auto !important;
right: auto !important;
max-height: none !important;
overflow: visible !important;
box-shadow: none !important;
border: none !important;
width: auto !important;
padding: 6px 0 0 0 !important;
}
}
</style>
</head>
<body>
<div class="slide" data-page="1">
<div class="slide-bg"></div>
<div class="phase-z2-marker">phase_z2 / mvp-1.5b / {{ layout_preset }} / single slide</div>
<div class="slide-title">{{ slide_title }}</div>
<div class="slide-divider"></div>
<div class="slide-body">
<div class="layout-{{ layout_preset }}">
{% for zone in zones %}
<div class="zone" data-zone-position="{{ zone.position }}" data-template-id="{{ zone.template_id }}"{% if zone.provisional %} data-provisional="1"{% endif %}{% if zone.has_popup %} data-has-popup="1"{% endif %} style="grid-area: {{ zone.position }};">
{{ zone.partial_html | safe }}
{% if zone.has_popup %}
{% set _popup_trigger = (zone.popup_binding.detail_trigger if zone.popup_binding else None) or {} %}
{% set _popup_placement = _popup_trigger.placement or 'top-right' %}
{% set _popup_label = _popup_trigger.label or 'details' %}
{% set _popup_strategy = (zone.popup_binding.display_strategy if zone.popup_binding else 'inline_preview_with_details') %}
<details class="zone__popup-details zone__popup-details--{{ _popup_placement }}" data-display-strategy="{{ _popup_strategy }}" data-popup-placement="{{ _popup_placement }}">
<summary class="zone__popup-summary">{{ _popup_label }}</summary>
<div class="zone__popup-body">{{ zone.popup_html }}</div>
</details>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% if slide_footer %}
<div class="slide-footer">
<div class="slide-footer-text">{{ slide_footer | safe }}</div>
</div>
{% endif %}
</div>
<script>
// IMP-90 u17 — beforeprint popup auto-expand (CLAUDE.md 자세히보기 contract).
// Body-level handler (outside any per-zone popup block) so the popup-render
// JS-free invariant (IMP-35 u8) is preserved on the per-zone path.
window.addEventListener('beforeprint', function () {
document.querySelectorAll('details').forEach(function (d) {
d.dataset.imp90PrintRestore = d.open ? '1' : '0';
d.open = true;
});
});
window.addEventListener('afterprint', function () {
document.querySelectorAll('details').forEach(function (d) {
if (d.dataset.imp90PrintRestore === '0') d.open = false;
delete d.dataset.imp90PrintRestore;
});
});
</script>
</body>
</html>