Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
/* 떠 있는 판(552-4) — 흰 판 · 1 px 선 · 그림자 · 머리 32 · 뒷막 없음 · 자리는 부르는 쪽(부모 안 absolute) */
|
||||
.ui-float-panel {
|
||||
position: absolute;
|
||||
z-index: var(--z-dropdown);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 160px;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-text);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.ui-float-panel__head {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
min-height: 32px;
|
||||
padding: 0 var(--spacing-4) 0 var(--spacing-12);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.ui-float-panel__head.is-draggable {
|
||||
cursor: move;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ui-float-panel__title {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-size: var(--text-body-sm);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui-float-panel__body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding: var(--spacing-12);
|
||||
overflow: auto;
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.ui-float-panel--popover > .ui-float-panel__body {
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.ui-float-panel.is-dragging {
|
||||
opacity: 0.92;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/* =============================================================================
|
||||
* ui_template_float_panel.ts
|
||||
* 떠 있는 판 `float-panel`(552-4 틀) — 지도 · 그림 위에 뜨는 작은 판: 머리(제목 · 닫기 ✕) + 몸 · 뒷막 없음.
|
||||
* 꼴 = 판(머리 + 몸) · 끌기 머리(draggable · 부모 안에서 옮김) · 팝오버(popover · 머리 없음 · 몸만)
|
||||
* 크기 = 머리 data-size 32 · 닫기 = 아이콘 단추 24 · 폭 = width(기본 내용 폭)
|
||||
* 상태 = 열림 · 닫힘(hidden) · 끄는 중(`is-dragging`)
|
||||
* 쓰는 법 = const fp = createFloatPanel({ title: "측정", onClose: () => fp.root.remove() }); fp.body.append(table); map.append(fp.root)
|
||||
* 대신하는 옛 변종 = B05 `b05-routeedit__hint/__info/__measure` · `b05-crossmodal` · `masshaul-overlay` · B06 `b06-structure-panel` ·
|
||||
* B08 `b08-milestone__tools` · B09 `.b09-popup` · CAD `.cad-quick-properties` · E03 `e03-view3d` · M02 `m02-pop` · D03 `d03-plan__cross` …
|
||||
* ========================================================================== */
|
||||
|
||||
import { createIconButton } from "./ui_template_button";
|
||||
import "./ui_template_float_panel.css";
|
||||
|
||||
export interface FloatPanelOptions {
|
||||
title?: string;
|
||||
/** 있으면 머리에 닫기 ✕ */
|
||||
onClose?: () => void;
|
||||
/** 머리를 잡아 부모 안에서 옮김 */
|
||||
draggable?: boolean;
|
||||
/** 머리 없는 팝오버 */
|
||||
popover?: boolean;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export interface FloatPanel {
|
||||
root: HTMLElement;
|
||||
body: HTMLElement;
|
||||
}
|
||||
|
||||
/** 머리 끌기 — 부모(위치 잡힌 칸) 안에서 left · top 만 바꿈 */
|
||||
function dragBy(root: HTMLElement, head: HTMLElement): void {
|
||||
head.addEventListener("pointerdown", (down) => {
|
||||
if ((down.target as HTMLElement).closest("button")) return;
|
||||
const x0 = down.clientX - root.offsetLeft;
|
||||
const y0 = down.clientY - root.offsetTop;
|
||||
head.setPointerCapture(down.pointerId);
|
||||
root.classList.add("is-dragging");
|
||||
const move = (ev: PointerEvent): void => {
|
||||
root.style.left = `${Math.max(0, ev.clientX - x0)}px`;
|
||||
root.style.top = `${Math.max(0, ev.clientY - y0)}px`;
|
||||
};
|
||||
const up = (): void => {
|
||||
root.classList.remove("is-dragging");
|
||||
head.removeEventListener("pointermove", move);
|
||||
};
|
||||
head.addEventListener("pointermove", move);
|
||||
head.addEventListener("pointerup", up, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
export function createFloatPanel(opts: FloatPanelOptions): FloatPanel {
|
||||
const root = document.createElement("section");
|
||||
root.className = `ui-float-panel${opts.popover ? " ui-float-panel--popover" : ""}`;
|
||||
if (opts.width) root.style.width = `${opts.width}px`;
|
||||
if (!opts.popover) {
|
||||
const head = document.createElement("header");
|
||||
head.className = "ui-float-panel__head";
|
||||
head.dataset.size = "32";
|
||||
const title = document.createElement("strong");
|
||||
title.className = "ui-float-panel__title";
|
||||
title.textContent = opts.title ?? "";
|
||||
head.append(title);
|
||||
if (opts.onClose)
|
||||
head.append(
|
||||
createIconButton({ icon: "x", title: "닫기", size: 24, onClick: () => opts.onClose?.() }),
|
||||
);
|
||||
if (opts.draggable) {
|
||||
head.classList.add("is-draggable");
|
||||
dragBy(root, head);
|
||||
}
|
||||
root.append(head);
|
||||
if (opts.title) root.setAttribute("aria-label", opts.title);
|
||||
}
|
||||
const body = document.createElement("div");
|
||||
body.className = "ui-float-panel__body";
|
||||
root.append(body);
|
||||
return { root, body };
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
/* 본문 오른쪽 접는 패널 여럿 — 왼쪽 가장자리 손잡이(좌측 패널과 같은 꼴) · 편 패널 = 공유 폭(--ui-side-panels-width)
|
||||
/* 오른쪽 서랍(552-4) — 흰 판 + 1 px 선 · 겉만 공통 값 · 자리 · 폭 · 열고 닫기 그대로
|
||||
본문 오른쪽 접는 패널 여럿 — 왼쪽 가장자리 손잡이(좌측 패널과 같은 꼴) · 편 패널 = 공유 폭(--ui-side-panels-width)
|
||||
보이는 칸 안에 붙음(JS followView) — 몸만 따로 스크롤 · 메인 스크롤과 따로 */
|
||||
|
||||
/* 부르는 쪽 칸 — [본문, 패널 묶음] */
|
||||
@@ -82,7 +83,7 @@
|
||||
height: 100%;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface);
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.ui-side-panels__panel[hidden] {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/* =============================================================================
|
||||
* ui_template_side_panels.ts
|
||||
* 본문 오른쪽 접는 패널 여럿 — 나란히 · 하나만 열림 · 모두 접힘 됨 · 폭 공유
|
||||
* 좌측 패널과 같은 접기 양식 — 패널 왼쪽 가장자리 손잡이(공용 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] 로 넣음
|
||||
* 오른쪽 서랍 `drawer`(552-4 틀) — 본문 오른쪽 접는 패널 여럿 · 나란히 · 하나만 열림 · 모두 접힘 됨 · 폭 공유
|
||||
* 꼴 = 패널마다 손잡이 하나(왼쪽 가장자리 · 이름표 세로 · 공용 createWorkflowPanelHandle · 오른쪽으로 접힘)
|
||||
* 편 패널 = 머리(제목 · 1 px 밑줄) + 몸 · 흰 판 + 1 px 선(그림자 없음) · 왼쪽 경계 끌기(화살표 키)
|
||||
* 보이는 칸 안에 붙어 몸만 따로 스크롤(followView) · `follow: false`(85-5) = 부르는 칸(고정 높이)을 채움(M02 상세)
|
||||
* 크기 = 폭 기본 420 · 가장 좁게 280 · 가장 넓게 화면의 85% · 폭 840 미만 = 본문 아래로 쌓음
|
||||
* 상태 = 열림 · 접힘(`is-collapsed`) · 끄는 중(`is-resizing`) — 열린 패널 · 폭 = 부르는 쪽 취향 키(`aislo:pref:{prefKey}`)
|
||||
* 쓰는 법 = const sp = createSidePanels({ prefKey, panels: [{ id, title, body }] }) →
|
||||
* 본문과 함께 `.ui-side-panels-row` 칸에 [본문, sp.root] 로 넣음
|
||||
* 대신하는 옛 변종 = M02 `m02-dfp`(옆 패널) · B01 채팅 판 `.b01-knowledge-chat__panel`
|
||||
* ========================================================================== */
|
||||
|
||||
import { readByKey, writeByKey } from "../A00_Common/b_page_state";
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/* 도구줄(552-4) — 한 줄 · 사이 = --btn-group-gap · 높이 = data-size 단 · 유리 꼴은 그림 위 */
|
||||
.ui-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--btn-group-gap);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ui-toolbar[data-size="24"] {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.ui-toolbar[data-size="28"] {
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.ui-toolbar[data-size="32"] {
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.ui-toolbar__divider {
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
width: 1px;
|
||||
margin: var(--spacing-4) var(--spacing-4);
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.ui-toolbar__spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.ui-toolbar--glass {
|
||||
padding: var(--spacing-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-buttons);
|
||||
background: color-mix(in srgb, var(--color-surface-raised) 88%, transparent);
|
||||
box-shadow: var(--shadow-sm);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/* =============================================================================
|
||||
* ui_template_toolbar.ts
|
||||
* 도구줄 `toolbar`(552-4 틀) — 단추 · 찾기 · 고르기를 한 줄에 묶음 · 나눔선 · 오른쪽 끝 밀기.
|
||||
* 꼴 = 보통(바탕 없음) · 유리(glass · 그림 위에 뜸) · 나눔선("divider") · 빈 칸 밀기("spacer")
|
||||
* 크기 = data-size 24 · 28 · 32(기본 28) — 크기 단이 없는 단추 · 아이콘 단추 · 입력 · 고르기에 같은 단을 붙임
|
||||
* 상태 = 줄 자체는 없음(안 단추가 제 상태를 가짐)
|
||||
* 쓰는 법 = createToolbar({ items: [createIconButton({ icon: "search", title: "찾기" }), "divider", "spacer", saveBtn] })
|
||||
* 대신하는 옛 변종 = `.ui-btn-group`(붙은 묶음은 그대로 씀) · `.ui-group-cards__bar` · `b05-routeedit__tools` ·
|
||||
* `b04-map__controls` · `b08-hydro__toolbar` · `b09-estimation__tools` · `m01-logic__bar` · M02 `*__bar` · `*__tools` 약 20
|
||||
* ========================================================================== */
|
||||
|
||||
import { setUiSize } from "./ui_template_button";
|
||||
import "./ui_template_toolbar.css";
|
||||
|
||||
export type ToolbarSize = 24 | 28 | 32;
|
||||
export type ToolbarItem = HTMLElement | "divider" | "spacer";
|
||||
|
||||
export interface ToolbarOptions {
|
||||
items: ToolbarItem[];
|
||||
size?: ToolbarSize;
|
||||
/** 그림 위에 뜨는 유리 꼴 */
|
||||
glass?: boolean;
|
||||
/** 보조기기 이름 */
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const SIZED = ".ui-btn, .ui-icon-btn, .ui-input, .ui-select";
|
||||
|
||||
export function createToolbar(opts: ToolbarOptions): HTMLElement {
|
||||
const size = opts.size ?? 28;
|
||||
const bar = document.createElement("div");
|
||||
bar.className = `ui-toolbar${opts.glass ? " ui-toolbar--glass" : ""}`;
|
||||
bar.setAttribute("role", "toolbar");
|
||||
if (opts.label) bar.setAttribute("aria-label", opts.label);
|
||||
bar.dataset.size = String(size);
|
||||
for (const item of opts.items) {
|
||||
if (typeof item === "string") {
|
||||
const gap = document.createElement("span");
|
||||
gap.className = `ui-toolbar__${item}`;
|
||||
if (item === "divider") gap.setAttribute("role", "separator");
|
||||
bar.append(gap);
|
||||
continue;
|
||||
}
|
||||
if (!item.dataset.size && item.matches(SIZED)) setUiSize(item, size);
|
||||
bar.append(item);
|
||||
}
|
||||
return bar;
|
||||
}
|
||||
@@ -4,12 +4,16 @@
|
||||
* 견본 = 진짜 부품 함수 · 클래스로 그림 · 화면에 붙는(fixed) 틀은 칸 안에 놓이게 자리만 인라인으로 풂.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createIconButton } from "./ui_template_button";
|
||||
import { createButton } from "./ui_template_elements";
|
||||
import { createFloatPanel } from "./ui_template_float_panel";
|
||||
import { createPageFrame } from "./ui_template_general_layout";
|
||||
import { createListRow } from "./ui_template_list_row";
|
||||
import { createWorkflowPanelHandle } from "./ui_template_overlay";
|
||||
import { createPanelHead } from "./ui_template_panel_head";
|
||||
import { createPublicFrame } from "./ui_template_public_layout";
|
||||
import { createSidePanels } from "./ui_template_side_panels";
|
||||
import { createToolbar } from "./ui_template_toolbar";
|
||||
import type { KitState } from "./ui_template_ui_kit_parts";
|
||||
import { createStepBar } from "./ui_template_workflow_layout";
|
||||
|
||||
@@ -150,11 +154,64 @@ function listRow(state: KitState): HTMLElement | null {
|
||||
return box;
|
||||
}
|
||||
|
||||
function floatPanel(): HTMLElement {
|
||||
const place = (root: HTMLElement): HTMLElement => {
|
||||
root.style.position = "relative";
|
||||
return root;
|
||||
};
|
||||
const one = createFloatPanel({ title: "측정", onClose: noop, draggable: true, width: 220 });
|
||||
one.body.append("거리 124.5 m · 기울기 8.2 %");
|
||||
const pop = createFloatPanel({ popover: true, width: 160 });
|
||||
pop.body.append("팝오버 — 머리 없음");
|
||||
return wrap(place(one.root), place(pop.root));
|
||||
}
|
||||
|
||||
function drawer(): HTMLElement {
|
||||
const row = div("ui-side-panels-row");
|
||||
row.style.cssText = "height:220px;width:100%";
|
||||
const main = div("", "본문");
|
||||
main.style.padding = "12px";
|
||||
const body = (text: string): HTMLElement => div("", text);
|
||||
const sp = createSidePanels({
|
||||
prefKey: "ui-kit-demo",
|
||||
follow: false,
|
||||
defaultWidth: 280,
|
||||
panels: [
|
||||
{ id: "info", title: "상세", body: body("고른 줄의 값") },
|
||||
{ id: "set", title: "설정", body: body("보기 설정") },
|
||||
],
|
||||
});
|
||||
row.append(main, sp.root);
|
||||
return row;
|
||||
}
|
||||
|
||||
function toolbar(): HTMLElement {
|
||||
const box = div("");
|
||||
box.style.cssText = "display:flex;flex-direction:column;gap:12px;width:100%";
|
||||
const items = () => [
|
||||
createIconButton({ icon: "search", title: "찾기", onClick: noop }),
|
||||
createIconButton({ icon: "adjustments", title: "보기", onClick: noop }),
|
||||
"divider" as const,
|
||||
createButton({ label: "더함", variant: "ghost", onClick: noop }),
|
||||
"spacer" as const,
|
||||
createButton({ label: "저장", onClick: noop }),
|
||||
];
|
||||
box.append(
|
||||
createToolbar({ items: items(), size: 24 }),
|
||||
createToolbar({ items: items() }),
|
||||
createToolbar({ items: items().slice(0, 3), size: 32, glass: true }),
|
||||
);
|
||||
return box;
|
||||
}
|
||||
|
||||
/** 열쇠 → 견본(상태별 · null = 그 상태 없음) */
|
||||
export const FRAME_DEMOS: Record<string, (state: KitState) => HTMLElement | null> = {
|
||||
"work-frame": (s) => (s === "default" ? workFrame() : null),
|
||||
"page-frame": (s) => (s === "default" ? pageFrame() : null),
|
||||
"public-frame": (s) => (s === "default" ? publicFrame() : null),
|
||||
"float-panel": (s) => (s === "default" ? floatPanel() : null),
|
||||
drawer: (s) => (s === "default" ? drawer() : null),
|
||||
toolbar: (s) => (s === "default" ? toolbar() : null),
|
||||
steps,
|
||||
"panel-head": (s) => (s === "default" ? panelHead() : null),
|
||||
"list-row": listRow,
|
||||
|
||||
@@ -176,9 +176,12 @@ export const KIT_PARTS: KitPart[] = [
|
||||
demo: FRAME_DEMOS["page-frame"] },
|
||||
{ key: "public-frame", name: "공개 화면 틀", group: "frame", kinds: "머리 띠(제목 · 설명 · 주 단추) · 구역 · 본문", file: "ui_template_public_layout.ts",
|
||||
demo: FRAME_DEMOS["public-frame"] },
|
||||
{ key: "float-panel", name: "떠 있는 판", group: "frame", kinds: "그림 위 판 · 끌기 머리 · 닫기 · 팝오버", file: "ui_template_float_panel.ts" },
|
||||
{ key: "drawer", name: "오른쪽 서랍", group: "frame", kinds: "상세 · 설정", file: "ui_template_side_panels.ts" },
|
||||
{ key: "toolbar", name: "도구줄", group: "frame", kinds: "단추 묶음 · 나눔선 · 찾기 · 고르기", file: "ui_template_toolbar.ts" },
|
||||
{ key: "float-panel", name: "떠 있는 판", group: "frame", kinds: "그림 위 판 · 끌기 머리 · 닫기 ✕ · 팝오버", file: "ui_template_float_panel.ts",
|
||||
demo: FRAME_DEMOS["float-panel"] },
|
||||
{ key: "drawer", name: "오른쪽 서랍", group: "frame", kinds: "상세 · 설정 · 폭 끌기 · 흰 판", file: "ui_template_side_panels.ts",
|
||||
demo: FRAME_DEMOS["drawer"] },
|
||||
{ key: "toolbar", name: "도구줄", group: "frame", kinds: "단추 묶음 · 나눔선 · 밀기 · 유리 · 24 · 28 · 32", file: "ui_template_toolbar.ts",
|
||||
demo: FRAME_DEMOS["toolbar"] },
|
||||
{ key: "steps", name: "단계 줄", group: "frame", kinds: "가로 막대 · compact(28) · 세로 목록(40 · ☰ 서랍)", file: "ui_template_workflow_layout.ts",
|
||||
demo: FRAME_DEMOS.steps },
|
||||
{ key: "panel-head", name: "패널 머리", group: "frame", kinds: "제목 · 부제 · 오른쪽 단추 · 밑줄", file: "ui_template_panel_head.ts",
|
||||
|
||||
Reference in New Issue
Block a user