feat(device): 기기 알아채기 + 공통 틀 — <html> data-size · pointer · input · touch 표시 · ☰ 서랍(위 메뉴 · 언어 · 테마 · 로그인 · 터치 모드 스위치) · 좁은 폭 좌측 패널 서랍 + 덮개 · 진행 판 바텀시트 · 오른쪽 패널 아래로 쌓기 · 휴대폰 팝업 전체 화면(.ui-popup) · 표 첫 열 고정 · 터치 누를 자리 44px · 100vh → 100dvh · 호버 정보 톡 (기기대응 2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
+185
-18
@@ -11,6 +11,7 @@ import { ui_locales, currentLanguageIndex, setLanguage } from "@ui/ui_template_l
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { THEME_STORAGE_KEY, LANG_STORAGE_KEY, ROUTES } from "@config/config_frontend";
|
||||
import { navigateTo, isAuthenticated } from "./router";
|
||||
import { readTouchMode, setTouchMode, type TouchMode } from "./device_context";
|
||||
|
||||
const BRAND_ICON_URL = new URL("../resources/app_branding/prog_icon.jpg", import.meta.url).href;
|
||||
|
||||
@@ -82,21 +83,7 @@ function buildHeader(): HTMLElement {
|
||||
// 네비게이션 링크
|
||||
const nav = document.createElement("nav");
|
||||
nav.className = "app-nav";
|
||||
const navItems: [keyof typeof ui_locales, string][] = [
|
||||
["App_Nav_Program", ROUTES.A02_PROG_DETAIL],
|
||||
["App_Nav_Company", ROUTES.A03_COMP_DETAIL],
|
||||
["App_Nav_News", ROUTES.A04_NEWS_HISTORY],
|
||||
["App_Nav_Education", ROUTES.A05_EDU_DETAIL],
|
||||
["App_Nav_Support", ROUTES.A08_SUPPORT],
|
||||
];
|
||||
for (const [labelKey, route] of navItems) {
|
||||
const link = createButton({
|
||||
label: L(labelKey),
|
||||
variant: "pill",
|
||||
onClick: () => navigateTo(route as never),
|
||||
});
|
||||
nav.append(link);
|
||||
}
|
||||
nav.append(...buildNavButtons());
|
||||
|
||||
// 우측 액션 (언어/테마/로그인)
|
||||
const actions = document.createElement("div");
|
||||
@@ -120,10 +107,118 @@ function buildHeader(): HTMLElement {
|
||||
actions.append(authSlot);
|
||||
void fillAuthActions(authSlot);
|
||||
|
||||
header.append(brand, nav, actions);
|
||||
header.append(brand, nav, actions, ...buildDrawer(header));
|
||||
return header;
|
||||
}
|
||||
|
||||
const NAV_ITEMS: [keyof typeof ui_locales, string][] = [
|
||||
["App_Nav_Program", ROUTES.A02_PROG_DETAIL],
|
||||
["App_Nav_Company", ROUTES.A03_COMP_DETAIL],
|
||||
["App_Nav_News", ROUTES.A04_NEWS_HISTORY],
|
||||
["App_Nav_Education", ROUTES.A05_EDU_DETAIL],
|
||||
["App_Nav_Support", ROUTES.A08_SUPPORT],
|
||||
];
|
||||
|
||||
function buildNavButtons(): HTMLElement[] {
|
||||
return NAV_ITEMS.map(([labelKey, route]) =>
|
||||
createButton({
|
||||
label: L(labelKey),
|
||||
variant: "pill",
|
||||
onClick: () => navigateTo(route as never),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* ☰ 서랍(PLAN_기기대응 2-2) — 폭 900 이하(위 메뉴가 숨는 폭) · 터치 모드에서 보임.
|
||||
* 위 메뉴 · 언어 · 테마 · 로그인 단추와 같은 항목 + 터치 모드 스위치. 데스크탑 마우스엔 안 보임.
|
||||
* -------------------------------------------------------------------------- */
|
||||
function buildDrawer(header: HTMLElement): HTMLElement[] {
|
||||
const burger = document.createElement("button");
|
||||
burger.type = "button";
|
||||
burger.className = "app-burger";
|
||||
burger.textContent = "☰";
|
||||
burger.setAttribute("aria-label", L("App_Menu"));
|
||||
burger.setAttribute("aria-expanded", "false");
|
||||
const drawer = document.createElement("div");
|
||||
drawer.className = "app-drawer";
|
||||
drawer.setAttribute("role", "dialog");
|
||||
drawer.setAttribute("aria-label", L("App_Menu"));
|
||||
const scrim = document.createElement("div");
|
||||
scrim.className = "app-drawer__scrim";
|
||||
const setOpen = (open: boolean): void => {
|
||||
header.classList.toggle("is-drawer-open", open);
|
||||
burger.setAttribute("aria-expanded", String(open));
|
||||
};
|
||||
burger.addEventListener("click", () => setOpen(!header.classList.contains("is-drawer-open")));
|
||||
scrim.addEventListener("click", () => setOpen(false));
|
||||
drawer.addEventListener("keydown", (e) => e.key === "Escape" && setOpen(false));
|
||||
// 메뉴 · 로그인 항목을 누르면 닫힘(언어 · 테마 · 스위치는 그대로 둠)
|
||||
drawer.addEventListener("click", (e) => {
|
||||
if ((e.target as Element).closest(".app-drawer__nav, .app-drawer__auth")) setOpen(false);
|
||||
});
|
||||
|
||||
const navList = document.createElement("nav");
|
||||
navList.className = "app-drawer__nav";
|
||||
navList.append(...buildNavButtons());
|
||||
const prefs = document.createElement("div");
|
||||
prefs.className = "app-drawer__row";
|
||||
prefs.append(
|
||||
createButton({
|
||||
label: currentLanguageIndex === 0 ? "EN" : "한",
|
||||
variant: "pill",
|
||||
onClick: onApp_Lang_Toggle_Click,
|
||||
}),
|
||||
createButton({
|
||||
label: document.documentElement.getAttribute("data-theme") === "dark" ? "☀" : "☾",
|
||||
variant: "pill",
|
||||
onClick: onApp_Theme_Toggle_Click,
|
||||
}),
|
||||
);
|
||||
const auth = document.createElement("div");
|
||||
auth.className = "app-drawer__row app-drawer__auth";
|
||||
void fillAuthActions(auth);
|
||||
drawer.append(navList, prefs, auth, buildTouchModeSwitch());
|
||||
return [burger, scrim, drawer];
|
||||
}
|
||||
|
||||
function buildTouchModeSwitch(): HTMLElement {
|
||||
const box = document.createElement("div");
|
||||
box.className = "app-drawer__touch";
|
||||
const label = document.createElement("span");
|
||||
label.textContent = L("App_TouchMode");
|
||||
const group = document.createElement("div");
|
||||
group.className = "app-drawer__row";
|
||||
group.setAttribute("role", "radiogroup");
|
||||
group.setAttribute("aria-label", L("App_TouchMode"));
|
||||
const modes: [TouchMode, keyof typeof ui_locales][] = [
|
||||
["auto", "App_TouchMode_Auto"],
|
||||
["on", "App_TouchMode_On"],
|
||||
["off", "App_TouchMode_Off"],
|
||||
];
|
||||
const buttons = modes.map(([mode, key]) => {
|
||||
const b = createButton({
|
||||
label: L(key),
|
||||
variant: "pill",
|
||||
onClick: () => {
|
||||
setTouchMode(mode);
|
||||
mark();
|
||||
},
|
||||
});
|
||||
b.setAttribute("role", "radio");
|
||||
b.dataset.touchMode = mode;
|
||||
return b;
|
||||
});
|
||||
const mark = (): void => {
|
||||
const now = readTouchMode();
|
||||
for (const b of buttons) b.setAttribute("aria-checked", String(b.dataset.touchMode === now));
|
||||
};
|
||||
mark();
|
||||
group.append(...buttons);
|
||||
box.append(label, group);
|
||||
return box;
|
||||
}
|
||||
|
||||
/** 서버 세션 상태에 따라 로그인/로그아웃 버튼을 채운다. */
|
||||
async function fillAuthActions(slot: HTMLElement): Promise<void> {
|
||||
try {
|
||||
@@ -259,10 +354,10 @@ const SHELL_CSS = `
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
.app-outlet {
|
||||
min-height: calc(100vh - 64px - 56px);
|
||||
min-height: calc(100dvh - 64px - 56px);
|
||||
}
|
||||
.app-outlet--without-footer {
|
||||
min-height: calc(100vh - 64px);
|
||||
min-height: calc(100dvh - 64px);
|
||||
}
|
||||
.app-footer {
|
||||
height: 56px;
|
||||
@@ -285,7 +380,79 @@ const SHELL_CSS = `
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.app-nav { display: none; }
|
||||
.app-actions { margin-left: auto; }
|
||||
}
|
||||
/* ☰ 서랍 — 폭 900 이하 · 터치 모드에서만 보임(데스크탑 마우스엔 없음) */
|
||||
.app-burger,
|
||||
.app-drawer,
|
||||
.app-drawer__scrim {
|
||||
display: none;
|
||||
}
|
||||
.app-burger {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-buttons);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.app-burger { display: inline-flex; }
|
||||
}
|
||||
html[data-touch="on"] .app-burger { display: inline-flex; }
|
||||
.app-header.is-drawer-open .app-drawer__scrim {
|
||||
display: block;
|
||||
position: fixed;
|
||||
inset: 64px 0 0 0;
|
||||
background: rgba(38, 17, 74, 0.35);
|
||||
}
|
||||
.app-header.is-drawer-open .app-drawer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-16);
|
||||
position: fixed;
|
||||
top: 64px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: min(320px, 100vw);
|
||||
padding: var(--spacing-16);
|
||||
overflow-y: auto;
|
||||
background: var(--color-surface-raised);
|
||||
border-left: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.app-drawer__nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
.app-drawer__nav > * { justify-content: flex-start; min-height: 44px; }
|
||||
.app-drawer__row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
.app-drawer__touch {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding-top: var(--spacing-16);
|
||||
border-top: 1px solid var(--color-border);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
.app-drawer__touch [aria-checked="true"] {
|
||||
background: var(--color-mist-violet);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
/* 휴대폰 폭 — 오른쪽 단추(언어 · 테마 · 로그인)는 서랍 안으로 */
|
||||
html[data-size="compact"] .app-header { padding: 0 var(--spacing-16); gap: var(--spacing-8); }
|
||||
html[data-size="compact"] .app-actions { display: none; }
|
||||
html[data-size="compact"] .app-burger { margin-left: auto; }
|
||||
`;
|
||||
|
||||
export function injectShellStyles(): void {
|
||||
|
||||
@@ -174,7 +174,7 @@ const SCAFFOLD_CSS = `
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.b-scaffold-wf { height: calc(100vh - 64px - 56px); }
|
||||
.b-scaffold-wf { height: calc(100dvh - 64px - 56px); }
|
||||
|
||||
.b-pending {
|
||||
display: flex;
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/* =============================================================================
|
||||
* device_context.css — 기기 대응 공통 규칙(PLAN_기기대응 2-6)
|
||||
* 모든 규칙은 `<html>` 표시(data-touch · data-size) 안에서만 — 데스크탑 마우스 화면은 안 바뀜.
|
||||
* 표시는 `device_context.ts` 가 적음.
|
||||
* ========================================================================== */
|
||||
|
||||
/* 누를 자리 44px — 손가락(주 입력 coarse) 또는 터치 모드 켬 */
|
||||
html[data-touch="on"]
|
||||
:is(
|
||||
.ui-btn,
|
||||
.ui-workflow-overlay__toggle,
|
||||
.ui-sidebar-row,
|
||||
.ui-workflow-layout__step,
|
||||
.ui-ctx-menu__item,
|
||||
select,
|
||||
input:not([type="checkbox"], [type="radio"], [type="range"], [type="hidden"], [type="color"])
|
||||
) {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
html[data-touch="on"] .ui-btn {
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
/* 체크 · 라디오 — 상자를 키우고 글까지 한 줄 전체가 누를 자리 */
|
||||
html[data-touch="on"] :is(input[type="checkbox"], input[type="radio"]) {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
html[data-touch="on"] .ui-checkbox {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
/* 좌측 패널 손잡이 — 손가락 폭 */
|
||||
html[data-touch="on"] .ui-workflow-overlay__handle--side {
|
||||
width: 32px;
|
||||
right: -31px;
|
||||
}
|
||||
|
||||
/* 팝업(2-4) — 폭 600 미만은 전체 화면 · 겹치면 나중 것(맨 위)만 보이고 × 로 앞 창.
|
||||
공용 모달(.ui-modal) · 페이지 팝업은 공용 클래스 `.ui-popup` 만 붙여 맞춤(자리 · 크기 인라인 값도 덮음) */
|
||||
html[data-size="compact"] :is(.ui-modal, .ui-popup) {
|
||||
position: fixed !important;
|
||||
inset: 0 !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
min-width: 0 !important;
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
margin: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
transform: none !important;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
html[data-size="compact"] .ui-confirm__panel {
|
||||
min-width: 0;
|
||||
max-width: calc(100vw - var(--spacing-32));
|
||||
}
|
||||
|
||||
/* 공용 표(2-5) — 폭 840 미만은 고정 열이 없는 표도 첫 열을 붙임(머리줄은 늘 붙어 있음 · 가로는 밀기) */
|
||||
html:is([data-size="compact"], [data-size="medium"])
|
||||
.ui-data-table__table:not(:has(.is-frozen))
|
||||
tr
|
||||
> :first-child {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
html:is([data-size="compact"], [data-size="medium"])
|
||||
.ui-data-table__table:not(:has(.is-frozen))
|
||||
thead
|
||||
tr
|
||||
> :first-child {
|
||||
z-index: 3;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/* =============================================================================
|
||||
* device_context.ts
|
||||
* 기기 상태 한 곳(요구 193~195 · PLAN_기기대응 2-1) — `<html>` 에 지금 상태를 늘 적음.
|
||||
*
|
||||
* - data-size = compact(<600) · medium(<840) · expanded(<1200) · large ← 배치는 이것만 봄
|
||||
* - data-pointer = 주 입력 coarse · fine(미디어 `pointer`)
|
||||
* - data-input = 마지막 누름의 pointerType(mouse · touch · pen)
|
||||
* - data-touch-mode = 사용자 스위치(auto · on · off · 이 기기 브라우저에만 저장)
|
||||
* - data-touch = on · off — 누를 자리 크기를 키울지(auto 면 주 입력 coarse 일 때)
|
||||
* 바뀔 때만 window 에 `app:context` 이벤트(detail = 지금 상태).
|
||||
* 경계 표는 여기 한 벌 — CSS 는 위 표시(data-size)로 가르고, 숫자가 필요하면 --bp-* 변수.
|
||||
* 첫 화면 깜빡임 — 화면은 모두 JS 가 그리므로 main.ts 가 맨 먼저 import 하면 그리기 전에 적힘.
|
||||
* ========================================================================== */
|
||||
|
||||
import "./device_context.css";
|
||||
|
||||
export const SIZE_BREAKS = { medium: 600, expanded: 840, large: 1200 } as const;
|
||||
export type SizeClass = "compact" | "medium" | "expanded" | "large";
|
||||
export type TouchMode = "auto" | "on" | "off";
|
||||
|
||||
export interface DeviceContext {
|
||||
size: SizeClass;
|
||||
pointer: "coarse" | "fine";
|
||||
input: string;
|
||||
touchMode: TouchMode;
|
||||
touch: boolean;
|
||||
}
|
||||
|
||||
const TOUCH_MODE_KEY = "frd_touch_mode";
|
||||
|
||||
export function sizeClassOf(width: number): SizeClass {
|
||||
if (width < SIZE_BREAKS.medium) return "compact";
|
||||
if (width < SIZE_BREAKS.expanded) return "medium";
|
||||
if (width < SIZE_BREAKS.large) return "expanded";
|
||||
return "large";
|
||||
}
|
||||
|
||||
let modeInMemory: TouchMode | null = null; // 저장이 막힌 브라우저용
|
||||
|
||||
export function readTouchMode(): TouchMode {
|
||||
if (modeInMemory) return modeInMemory;
|
||||
try {
|
||||
const v = localStorage.getItem(TOUCH_MODE_KEY);
|
||||
return v === "on" || v === "off" ? v : "auto";
|
||||
} catch {
|
||||
return "auto";
|
||||
}
|
||||
}
|
||||
|
||||
const coarseQuery = window.matchMedia("(pointer: coarse)");
|
||||
let current: DeviceContext | null = null;
|
||||
let lastInput = "";
|
||||
|
||||
function read(): DeviceContext {
|
||||
const pointer = coarseQuery.matches ? "coarse" : "fine";
|
||||
const touchMode = readTouchMode();
|
||||
return {
|
||||
size: sizeClassOf(window.innerWidth),
|
||||
pointer,
|
||||
input: lastInput || (pointer === "coarse" ? "touch" : "mouse"),
|
||||
touchMode,
|
||||
touch: touchMode === "on" || (touchMode === "auto" && pointer === "coarse"),
|
||||
};
|
||||
}
|
||||
|
||||
function apply(): void {
|
||||
const next = read();
|
||||
if (current && JSON.stringify(current) === JSON.stringify(next)) return;
|
||||
const html = document.documentElement;
|
||||
html.dataset.size = next.size;
|
||||
html.dataset.pointer = next.pointer;
|
||||
html.dataset.input = next.input;
|
||||
html.dataset.touchMode = next.touchMode;
|
||||
html.dataset.touch = next.touch ? "on" : "off";
|
||||
const first = !current;
|
||||
current = next;
|
||||
if (!first) window.dispatchEvent(new CustomEvent("app:context", { detail: next }));
|
||||
}
|
||||
|
||||
export function deviceContext(): DeviceContext {
|
||||
return current ?? read();
|
||||
}
|
||||
|
||||
/** 터치 모드 스위치(☰ 서랍) — 이 기기 브라우저에만 저장 */
|
||||
export function setTouchMode(mode: TouchMode): void {
|
||||
modeInMemory = mode;
|
||||
try {
|
||||
if (mode === "auto") localStorage.removeItem(TOUCH_MODE_KEY);
|
||||
else localStorage.setItem(TOUCH_MODE_KEY, mode);
|
||||
} catch {
|
||||
/* 저장 막힌 브라우저 — 이번 화면만 */
|
||||
}
|
||||
apply();
|
||||
}
|
||||
|
||||
function init(): void {
|
||||
const html = document.documentElement;
|
||||
for (const [name, px] of Object.entries(SIZE_BREAKS))
|
||||
html.style.setProperty(`--bp-${name}`, `${px}px`);
|
||||
apply();
|
||||
coarseQuery.addEventListener("change", apply);
|
||||
window.addEventListener("resize", apply);
|
||||
window.visualViewport?.addEventListener("resize", apply);
|
||||
screen.orientation?.addEventListener("change", apply);
|
||||
window.addEventListener(
|
||||
"pointerdown",
|
||||
(e) => {
|
||||
if (e.pointerType === lastInput) return;
|
||||
lastInput = e.pointerType;
|
||||
apply();
|
||||
},
|
||||
{ capture: true, passive: true },
|
||||
);
|
||||
}
|
||||
|
||||
init();
|
||||
@@ -9,6 +9,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import "./random_uuid_polyfill";
|
||||
import "./device_context"; // 맨 앞 — 그리기 전에 <html> 기기 표시(기기대응 2-1)
|
||||
import "@ui/ui_template_theme.css";
|
||||
import { injectBaseStyles } from "@ui/ui_template_elements";
|
||||
import { blockNativeMenu, NATIVE_MENU_ROUTE } from "@ui/ui_template_native_menu";
|
||||
|
||||
@@ -129,7 +129,7 @@ export function mountDataMenu(ctx: SpreadsheetContext, back: () => void): DataMe
|
||||
attrs: { type: "button", title: TEXT.close },
|
||||
});
|
||||
const dialog = el("div", {
|
||||
className: "ss-dialog",
|
||||
className: "ss-dialog ui-popup",
|
||||
attrs: { hidden: "" },
|
||||
children: [el("div", { className: "ss-dialog__head", children: [title, shut] }), body],
|
||||
});
|
||||
|
||||
@@ -210,7 +210,7 @@ export function openEvalSteps(ctx: SpreadsheetContext): () => void {
|
||||
onClick: () => go(i + 1 < steps.length ? i + 1 : 0),
|
||||
});
|
||||
const box = el("div", {
|
||||
className: "ss-steps__box",
|
||||
className: "ss-steps__box ui-popup",
|
||||
attrs: { role: "dialog", "aria-label": t("Title") },
|
||||
});
|
||||
const shade = el("div", { className: "ss-steps", children: [box] });
|
||||
|
||||
@@ -87,7 +87,7 @@ export function openSortDialog(ctx: SpreadsheetContext): () => void {
|
||||
const caseBox = el("input", { attrs: { type: "checkbox" } });
|
||||
const list = el("div", { className: "ss-sort__keys" });
|
||||
const box = el("div", {
|
||||
className: "ss-sort__box",
|
||||
className: "ss-sort__box ui-popup",
|
||||
attrs: { role: "dialog", "aria-label": t("Title") },
|
||||
});
|
||||
const shade = el("div", { className: "ss-sort", children: [box] });
|
||||
|
||||
@@ -560,7 +560,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(380px, calc(100vw - 32px));
|
||||
height: min(680px, calc(100vh - 104px));
|
||||
height: min(680px, calc(100dvh - 104px));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
background: var(--color-surface, #fff);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.b03-file {
|
||||
min-height: calc(100vh - var(--app-header-height, 0px));
|
||||
min-height: calc(100dvh - var(--app-header-height, 0px));
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--color-canvas, #ffffff) 0%,
|
||||
|
||||
@@ -74,7 +74,7 @@ export function createTempPicker(
|
||||
const backdrop = document.createElement("div");
|
||||
backdrop.className = "b03-file__modal-backdrop";
|
||||
const modal = document.createElement("div");
|
||||
modal.className = "b03-file__modal b03-file__temp-modal";
|
||||
modal.className = "b03-file__modal b03-file__temp-modal ui-popup";
|
||||
modal.setAttribute("role", "dialog");
|
||||
modal.setAttribute("aria-modal", "true");
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
min-height: calc(100vh - var(--spacing-64));
|
||||
min-height: calc(100dvh - var(--spacing-64));
|
||||
}
|
||||
|
||||
.b04-surface__workspace > .b04-surface__status {
|
||||
|
||||
@@ -45,7 +45,7 @@ export function createRouteEditChrome(): RouteEditChrome {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "ui-modal__backdrop b05-routeedit";
|
||||
overlay.innerHTML = `
|
||||
<div class="b05-routeedit__box" role="dialog" aria-label="계획노선 편집">
|
||||
<div class="b05-routeedit__box ui-popup" role="dialog" aria-label="계획노선 편집">
|
||||
<div class="ui-modal__top b05-routeedit__head">
|
||||
<h3>계획노선 편집</h3>
|
||||
<span class="b05-routeedit__rec-now" data-rec-now></span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.b05-route-layout {
|
||||
height: calc(100vh - var(--spacing-64));
|
||||
height: calc(100dvh - var(--spacing-64));
|
||||
height: calc(100dvh - var(--spacing-64));
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
/* --- 좌측 입력 폼 --- */
|
||||
.b06-profile-layout {
|
||||
height: calc(100vh - var(--spacing-64));
|
||||
height: calc(100dvh - var(--spacing-64));
|
||||
height: calc(100dvh - var(--spacing-64));
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
/* 워크플로우 레이아웃 높이 (B06 패턴 준수) */
|
||||
.b07-design-layout {
|
||||
height: calc(100vh - var(--spacing-64));
|
||||
height: calc(100dvh - var(--spacing-64));
|
||||
height: calc(100dvh - var(--spacing-64));
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -39,11 +39,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 카드 높이 = 화면 따라(B08 다른 표와 같은 calc(100vh − N)) · 넘치면 카드 몸만 스크롤 */
|
||||
/* 카드 높이 = 화면 따라(B08 다른 표와 같은 calc(100dvh − N)) · 넘치면 카드 몸만 스크롤 */
|
||||
.b08-hydro__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 240px);
|
||||
max-height: calc(100dvh - 240px);
|
||||
}
|
||||
|
||||
.b08-hydro__card .ui-card__body {
|
||||
|
||||
@@ -113,7 +113,7 @@ export function openRowPopup(o: PopupOpen): void {
|
||||
listen();
|
||||
const level = stack.length;
|
||||
const box = document.createElement("div");
|
||||
box.className = "b09-estimation b09-popup";
|
||||
box.className = "b09-estimation b09-popup ui-popup";
|
||||
box.setAttribute("role", "dialog");
|
||||
box.setAttribute("aria-label", tabName(tab));
|
||||
box.dataset.b09Popup = tab;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
height: calc(100vh - var(--spacing-64));
|
||||
height: calc(100dvh - var(--spacing-64));
|
||||
padding: var(--spacing-16) var(--spacing-24);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ function openTablePick(onPick: (table: TableHead) => void): void {
|
||||
const search = createInputField({ type: "search", placeholder: tl("Copy_PickSearch") });
|
||||
const list = el("div", { className: "m01-logic__pick-list" });
|
||||
const dialog = el("div", {
|
||||
className: "m01-logic__pick",
|
||||
className: "m01-logic__pick ui-popup",
|
||||
attrs: { role: "dialog", "aria-label": tl("Copy_PickTitle") },
|
||||
children: [
|
||||
el("div", {
|
||||
|
||||
@@ -47,7 +47,7 @@ export function openPicker(mode: PickMode, group: string, done: PickDone): void
|
||||
const list = el("div", { className: "m01-logic__pick-list" });
|
||||
const close = (): void => backdrop.remove();
|
||||
const dialog = el("div", {
|
||||
className: "m01-logic__pick",
|
||||
className: "m01-logic__pick ui-popup",
|
||||
attrs: { role: "dialog", "aria-label": tx("Pick_Title") },
|
||||
children: [
|
||||
el("div", {
|
||||
|
||||
@@ -51,7 +51,7 @@ export function openPickTable(opt: PickTableOptions): void {
|
||||
const search = searchField.input;
|
||||
const host = el("div", { className: "m01-logic__pick-list" });
|
||||
const dialog = el("div", {
|
||||
className: "m01-logic__pick m01-logic__pick--table",
|
||||
className: "m01-logic__pick m01-logic__pick--table ui-popup",
|
||||
attrs: { role: "dialog", "aria-label": opt.title },
|
||||
children: [
|
||||
el("div", {
|
||||
|
||||
@@ -347,7 +347,7 @@ input.m01-logic__input {
|
||||
width: min(720px, 92vw);
|
||||
/* 위에서 고정 거리 — 공용 .ui-modal 과 같은 규칙(찾기 칸이 안 움직임) */
|
||||
max-height: var(--modal-max, 80vh);
|
||||
margin-top: calc((100vh - var(--modal-max, 80vh)) / 2);
|
||||
margin-top: calc((100dvh - var(--modal-max, 80vh)) / 2);
|
||||
padding: var(--spacing-16);
|
||||
border-radius: var(--radius-xl);
|
||||
border: 1px solid var(--color-border);
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
}
|
||||
|
||||
.m01-master__grid-wrap {
|
||||
max-height: calc(100vh - 300px);
|
||||
max-height: calc(100dvh - 300px);
|
||||
overflow: auto;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
|
||||
@@ -121,7 +121,7 @@ button.m02-dfpanel-chip:focus-visible {
|
||||
/* 판 높이 크게 — 안쪽 세로 스크롤 없이 보이게 */
|
||||
.m02-dfpanel-sheet {
|
||||
position: relative;
|
||||
height: max(640px, calc(100vh - 260px));
|
||||
height: max(640px, calc(100dvh - 260px));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
overflow: hidden;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
표 양식일 때는 페이지 쪽 모양 그대로 두려고 도면을 품은 때만 건다. */
|
||||
.m02-main:has(> .m02-main__host > .m02-drawing) {
|
||||
box-sizing: border-box;
|
||||
height: calc(100vh - var(--spacing-64));
|
||||
height: calc(100dvh - var(--spacing-64));
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
}
|
||||
|
||||
.m02-itempick .m01-master__grid-wrap {
|
||||
max-height: min(60vh, calc(100vh - 300px));
|
||||
max-height: min(60vh, calc(100dvh - 300px));
|
||||
}
|
||||
|
||||
.ui-modal.m02-itempick__dialog {
|
||||
|
||||
@@ -252,7 +252,7 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
});
|
||||
|
||||
const dialog = el("div", {
|
||||
className: "m01-logic__pick m01-combo",
|
||||
className: "m01-logic__pick m01-combo ui-popup",
|
||||
attrs: { role: "dialog", "aria-label": tc("Pick_Title") },
|
||||
children: [
|
||||
el("div", {
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
/* 카드가 보이면 상세 칸은 창 높이까지 — 표 + 카드가 넘치면 상세 칸 전체가 스크롤 */
|
||||
.m02-main:has(> .m02-main__extra > .m02-struct:not([hidden])) {
|
||||
box-sizing: border-box;
|
||||
max-height: calc(100vh - var(--spacing-64));
|
||||
max-height: calc(100dvh - var(--spacing-64));
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
/* 면적산출 · 재료산출 전체 편집 — `.ss` 는 height:100% 로 부모 높이를 받음 · 도면 편집과 같이 메인 칸을 화면 높이로 채움 */
|
||||
.m02-main:has(> .m02-main__host > .ss) {
|
||||
box-sizing: border-box;
|
||||
height: calc(100vh - var(--spacing-64));
|
||||
height: calc(100dvh - var(--spacing-64));
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
/* 일위대가 상세(85-5) — 면적 상세와 같은 고정 높이 · 본문 칸만 굴림 · 오른쪽 패널은 칸 높이(따로 스크롤) */
|
||||
.m02-main:has(> .m02-main__host > .ui-side-panels-row) {
|
||||
box-sizing: border-box;
|
||||
height: calc(100vh - var(--spacing-64));
|
||||
height: calc(100dvh - var(--spacing-64));
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
.m02-opts__kinds,
|
||||
.m02-opts__list {
|
||||
max-height: calc(100vh - 20rem);
|
||||
max-height: calc(100dvh - 20rem);
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -72,7 +72,7 @@ def test_공용_표_옵션과_연결():
|
||||
def test_합계_줄_고정_그대로():
|
||||
css = _read("ui_template", "ui_template_data_table.css")
|
||||
assert re.search(r"tfoot[^{]*\{[^}]*position:\s*sticky[^}]*bottom:\s*0", css, re.S)
|
||||
assert "max-height: calc(100vh - 260px)" in css # 옵션 안 준 표의 기본값은 그대로
|
||||
assert "max-height: calc(100dvh - 260px)" in css # 옵션 안 준 표의 기본값은 그대로
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -238,7 +238,7 @@ def test_창_열_제목_강조_줄_자리():
|
||||
" table: box.children[1].dataset.b09Table, left: box.style.left, top: box.style.top }));"
|
||||
)
|
||||
assert out["boxes"] == ["bill"] and out["n"] == 1
|
||||
assert out["cls"] == "b09-estimation b09-popup" # 창 안 = b09 표 꼴(빨강 테두리)
|
||||
assert out["cls"] == "b09-estimation b09-popup ui-popup" # 창 안 = b09 표 꼴(빨강 테두리)
|
||||
assert out["head"] == ["설계내역서", "탭으로", "×"]
|
||||
assert out["rows"] == ["b1", "b2", "b3"] # 그 줄 + 자손
|
||||
assert out["rowCls"] == ["is-title is-pick is-pick-top", "is-pick", "is-pick is-pick-end"]
|
||||
|
||||
@@ -157,6 +157,14 @@ def _open(page, route):
|
||||
page.wait_for_load_state("networkidle", timeout=15000)
|
||||
except Exception:
|
||||
pass
|
||||
try: # 불러오는 중 표시(돌이 · 진행 원)가 사라질 때까지 — 반쯤 그린 화면을 찍지 않게
|
||||
page.wait_for_function(
|
||||
"() => ![...document.querySelectorAll('.ui-loading, .ui-progress-circle')]"
|
||||
".some((e) => e.getClientRects().length)",
|
||||
timeout=45000,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
page.wait_for_timeout(1500)
|
||||
|
||||
|
||||
|
||||
@@ -155,6 +155,6 @@ def test_항목_추가_창_카드_격자_표_접기():
|
||||
assert "grid-auto-rows: var(--m02-struct-card-h);" in css
|
||||
assert "--m02-struct-card-h: min(640px, 70vh);" in css # 31-6 화면 높이에 맞춤
|
||||
assert "flex: 1 0 var(--m02-struct-grid-min);" in css # 엑셀 격자가 카드 높이를 채움
|
||||
assert "max-height: calc(100vh - var(--spacing-64));\n overflow-y: auto;" in css
|
||||
assert "max-height: calc(100dvh - var(--spacing-64));\n overflow-y: auto;" in css
|
||||
main = (m02 / "M02_MasterTemplete_UI_Main.ts").read_text("utf-8")
|
||||
assert "host.hidden = !host.hidden;" in main and 'title.classList.toggle("is-folded"' in main
|
||||
|
||||
@@ -28,7 +28,7 @@ def test_공용_모달_위_고정():
|
||||
assert "align-items: flex-start;" in _block(css, ".ui-modal__backdrop")
|
||||
modal = _block(css, ".ui-modal")
|
||||
assert "max-height: var(--modal-max, 80vh);" in modal
|
||||
assert "margin-top: calc((100vh - var(--modal-max, 80vh)) / 2);" in modal
|
||||
assert "margin-top: calc((100dvh - var(--modal-max, 80vh)) / 2);" in modal
|
||||
# 큰 창은 max-height 대신 --modal-max(위 거리가 함께 맞음)
|
||||
# 고르기 창 = 공용 큰 창 곁값(147-2)
|
||||
assert "--modal-max: 94vh;" in _block(css, ".ui-modal.ui-modal--wide")
|
||||
@@ -44,7 +44,7 @@ def test_공용_모달_위_고정():
|
||||
def test_따로_둔_모달도_위_고정():
|
||||
logic = _text("M01_MasterData/M01_MasterData_UI_Logic_Style.css")
|
||||
assert "align-items: flex-start;" in _block(logic, ".m01-logic__backdrop")
|
||||
assert "margin-top: calc((100vh - var(--modal-max, 80vh)) / 2);" in _block(
|
||||
assert "margin-top: calc((100dvh - var(--modal-max, 80vh)) / 2);" in _block(
|
||||
logic, ".m01-logic__pick"
|
||||
)
|
||||
assert "--modal-max: 90vh;" in _block(
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
.ui-sheet__scroll {
|
||||
overflow: auto;
|
||||
max-height: calc(100vh - 260px);
|
||||
max-height: calc(100dvh - 260px);
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-canvas);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:where(.ui-ctx-menu) {
|
||||
position: absolute;
|
||||
z-index: var(--z-dropdown);
|
||||
max-height: calc(100vh - 8px);
|
||||
max-height: calc(100dvh - 8px);
|
||||
}
|
||||
|
||||
.ui-ctx-menu {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
--dt-lv5-size: var(--text-caption);
|
||||
width: fit-content; /* 표가 좁으면 틀도 표만큼 */
|
||||
max-width: 100%;
|
||||
max-height: calc(100vh - 260px);
|
||||
max-height: calc(100dvh - 260px);
|
||||
overflow: auto;
|
||||
border: 1px solid var(--dt-line);
|
||||
border-radius: var(--radius-lg);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.ui-general-layout {
|
||||
min-height: calc(100vh - var(--spacing-64));
|
||||
min-height: calc(100dvh - var(--spacing-64));
|
||||
padding: var(--spacing-40) var(--spacing-24) var(--spacing-48);
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
@@ -121,11 +121,17 @@ export function attachHoverInfo(root: HTMLElement, text: HoverText, selector?: s
|
||||
};
|
||||
const over = (e: Event): void => enter(e, HOVER_DELAY_MS);
|
||||
const focus = (e: Event): void => enter(e, 0);
|
||||
// 손가락 — hover 가 없으니 톡 하면 바로 뜸(기기대응 2-6 · 다음 누름에 닫힘)
|
||||
const tap = (e: PointerEvent): void => {
|
||||
if (e.pointerType === "touch") enter(e, 0);
|
||||
};
|
||||
root.addEventListener("pointerup", tap);
|
||||
root.addEventListener("mouseover", over);
|
||||
root.addEventListener("mouseout", leave);
|
||||
root.addEventListener("focusin", focus);
|
||||
root.addEventListener("focusout", leave);
|
||||
return () => {
|
||||
root.removeEventListener("pointerup", tap);
|
||||
root.removeEventListener("mouseover", over);
|
||||
root.removeEventListener("mouseout", leave);
|
||||
root.removeEventListener("focusin", focus);
|
||||
|
||||
@@ -100,6 +100,11 @@ export const ui_locales_common = {
|
||||
App_Nav_News: ["소식", "News"],
|
||||
App_Nav_Education: ["교육", "Education"],
|
||||
App_Nav_Support: ["기술지원", "Support"],
|
||||
App_Menu: ["메뉴", "Menu"],
|
||||
App_TouchMode: ["터치 모드", "Touch mode"],
|
||||
App_TouchMode_Auto: ["자동", "Auto"],
|
||||
App_TouchMode_On: ["켬", "On"],
|
||||
App_TouchMode_Off: ["끔", "Off"],
|
||||
App_Footer_Copyright: [
|
||||
"© 2026 임도설계 및 견적 자동화. All rights reserved.",
|
||||
"© 2026 Forest Road Design & Estimation. All rights reserved.",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
gap: var(--spacing-8);
|
||||
width: min(720px, 92vw);
|
||||
max-height: var(--modal-max, 80vh);
|
||||
margin-top: calc((100vh - var(--modal-max, 80vh)) / 2);
|
||||
margin-top: calc((100dvh - var(--modal-max, 80vh)) / 2);
|
||||
padding: var(--spacing-16);
|
||||
border-radius: var(--radius-xl);
|
||||
border: 1px solid var(--color-border);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
z-index: var(--z-dropdown);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - var(--spacing-64) - var(--spacing-32));
|
||||
max-height: calc(100dvh - var(--spacing-64) - var(--spacing-32));
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
@@ -305,7 +305,7 @@
|
||||
.ui-workflow-overlay__panel--progress {
|
||||
top: calc(var(--spacing-64) + var(--spacing-8));
|
||||
right: var(--spacing-8);
|
||||
max-height: calc(100vh - var(--spacing-64) - var(--spacing-16));
|
||||
max-height: calc(100dvh - var(--spacing-64) - var(--spacing-16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,3 +523,31 @@
|
||||
background: var(--color-mist-violet);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* 기기 대응(PLAN_기기대응 2-3) — 폭 840 미만(data-size compact · medium)만.
|
||||
좌측 패널 = 서랍(본문 위에 겹침 · 덮개를 누르면 닫힘) · 진행단계 판 = 아래에서 올라오는 판(뒤 화면 계속 조작). */
|
||||
.ui-workflow-overlay__scrim {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html:is([data-size="compact"], [data-size="medium"])
|
||||
.ui-workflow-overlay:has(> .ui-workflow-overlay__panel--title:not(.is-collapsed))
|
||||
> .ui-workflow-overlay__scrim {
|
||||
display: block;
|
||||
position: fixed;
|
||||
inset: var(--spacing-64) 0 0 0;
|
||||
z-index: calc(var(--z-dropdown) - 1);
|
||||
background: rgba(38, 17, 74, 0.35);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
html:is([data-size="compact"], [data-size="medium"]) .ui-workflow-overlay__panel--progress {
|
||||
top: auto !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
width: auto !important;
|
||||
max-height: 50dvh;
|
||||
flex-direction: column;
|
||||
border-radius: var(--radius-cards) var(--radius-cards) 0 0;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { makePanelDraggable } from "./ui_template_overlay_drag";
|
||||
// 두 번 받는다(2026-09-06 실측). 그 창구가 짧은 시간 동안 캐시한다.
|
||||
import { fetchWorkflowState } from "../A00_Common/b_workflow_nav";
|
||||
import { readByKey, writeByKey } from "../A00_Common/b_page_state";
|
||||
import { SIZE_BREAKS } from "../A00_Common/device_context";
|
||||
|
||||
const TITLE_OVERLAY_STATE_KEY = "frd_workflow_title_overlay_open";
|
||||
const PROGRESS_OVERLAY_STATE_KEY = "frd_workflow_progress_overlay_open";
|
||||
@@ -99,6 +100,8 @@ const NARROW_START_COLLAPSED_MAX_PX = 560;
|
||||
* 위에 겹쳐 떠서, 펼친 채로 열면 본문이 가려진 화면부터 보게 된다(2026-09-08).
|
||||
*/
|
||||
function readOpenState(key: string, collapseWhenNarrow = false): boolean {
|
||||
// 폭 840 미만 = 서랍(기기대응 2-3) — 저장값과 상관없이 접은 채로 시작
|
||||
if (collapseWhenNarrow && window.innerWidth < SIZE_BREAKS.expanded) return false;
|
||||
const raw = readByKey(key);
|
||||
if (raw !== null) return raw !== "false";
|
||||
if (!collapseWhenNarrow) return true;
|
||||
@@ -258,7 +261,11 @@ export function createWorkflowOverlays(options: WorkflowOverlayOptions): Workflo
|
||||
options.onOptionsOpenChange,
|
||||
options.showProjectName === false ? undefined : createProjectNameTag(),
|
||||
);
|
||||
root.append(titlePanel.root);
|
||||
// 좁은 폭 서랍의 덮개 — 누르면 닫힘(보임은 CSS · 폭 840 미만 + 펼침)
|
||||
const scrim = document.createElement("div");
|
||||
scrim.className = "ui-workflow-overlay__scrim";
|
||||
scrim.addEventListener("click", () => titlePanel.setOpen(false));
|
||||
root.append(scrim, titlePanel.root);
|
||||
setTitleOpen = titlePanel.setOpen;
|
||||
}
|
||||
let setProgressOpen = (_isOpen: boolean): void => {};
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
flex: none;
|
||||
align-self: flex-start;
|
||||
/* 높이 · 붙는 자리 = JS(followView) · 처음 한 번 그릴 때까지 */
|
||||
height: calc(100vh - 80px);
|
||||
height: calc(100dvh - 80px);
|
||||
/* 손잡이 자리(24) + 틈 */
|
||||
margin-left: calc(var(--spacing-24) + var(--spacing-8));
|
||||
}
|
||||
@@ -130,3 +130,40 @@
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain; /* 몸 끝에서 메인으로 이어 굴리지 않음(85-5) */
|
||||
}
|
||||
|
||||
/* 기기 대응(PLAN_기기대응 2-3) — 폭 840 미만은 오른쪽 패널을 본문 아래로 쌓음(좁으면 보조 칸은 아래) ·
|
||||
손잡이 줄은 패널 위 가로 · 붙어 따라오기(followView 인라인 높이 · 이동)는 끔 */
|
||||
html:is([data-size="compact"], [data-size="medium"]) .ui-side-panels-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
html:is([data-size="compact"], [data-size="medium"]) .ui-side-panels,
|
||||
html:is([data-size="compact"], [data-size="medium"]) .ui-side-panels.is-fill,
|
||||
html:is([data-size="compact"], [data-size="medium"]) .ui-side-panels.is-collapsed {
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
width: auto;
|
||||
height: auto !important;
|
||||
margin: var(--spacing-8) 0 0;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
html:is([data-size="compact"], [data-size="medium"]) .ui-side-panels__handles {
|
||||
position: static;
|
||||
flex-direction: row;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
html:is([data-size="compact"], [data-size="medium"]) .ui-side-panels__panel,
|
||||
html:is([data-size="compact"], [data-size="medium"])
|
||||
.ui-side-panels.is-fill
|
||||
> .ui-side-panels__panel {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
width: auto;
|
||||
height: 60dvh;
|
||||
}
|
||||
|
||||
html:is([data-size="compact"], [data-size="medium"]) .ui-side-panels__grip {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.ui-workflow-layout {
|
||||
position: relative;
|
||||
min-height: calc(100vh - var(--spacing-64));
|
||||
min-height: calc(100dvh - var(--spacing-64));
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
}
|
||||
|
||||
.ui-workflow-layout__body {
|
||||
min-height: calc(100vh - var(--spacing-64));
|
||||
min-height: calc(100dvh - var(--spacing-64));
|
||||
transition: padding-left var(--transition-base);
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
.ui-workflow-layout__main {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: calc(100vh - var(--spacing-64));
|
||||
min-height: calc(100dvh - var(--spacing-64));
|
||||
overflow: auto;
|
||||
background: var(--color-canvas);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user