feat(view): collapsible 시점 menu, origin toggle, and remembered panel states on the 3D band
The title stays. The view buttons become a menu that shows the current view and closes on a choice, Esc or an outside click. The origin note is an ⓘ toggle. 부재 표시 and 색 기준 are collapsed by default and show their state in the summary (e.g. 부재 표시 7/7 ▾, 색 기준: 역할 ▾); their open state is remembered per viewer, with guarded storage access. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
678215267c
commit
e66c259d1e
3 files changed
+76
-7
No files matched your search
+10
-6
@@ -74,16 +74,20 @@
|
||||
<section class="panel viewer" id="viewer-block" aria-labelledby="viewer-title">
|
||||
<div class="viewer-head">
|
||||
<h2 id="viewer-title">교각 3D 뷰</h2>
|
||||
<div id="views" class="views" role="group" aria-label="시점"></div>
|
||||
<span id="origin" class="muted"></span>
|
||||
<div class="views-menu" id="views-menu">
|
||||
<button id="views-toggle" class="btn views-toggle" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="views">시점</button>
|
||||
<div id="views" class="views views-list" role="group" aria-label="시점" hidden></div>
|
||||
</div>
|
||||
<button id="origin-toggle" class="info-btn" type="button" aria-label="원점 표기" aria-expanded="false" aria-controls="origin">ⓘ</button>
|
||||
<span id="origin" class="origin-pop muted" hidden></span>
|
||||
</div>
|
||||
<div id="canvas" class="canvas" aria-label="교각 3D 뷰"></div>
|
||||
<details class="info-panel parts-info" open>
|
||||
<summary>부재 표시</summary>
|
||||
<details class="info-panel parts-info" id="parts-info">
|
||||
<summary>부재 표시 <span id="parts-count" class="panel-state"></span></summary>
|
||||
<div id="parts"></div>
|
||||
</details>
|
||||
<details class="info-panel color-info" id="color-info" open hidden>
|
||||
<summary>색 기준</summary>
|
||||
<details class="info-panel color-info" id="color-info" hidden>
|
||||
<summary>색 기준 <span id="color-state" class="panel-state"></span></summary>
|
||||
<div class="rebar-color" id="rebar-color" role="group" aria-label="철근 색 기준">
|
||||
<button type="button" data-color="role" aria-pressed="true">역할</button>
|
||||
<button type="button" data-color="diameter" aria-pressed="false">직경</button>
|
||||
|
||||
+55
@@ -244,6 +244,9 @@ function setView(kind: ViewKind): void {
|
||||
document.querySelectorAll<HTMLButtonElement>('#views button').forEach((b, i) => {
|
||||
b.setAttribute('aria-pressed', String(VIEWS[i].kind === kind));
|
||||
});
|
||||
// The band shows the current view only; choosing one closes the menu (승인 2026-10-23).
|
||||
$('views-toggle').textContent = `${VIEWS.find((v) => v.kind === kind)?.label ?? ''} ▾`;
|
||||
setMenuOpen('views', false);
|
||||
if (!state.scene) return;
|
||||
const pier = activePier();
|
||||
if (!pier?.model) return;
|
||||
@@ -445,6 +448,7 @@ function renderDefaults(): void {
|
||||
}
|
||||
|
||||
function renderAll(): void {
|
||||
updatePartsCount();
|
||||
$('status').textContent = state.piers.length ? '' : '엑셀을 불러오면 교각 형상을 생성합니다.';
|
||||
renderHeader();
|
||||
renderPierList();
|
||||
@@ -810,6 +814,8 @@ const REBAR_MEMBERS: { id: RebarRow['member']; label: string }[] = [
|
||||
|
||||
/** 3D colour basis buttons on ③ and their legend (spec §6.7). */
|
||||
function renderRebarColor(): void {
|
||||
const colorLabel: Record<RebarColorMode, string> = { role: '역할', diameter: '직경', member: '부재' };
|
||||
$('color-state').textContent = `${colorLabel[state.rebarColor]} ▾`;
|
||||
// The colour control lives in the 3D window and is shown only on ③.
|
||||
$<HTMLDetailsElement>('color-info').hidden = state.stage !== 'rebar';
|
||||
for (const b of document.querySelectorAll<HTMLButtonElement>('#rebar-color button')) {
|
||||
@@ -1769,3 +1775,52 @@ if (new URLSearchParams(window.location.search).get('sample') === '1') {
|
||||
})
|
||||
.catch((err: Error) => setError(`샘플을 불러오지 못했습니다: ${err.message}`));
|
||||
}
|
||||
|
||||
// --- 3D band: collapsible view menu, origin toggle, and remembered panel states (승인 2026-10-23) ---
|
||||
/** Open or close the view menu or the origin note, and keep the buttons' aria-expanded in step. */
|
||||
function setMenuOpen(which: 'views' | 'origin', open: boolean): void {
|
||||
const panel = $<HTMLElement>(which);
|
||||
panel.hidden = !open;
|
||||
$(`${which}-toggle`).setAttribute('aria-expanded', String(open));
|
||||
}
|
||||
|
||||
/** Counts the bar parts shown on the 3D view in the 부재 표시 summary. */
|
||||
function updatePartsCount(): void {
|
||||
const boxes = document.querySelectorAll<HTMLInputElement>('#parts input[type="checkbox"]');
|
||||
const on = Array.from(boxes).filter((b) => b.checked).length;
|
||||
$('parts-count').textContent = `${on}/${boxes.length} ▾`;
|
||||
}
|
||||
|
||||
/** Panel open states remembered per viewer; the storage can be missing or blocked, so every access is guarded. */
|
||||
const PANEL_KEY = 'pierzainer.panel.';
|
||||
function restorePanel(id: string): void {
|
||||
const panel = $<HTMLDetailsElement>(id);
|
||||
try {
|
||||
panel.open = localStorage.getItem(PANEL_KEY + id) === '1';
|
||||
} catch {
|
||||
panel.open = false;
|
||||
}
|
||||
panel.addEventListener('toggle', () => {
|
||||
try {
|
||||
localStorage.setItem(PANEL_KEY + id, panel.open ? '1' : '0');
|
||||
} catch {
|
||||
/* storage unavailable: the panel still works */
|
||||
}
|
||||
});
|
||||
}
|
||||
restorePanel('parts-info');
|
||||
restorePanel('color-info');
|
||||
|
||||
$<HTMLButtonElement>('views-toggle').addEventListener('click', () => setMenuOpen('views', $<HTMLDivElement>('views').hidden === true));
|
||||
$<HTMLButtonElement>('origin-toggle').addEventListener('click', () => setMenuOpen('origin', $<HTMLSpanElement>('origin').hidden === true));
|
||||
document.addEventListener('click', (ev) => {
|
||||
const target = ev.target as Node;
|
||||
if (!$<HTMLDivElement>('views-menu').contains(target)) setMenuOpen('views', false);
|
||||
if (!$<HTMLButtonElement>('origin-toggle').contains(target) && !$<HTMLSpanElement>('origin').contains(target)) setMenuOpen('origin', false);
|
||||
});
|
||||
document.addEventListener('keydown', (ev) => {
|
||||
if (ev.key === 'Escape') {
|
||||
setMenuOpen('views', false);
|
||||
setMenuOpen('origin', false);
|
||||
}
|
||||
});
|
||||
+11
-1
@@ -782,5 +782,15 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif;
|
||||
|
||||
/* 좁은 창 (601–1100px, 승인 2026-10-23): 상단 띠에서 시점 버튼과 원점 표기를 오른쪽으로 */
|
||||
@media (min-width: 601px) and (max-width: 1100px) {
|
||||
#viewer-block .viewer-head #views { margin-left: auto; }
|
||||
#viewer-block .viewer-head #views-menu { margin-left: auto; }
|
||||
}
|
||||
|
||||
/* 3D band (승인 2026-10-23): view menu, origin popover, collapsed panel summaries */
|
||||
#viewer-block .views-menu { position: relative; margin-left: auto; }
|
||||
#viewer-block .views-toggle { min-height: 44px; font-weight: 700; background: #eef3f8; border: 1px solid #c3cbd4; }
|
||||
#viewer-block #views.views-list { position: absolute; top: 100%; right: 0; z-index: 10; display: flex; flex-direction: column; gap: 0; min-width: 120px; padding: 4px; margin-top: 4px; background: #ffffff; border: 1px solid #c3cbd4; border-radius: 6px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.15); }
|
||||
#viewer-block #views.views-list .btn { min-height: 44px; justify-content: flex-start; text-align: left; border: 0; background: transparent; }
|
||||
#viewer-block #views.views-list .btn[aria-pressed='true'] { background: #1f5fa8; color: #ffffff; }
|
||||
#viewer-block .info-btn { min-width: 44px; min-height: 44px; border: 1px solid #c3cbd4; border-radius: 6px; background: #ffffff; font-size: 16px; cursor: pointer; }
|
||||
#viewer-block .origin-pop { position: absolute; top: 100%; right: 8px; z-index: 10; margin-top: 4px; padding: 6px 10px; background: #ffffff; border: 1px solid #c3cbd4; border-radius: 6px; font-size: 12px; white-space: nowrap; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12); }
|
||||
#viewer-block .panel-state { font-weight: 400; color: #55606e; margin-left: 6px; font-size: 12px; }
|
||||
Reference in new issue
Block a user