Merge remote-tracking branch 'origin/dev' into sub_laptop_2
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
/* ui_template/cad_host/cad_host_tabs — CAD 를 감싸는 공용 탭 줄(82-1 · M02 구조물 도면 · B07). */
|
||||
|
||||
.cad-tabs {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
padding: 4px 6px 0;
|
||||
overflow-x: auto;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.cad-tabs__tab {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 220px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-bottom: none;
|
||||
border-radius: 6px 6px 0 0;
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.cad-tabs__tab[data-active="true"] {
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
box-shadow: inset 0 2px 0 var(--color-primary);
|
||||
}
|
||||
|
||||
.cad-tabs__tab[data-dirty="true"] .cad-tabs__label::after {
|
||||
content: " ●";
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.cad-tabs__tab[data-drop="before"] {
|
||||
box-shadow: inset 2px 0 0 var(--color-accent);
|
||||
}
|
||||
|
||||
.cad-tabs__tab[data-drop="after"] {
|
||||
box-shadow: inset -2px 0 0 var(--color-accent);
|
||||
}
|
||||
|
||||
.cad-tabs__label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cad-tabs__input {
|
||||
width: 110px;
|
||||
padding: 0 4px;
|
||||
border: 1px solid var(--color-accent);
|
||||
border-radius: 4px;
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-text);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.cad-tabs__x,
|
||||
.cad-tabs__add {
|
||||
padding: 0 4px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cad-tabs__x:hover,
|
||||
.cad-tabs__add:hover {
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.cad-tabs__add {
|
||||
align-self: center;
|
||||
margin-left: 2px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.cad-tabs[data-busy="true"] .cad-tabs__tab {
|
||||
cursor: progress;
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/* =============================================================================
|
||||
* ui_template/cad_host/cad_host_tabs.ts
|
||||
* CAD 를 감싸는 공용 탭 줄(PLAN 82-1) — 한 iframe 에 여러 장 · CAD 안 상태는 안 건드림.
|
||||
*
|
||||
* 탭을 바꿀 때 지금 장을 `capture`(= `cad_host.requestSave`)로 받아 그 탭에 두고 다음 장을
|
||||
* `show` 로 싣는다(받아 둔 편집본이 있으면 그것). 저장 길 · 장 내용 꼴은 쓰는 페이지 몫.
|
||||
* 기능(더하기 · 이름 · 지우기 · 끌어 순서 · 닫기)은 쓰는 페이지가 고른다 — M02 구조물 도면 ·
|
||||
* B07 상세 설계(목록에서 연 도면 = 탭 · 닫기만).
|
||||
* ========================================================================== */
|
||||
|
||||
import "./cad_host_tabs.css";
|
||||
import { showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
|
||||
export interface CadTab<D> {
|
||||
id: string;
|
||||
name: string;
|
||||
/** 받아 둔 편집본 — 없으면 `show` 가 페이지 자료에서 받아 옴 */
|
||||
drawing?: D;
|
||||
/** 저장 안 한 고침 — 받아 둘 때 페이지 미저장 표시를 옮김 */
|
||||
dirty?: boolean;
|
||||
/** 고정 — 끌기 · 지우기 · 닫기 · 이름 바꾸기 없음(M02 1장) */
|
||||
fixed?: boolean;
|
||||
/** 풍선 글(장 도번 등) */
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface CadTabsFeatures {
|
||||
add?: boolean;
|
||||
rename?: boolean;
|
||||
remove?: boolean;
|
||||
reorder?: boolean;
|
||||
close?: boolean;
|
||||
}
|
||||
|
||||
export interface CadTabsOptions<D> {
|
||||
features?: CadTabsFeatures;
|
||||
/** 지금 장 편집본 받기(requestSave) · dirty = 페이지 미저장 표시 */
|
||||
capture: () => Promise<{ drawing: D; dirty: boolean }>;
|
||||
/** 장 싣기 — `tab.drawing` 이 있으면 받아 둔 편집본 */
|
||||
show: (tab: CadTab<D>) => void | Promise<void>;
|
||||
/** 떠나도 되나(도각 편집 중 등) — false 면 그대로 */
|
||||
canLeave?: () => boolean;
|
||||
/** [+] — 새 장 · null = 그만 */
|
||||
create?: () => CadTab<D> | null | Promise<CadTab<D> | null>;
|
||||
/** 장 목록이 바뀜(더하기 · 이름 · 지우기 · 순서) — 페이지 미저장 표시 */
|
||||
onChange?: () => void;
|
||||
/** 탭을 닫음(close) — 남은 탭이 없으면 페이지가 정함 */
|
||||
onClosed?: (tab: CadTab<D>) => void;
|
||||
/** 물음 글 */
|
||||
text?: Partial<typeof TEXT>;
|
||||
}
|
||||
|
||||
export interface CadTabs<D> {
|
||||
element: HTMLElement;
|
||||
tabs: () => CadTab<D>[];
|
||||
active: () => CadTab<D> | undefined;
|
||||
/** 탭 목록을 갈아 끼움(싣지 않음) */
|
||||
set: (tabs: CadTab<D>[], activeId: string) => void;
|
||||
/** 그 탭으로(없으면 끝에 더함) — 지금 장은 받아 둠 · 못 떠나면 false */
|
||||
open: (tab: CadTab<D>) => Promise<boolean>;
|
||||
select: (id: string) => Promise<boolean>;
|
||||
/** 지금 장을 받아 두고 모든 탭 */
|
||||
collect: () => Promise<CadTab<D>[]>;
|
||||
/** 저장 뒤 — 받아 둔 편집본 · 미저장 표시를 지움(`tab` 없으면 모두 · `keepDrawings` = 편집본은 둠) */
|
||||
markClean: (tab?: CadTab<D>, keepDrawings?: boolean) => void;
|
||||
/** 지금 탭에 미저장 점(●) — 페이지 편집 통지(참만 · 거짓은 저장 뒤 `markClean`) */
|
||||
markDirty: () => void;
|
||||
/** 미저장 탭이 있나(지금 탭은 페이지 표시로) */
|
||||
anyDirty: () => boolean;
|
||||
}
|
||||
|
||||
const TEXT = {
|
||||
add: "새 장",
|
||||
rename: "두 번 눌러 이름 바꾸기",
|
||||
remove: "장 지우기",
|
||||
close: "닫기",
|
||||
removeAsk: "「{name}」 장을 지울까요? 저장하면 되돌릴 수 없음",
|
||||
removeOk: "지우기",
|
||||
closeAsk: "「{name}」 에 저장 안 한 고침이 있음 — 버리고 닫을까요?",
|
||||
closeOk: "버리고 닫기",
|
||||
emptyName: "장 이름을 넣을 것",
|
||||
sameName: "같은 이름의 장이 있음",
|
||||
};
|
||||
|
||||
export function createCadTabs<D>(options: CadTabsOptions<D>): CadTabs<D> {
|
||||
const features = options.features ?? {};
|
||||
const text = { ...TEXT, ...options.text };
|
||||
const element = document.createElement("div");
|
||||
element.className = "cad-tabs";
|
||||
element.setAttribute("role", "tablist");
|
||||
let list: CadTab<D>[] = [];
|
||||
let activeId = "";
|
||||
let busy = false;
|
||||
let dragId: string | null = null;
|
||||
|
||||
const active = (): CadTab<D> | undefined => list.find((tab) => tab.id === activeId);
|
||||
const changed = (): void => options.onChange?.();
|
||||
|
||||
/** 지금 장을 받아 그 탭에 둠 */
|
||||
const stash = async (): Promise<void> => {
|
||||
const now = active();
|
||||
if (!now) return;
|
||||
const got = await options.capture();
|
||||
now.drawing = got.drawing;
|
||||
now.dirty = !!now.dirty || got.dirty;
|
||||
};
|
||||
|
||||
const go = async (id: string): Promise<boolean> => {
|
||||
const next = list.find((tab) => tab.id === id);
|
||||
if (!next || busy) return false;
|
||||
if (id === activeId) return true;
|
||||
if (options.canLeave && !options.canLeave()) return false;
|
||||
busy = true;
|
||||
try {
|
||||
await stash();
|
||||
activeId = id;
|
||||
render();
|
||||
await options.show(next);
|
||||
return true;
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : String(error), "error");
|
||||
return false;
|
||||
} finally {
|
||||
busy = false;
|
||||
render();
|
||||
}
|
||||
};
|
||||
|
||||
const rename = (tab: CadTab<D>, label: HTMLElement): void => {
|
||||
const input = document.createElement("input");
|
||||
input.className = "cad-tabs__input";
|
||||
input.value = tab.name;
|
||||
input.maxLength = 40;
|
||||
let done = false;
|
||||
const finish = (save: boolean): void => {
|
||||
if (done) return;
|
||||
const name = input.value.trim();
|
||||
if (save && name !== tab.name) {
|
||||
if (!name) return void showToast(text.emptyName, "error");
|
||||
if (list.some((other) => other !== tab && other.name === name))
|
||||
return void showToast(text.sameName, "error");
|
||||
tab.name = name;
|
||||
changed();
|
||||
}
|
||||
done = true;
|
||||
render();
|
||||
};
|
||||
input.addEventListener("keydown", (event) => {
|
||||
event.stopPropagation();
|
||||
if (event.isComposing) return;
|
||||
if (event.key === "Enter") finish(true);
|
||||
else if (event.key === "Escape") finish(false);
|
||||
});
|
||||
input.addEventListener("blur", () => {
|
||||
finish(true);
|
||||
if (!done) finish(false); // 틀린 이름으로 초점을 잃으면 옛 이름 그대로
|
||||
});
|
||||
label.replaceWith(input);
|
||||
input.select();
|
||||
};
|
||||
|
||||
const remove = async (tab: CadTab<D>): Promise<void> => {
|
||||
if (busy) return;
|
||||
const closing = !features.remove;
|
||||
if (closing && tab.id === activeId) {
|
||||
// 닫기 — 지금 탭의 미저장은 페이지 표시로 · 받아 둔 장은 탭에
|
||||
await stash().catch(() => undefined);
|
||||
}
|
||||
const ask = closing
|
||||
? tab.dirty && text.closeAsk.replace("{name}", tab.name)
|
||||
: text.removeAsk.replace("{name}", tab.name);
|
||||
if (ask && !(await showConfirmDialog(ask, closing ? text.closeOk : text.removeOk))) return;
|
||||
const at = list.indexOf(tab);
|
||||
list = list.filter((one) => one !== tab);
|
||||
if (closing) options.onClosed?.(tab);
|
||||
else changed();
|
||||
if (tab.id === activeId) {
|
||||
activeId = "";
|
||||
const next = list[Math.min(at, list.length - 1)];
|
||||
if (next) {
|
||||
activeId = next.id;
|
||||
render();
|
||||
busy = true;
|
||||
try {
|
||||
await options.show(next);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
render();
|
||||
};
|
||||
|
||||
const drop = (target: CadTab<D>, after: boolean): void => {
|
||||
const moving = list.find((tab) => tab.id === dragId);
|
||||
dragId = null;
|
||||
if (!moving || moving === target) return;
|
||||
const rest = list.filter((tab) => tab !== moving);
|
||||
const at = rest.indexOf(target) + (after ? 1 : 0);
|
||||
rest.splice(Math.max(at, rest.filter((tab) => tab.fixed).length), 0, moving); // 고정 탭 앞으로는 못 감
|
||||
if (rest.every((tab, index) => tab === list[index])) return render();
|
||||
list = rest;
|
||||
changed();
|
||||
render();
|
||||
};
|
||||
|
||||
function render(): void {
|
||||
element.dataset.busy = String(busy);
|
||||
element.replaceChildren(
|
||||
...list.map((tab) => {
|
||||
const node = document.createElement("div");
|
||||
node.className = "cad-tabs__tab";
|
||||
node.setAttribute("role", "tab");
|
||||
node.dataset.tabId = tab.id;
|
||||
node.dataset.active = String(tab.id === activeId);
|
||||
node.dataset.dirty = String(!!tab.dirty);
|
||||
node.setAttribute("aria-selected", String(tab.id === activeId));
|
||||
node.title = [tab.title, features.rename && !tab.fixed ? text.rename : ""]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
const label = document.createElement("span");
|
||||
label.className = "cad-tabs__label";
|
||||
label.textContent = tab.name;
|
||||
node.append(label);
|
||||
node.addEventListener("click", () => void go(tab.id));
|
||||
if (features.rename && !tab.fixed) {
|
||||
node.addEventListener("dblclick", (event) => {
|
||||
event.preventDefault();
|
||||
rename(tab, label);
|
||||
});
|
||||
}
|
||||
// 닫기는 마지막 한 탭이면 없음(볼 장이 하나는 남음)
|
||||
if ((features.remove || (features.close && list.length > 1)) && !tab.fixed) {
|
||||
const x = document.createElement("button");
|
||||
x.type = "button";
|
||||
x.className = "cad-tabs__x";
|
||||
x.textContent = "×";
|
||||
x.title = x.ariaLabel = features.remove ? text.remove : text.close;
|
||||
x.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
void remove(tab);
|
||||
});
|
||||
node.append(x);
|
||||
}
|
||||
if (features.reorder && !tab.fixed) {
|
||||
node.draggable = true;
|
||||
node.addEventListener("dragstart", (event) => {
|
||||
dragId = tab.id;
|
||||
event.dataTransfer?.setData("text/plain", tab.id);
|
||||
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
||||
});
|
||||
node.addEventListener("dragend", () => (dragId = null));
|
||||
}
|
||||
if (features.reorder) {
|
||||
node.addEventListener("dragover", (event) => {
|
||||
if (!dragId) return;
|
||||
event.preventDefault();
|
||||
const box = node.getBoundingClientRect();
|
||||
node.dataset.drop = event.clientX > box.left + box.width / 2 ? "after" : "before";
|
||||
});
|
||||
node.addEventListener("dragleave", () => delete node.dataset.drop);
|
||||
node.addEventListener("drop", (event) => {
|
||||
event.preventDefault();
|
||||
const after = node.dataset.drop === "after";
|
||||
delete node.dataset.drop;
|
||||
drop(tab, after);
|
||||
});
|
||||
}
|
||||
return node;
|
||||
}),
|
||||
);
|
||||
if (features.add) {
|
||||
const plus = document.createElement("button");
|
||||
plus.type = "button";
|
||||
plus.className = "cad-tabs__add";
|
||||
plus.textContent = "+";
|
||||
plus.title = plus.ariaLabel = text.add;
|
||||
plus.disabled = busy;
|
||||
plus.addEventListener("click", async () => {
|
||||
if (busy) return;
|
||||
const made = await options.create?.();
|
||||
if (!made) return;
|
||||
list = [...list, made];
|
||||
changed();
|
||||
await go(made.id);
|
||||
});
|
||||
element.append(plus);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
element,
|
||||
tabs: () => list,
|
||||
active,
|
||||
set: (tabs, id) => {
|
||||
list = [...tabs];
|
||||
activeId = id;
|
||||
render();
|
||||
},
|
||||
open: async (tab) => {
|
||||
if (!list.some((one) => one.id === tab.id)) {
|
||||
if (!activeId) {
|
||||
// 첫 탭 — 받아 둘 지금 장이 없음
|
||||
list = [...list, tab];
|
||||
activeId = tab.id;
|
||||
render();
|
||||
await options.show(tab);
|
||||
return true;
|
||||
}
|
||||
if (options.canLeave && !options.canLeave()) return false;
|
||||
list = [...list, tab];
|
||||
if (await go(tab.id)) return true;
|
||||
list = list.filter((one) => one !== tab); // 못 옮겨 감 — 더한 탭도 뺌
|
||||
render();
|
||||
return false;
|
||||
}
|
||||
return go(tab.id);
|
||||
},
|
||||
select: go,
|
||||
collect: async () => {
|
||||
await stash();
|
||||
return list;
|
||||
},
|
||||
markClean: (only, keepDrawings = false) => {
|
||||
for (const tab of only ? [only] : list) {
|
||||
tab.dirty = false;
|
||||
if (!keepDrawings) delete tab.drawing;
|
||||
}
|
||||
render();
|
||||
},
|
||||
markDirty: () => {
|
||||
const now = active();
|
||||
if (!now || now.dirty) return;
|
||||
now.dirty = true;
|
||||
render();
|
||||
},
|
||||
anyDirty: () => list.some((tab) => tab.dirty),
|
||||
};
|
||||
}
|
||||
@@ -64,3 +64,10 @@
|
||||
.ui-ctx-menu__item.is-sub {
|
||||
padding-left: var(--spacing-24);
|
||||
}
|
||||
|
||||
/* 구분선(`"-"` 항목) */
|
||||
.ui-ctx-menu__sep {
|
||||
height: 1px;
|
||||
margin: var(--spacing-4) 0;
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
import "./ui_template_context_menu.css";
|
||||
|
||||
/** 평면 항목 = [문구, 동작]. 하위 메뉴 항목 = {label, children} — 누르면 바로 아래로
|
||||
* 펼쳐지는 아코디언(구조물군 → 종류, 2026-08-18 B05 우클릭 메뉴 일원화). */
|
||||
* 펼쳐지는 아코디언(구조물군 → 종류, 2026-08-18 B05 우클릭 메뉴 일원화).
|
||||
* 구분선 = `"-"` — 맨 앞 · 맨 뒤 · 겹친 것은 안 그림. */
|
||||
export type MapContextMenuItem =
|
||||
| [label: string, action: () => void]
|
||||
| { label: string; children: Array<[label: string, action: () => void]> };
|
||||
| { label: string; children: Array<[label: string, action: () => void]> }
|
||||
| "-";
|
||||
|
||||
export interface MapContextMenu {
|
||||
/** 뷰포트에 append할 요소. */
|
||||
@@ -118,6 +120,14 @@ export function createMapContextMenu(blockClass: string): MapContextMenu {
|
||||
enterMenu(registryEntry);
|
||||
element.replaceChildren();
|
||||
items.forEach((item) => {
|
||||
if (item === "-") {
|
||||
const last = element.lastElementChild;
|
||||
if (last && !last.classList.contains("ui-ctx-menu__sep"))
|
||||
element.append(
|
||||
Object.assign(document.createElement("div"), { className: "ui-ctx-menu__sep" }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(item)) {
|
||||
element.append(buildLeaf(item[0], item[1]));
|
||||
return;
|
||||
@@ -145,6 +155,8 @@ export function createMapContextMenu(blockClass: string): MapContextMenu {
|
||||
});
|
||||
element.append(head, body);
|
||||
});
|
||||
if (element.lastElementChild?.classList.contains("ui-ctx-menu__sep"))
|
||||
element.lastElementChild.remove();
|
||||
element.style.left = `${x}px`;
|
||||
element.style.top = `${y}px`;
|
||||
element.hidden = false;
|
||||
|
||||
@@ -290,10 +290,6 @@ export const ui_locales_b2 = {
|
||||
"Distribute tiers evenly along the slope",
|
||||
],
|
||||
B06_Cross_Extra_Remove: ["한 단 삭제", "Remove one tier"],
|
||||
B06_Cross_Extra_Limit: [
|
||||
"지형상 추가 기슭막이는 {n}단까지만 가능합니다 — 벽이 원지반에 0.5m 이상 묻히면 성토가 필요 없습니다",
|
||||
"Terrain allows only {n} extra revetment tier(s) — no fill needed once a wall is buried 0.5m+",
|
||||
],
|
||||
B06_Cross_Struct_Auto: ["자동(규칙)", "Auto (by rule)"],
|
||||
B06_Cross_Struct_Revet: ["기슭막이+배관", "Revetment + pipe"],
|
||||
B06_Cross_Struct_I: ["집수정 I형", "Catch basin — I"],
|
||||
|
||||
@@ -28,6 +28,21 @@
|
||||
margin-left: calc(var(--spacing-24) + var(--spacing-8));
|
||||
}
|
||||
|
||||
/* follow:false(85-5) — 부르는 칸(고정 높이) 높이를 채움 · 패널 내용이 칸 높이를 늘리지 않음 */
|
||||
.ui-side-panels.is-fill {
|
||||
align-self: stretch;
|
||||
width: var(--ui-side-panels-width, 420px);
|
||||
height: auto;
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
.ui-side-panels.is-fill > .ui-side-panels__panel {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* 모두 접힘 — 폭 0 · 손잡이만 왼쪽 밖에 남음 */
|
||||
.ui-side-panels.is-collapsed {
|
||||
width: 0;
|
||||
@@ -113,4 +128,5 @@
|
||||
min-height: 0;
|
||||
padding: var(--spacing-12);
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain; /* 몸 끝에서 메인으로 이어 굴리지 않음(85-5) */
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* 좌측 패널과 같은 접기 양식 — 패널 왼쪽 가장자리 손잡이(공용 createWorkflowPanelHandle · 오른쪽으로 접힘)
|
||||
* 패널마다 손잡이 하나(이름표 세로) · 누르면 폄/접음 · 모두 접히면 손잡이만 남음
|
||||
* 편 패널 = 머리(제목) + 몸 · 왼쪽 경계 끌기(화살표 키) · 보이는 칸 안에 붙어 몸만 따로 스크롤(followView)
|
||||
* `follow: false`(85-5) = followView 안 씀 · 부르는 칸(고정 높이) 높이를 채움 · 메인 스크롤과 따로(M02 상세)
|
||||
* 열린 패널 · 폭 = 부르는 쪽 취향 키(`aislo:pref:{prefKey}`)
|
||||
* 부르기: const sp = createSidePanels({ prefKey, panels: [{ id, title, body }] }) →
|
||||
* 본문과 함께 `.ui-side-panels-row` 칸에 [본문, sp.root] 로 넣음
|
||||
@@ -42,6 +43,8 @@ export interface SidePanelsOpts {
|
||||
onChange?(open: string | null): void;
|
||||
/** 폭이 바뀜(끌기 · 화살표) */
|
||||
onResize?(px: number): void;
|
||||
/** 보이는 칸 따라 붙음(followView) · 기본 참 · 거짓 = 부르는 칸 높이를 채움(칸이 고정 높이 · 85-5) */
|
||||
follow?: boolean;
|
||||
}
|
||||
|
||||
export interface SidePanels {
|
||||
@@ -166,7 +169,8 @@ export function createSidePanels(opts: SidePanelsOpts): SidePanels {
|
||||
}
|
||||
|
||||
draw();
|
||||
followView(root);
|
||||
if (opts.follow === false) root.classList.add("is-fill");
|
||||
else followView(root);
|
||||
return { root, open, current: () => now, width: () => width };
|
||||
}
|
||||
|
||||
|
||||
@@ -510,3 +510,20 @@ a:hover {
|
||||
outline: 2px solid var(--color-focus-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ─── B · M 화면 전역 글 선택 막음(81-1) ───────────────────────────────────
|
||||
`html.is-bm-route` 는 main.ts 가 라우트마다 토글(A 영역은 안 붙음).
|
||||
허용 = input · textarea · select · [contenteditable] · 원문 본문(m01lab-src__body). */
|
||||
html.is-bm-route {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
html.is-bm-route input,
|
||||
html.is-bm-route textarea,
|
||||
html.is-bm-route select,
|
||||
html.is-bm-route [contenteditable],
|
||||
html.is-bm-route .m01lab-src__body,
|
||||
html.is-bm-route .m01lab-src__body * {
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user