Merge remote-tracking branch 'origin/sub_laptop_4' into sub_laptop_2

This commit is contained in:
2026-10-09 19:05:57 +09:00
19 changed files with 198 additions and 156 deletions
+3 -10
View File
@@ -1,13 +1,5 @@
/* 항목 고르기 모달 — 왼쪽 나무 · 오른쪽 폴더 모양 · 아래 단추 */
/* 공용 모달(.ui-modal)의 폭 · 높이보다 앞서게 — 불러오는 차례에 따라 뒤집히지 않도록 두 클래스 */
.ui-modal.ui-item-picker {
width: min(1320px, 96vw);
--modal-max: 94vh;
}
.ui-item-picker .ui-modal__body {
height: min(80vh, 800px);
}
/* 항목 고르기 모달 — 왼쪽 나무 · 오른쪽 폴더 모양 · 머리 단추(147-2) */
/* 크기 = 공용 큰 창(곁값 wide · 96vw × 94vh · 147-2) */
.ui-item-picker__cols {
display: grid;
@@ -51,6 +43,7 @@
.ui-item-picker__cols[hidden],
.ui-item-picker__actions[hidden],
.ui-item-picker .ui-modal__actions[hidden],
.ui-item-picker__edit-host[hidden] {
display: none;
}
+13 -12
View File
@@ -103,7 +103,6 @@ const NEW_TEXT = {
name: "이름",
unit: "단위",
ok: "만들기",
cancel: "취소",
empty: "{label} 을(를) 넣을 것",
made: "새 항목을 만들어 지정했음 — 구조물 [저장]을 눌러야 이 구조물 복사본이 남음",
failed: "새 항목을 만들지 못함 — {value}",
@@ -289,7 +288,6 @@ export function openItemPicker(opts: ItemPickerOptions): Promise<ItemPickerResul
{ key: "unit", label: NEW_TEXT.unit, placeholder: "m · ㎥ · 개소", required: true },
],
okLabel: NEW_TEXT.ok,
cancelLabel: NEW_TEXT.cancel,
closeLabel: t("M02_Close"),
emptyText: NEW_TEXT.empty,
});
@@ -308,17 +306,16 @@ export function openItemPicker(opts: ItemPickerOptions): Promise<ItemPickerResul
}
}
// 단추 넷 = 머리 [닫기] 왼쪽(147-2) · 안내 글은 아래 남김
const headButtons = el("div", {
className: "ui-modal__actions",
children: [copyButton, manageButton, clearButton, applyButton],
});
const actions = el("div", {
className: "ui-item-picker__actions",
children: [
...(opts.projectId
? [el("span", { className: "ui-item-picker__saveline", text: NEW_TEXT.saveLine })]
: []),
copyButton,
manageButton,
clearButton,
applyButton,
],
children: opts.projectId
? [el("span", { className: "ui-item-picker__saveline", text: NEW_TEXT.saveLine })]
: [],
});
/** 카드 [수정] — 모달 안에서 편집 화면으로 바뀜(나무 · 카드 · 단추는 숨김) */
@@ -328,6 +325,7 @@ export function openItemPicker(opts: ItemPickerOptions): Promise<ItemPickerResul
editHost.replaceChildren(view.root);
cols.hidden = true;
actions.hidden = true;
headButtons.hidden = true;
editHost.hidden = false;
view.start();
}
@@ -338,6 +336,7 @@ export function openItemPicker(opts: ItemPickerOptions): Promise<ItemPickerResul
editHost.hidden = true;
cols.hidden = false;
actions.hidden = false;
headButtons.hidden = false;
if (saved) detailView.markEdited();
detailView.reload();
}
@@ -346,6 +345,8 @@ export function openItemPicker(opts: ItemPickerOptions): Promise<ItemPickerResul
title: t("M02_PickerTitle"),
closeLabel: t("M02_Close"),
dialogClass: "ui-item-picker",
wide: true,
actions: [headButtons],
// 닫기 단추 · Esc · 바깥 누르기 = 취소 — 편집 중 저장 안 한 고침이 있으면 물음
beforeClose: async () => {
if (editView && !(await editView.leave())) return false;
@@ -354,7 +355,7 @@ export function openItemPicker(opts: ItemPickerOptions): Promise<ItemPickerResul
return true;
},
mount: (body) => {
body.append(cols, editHost, actions);
body.append(cols, editHost, ...(opts.projectId ? [actions] : []));
paintDetail();
void load(null);
},
+4 -17
View File
@@ -1,7 +1,8 @@
/* =============================================================================
* ui_template_item_picker_ask.ts
* 고르기 모달에서 글을 물을 때 쓰는 작은 입력 창(계획서 26-3b) — [개인에 저장](경로 · 이름) ·
* 나무 오른쪽 클릭 「새 항목」(이름 · 단위). 확인 = 넣은 글들 · 취소 · 닫기 = null.
* 나무 오른쪽 클릭 「새 항목」(이름 · 단위). 확인 = 넣은 글들 · 닫기 = null.
* [확인] = 머리 [닫기] 왼쪽 · [취소] 없음(머리 [닫기] = 취소 · 147-2).
* ========================================================================== */
import { createButton, createInputField, el } from "./ui_template_elements";
@@ -22,7 +23,6 @@ export interface AskOptions {
note?: string;
fields: AskField[];
okLabel: string;
cancelLabel: string;
closeLabel: string;
/** 빈 값 알림 — `{label}` 자리에 칸 이름 */
emptyText: string;
@@ -45,7 +45,7 @@ export function askFields(opts: AskOptions): Promise<Record<string, string> | nu
finish(null);
return true;
},
mount: (body, close) => {
mount: (body, close, head) => {
const inputs = opts.fields.map((field) => ({
field,
input: createInputField({
@@ -76,21 +76,8 @@ export function askFields(opts: AskOptions): Promise<Record<string, string> | nu
body.append(
...(opts.note ? [el("p", { className: "ui-item-picker__where", text: opts.note })] : []),
...inputs.map(({ input }) => input.root),
el("div", {
className: "ui-item-picker__actions",
children: [
createButton({
label: opts.cancelLabel,
variant: "ghost",
onClick: () => {
finish(null);
close();
},
}),
createButton({ label: opts.okLabel, variant: "filled", onClick: submit }),
],
}),
);
head.append(createButton({ label: opts.okLabel, variant: "filled", onClick: submit }));
},
});
});
@@ -98,7 +98,6 @@ const TEXT = {
saved: "개인 층에 저장했음",
saveFailed: "개인에 저장하지 못함 — {value}",
ok: "확인",
cancel: "취소",
close: "닫기",
empty: "{label} 을(를) 넣을 것",
importFailed: "이 구조물로 가져오지 못함 — {value}",
@@ -267,7 +266,6 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
{ key: "name", label: TEXT.nameLabel, value: itemName(row), required: true },
],
okLabel: TEXT.ok,
cancelLabel: TEXT.cancel,
closeLabel: TEXT.close,
emptyText: TEXT.empty,
});
+26
View File
@@ -33,6 +33,32 @@
gap: var(--spacing-8);
}
/* 머리 단추 자리(곁값 actions) — 제목이 남는 폭을 먹고 단추는 [닫기] 바로 왼쪽 */
.ui-modal__top > h3 {
margin-right: auto;
}
.ui-modal__actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: var(--spacing-8);
}
/* 큰 창(곁값 wide) — 화면 거의 꽉 · 몸 넘침만 스크롤. 화면별 클래스보다 앞서게 두 클래스 */
.ui-modal.ui-modal--wide {
width: 96vw;
height: 94vh;
--modal-max: 94vh;
overflow: hidden;
}
.ui-modal--wide > .ui-modal__body {
flex: 1;
min-height: 0;
overflow: auto;
}
.ui-modal__body {
display: flex;
flex-direction: column;
+23 -13
View File
@@ -2,6 +2,8 @@
* ui_template_modal.ts
* 공용 모달 겉틀 — 배경 · 제목줄 · 닫기 · Esc · 포커스. 몸은 `mount` 가 채움.
* M01 · M02 가 함께 씀 — `dialogClass` · `backdropClass` 로 화면별 겉모양을 덧붙임.
* 곁값 — `actions`(머리 [닫기] 바로 왼쪽 단추 · mount 셋째 인자 자리에 넣어도 됨) · `wide`(큰 창 96vw × 94vh).
* 안 넘긴 창은 지금 꼴 그대로.
* ========================================================================== */
import "./ui_template_modal.css";
@@ -10,8 +12,13 @@ import { createButton, el } from "./ui_template_elements";
export interface ModalOptions {
title: string;
closeLabel: string;
mount: (body: HTMLElement, close: () => void) => void;
/** `actions` = 머리 단추 자리 — 몸에서 만든 단추([확인] 등)를 넣음 */
mount: (body: HTMLElement, close: () => void, actions: HTMLElement) => void;
dialogClass?: string;
/** 머리 [닫기] 바로 왼쪽에 둘 단추(넘긴 창만) */
actions?: HTMLElement[];
/** 큰 창 — 96vw × 94vh · 몸 넘침만 스크롤 */
wide?: boolean;
backdropClass?: string;
/** 닫기 단추 · Esc · 바깥 누르기 전에 물음 — false 면 안 닫음(몸의 `close` 는 묻지 않고 닫음) */
beforeClose?: () => boolean | Promise<boolean>;
@@ -19,23 +26,24 @@ export interface ModalOptions {
export function openModal(opts: ModalOptions): { close: () => void } {
const body = el("div", { className: "ui-modal__body" });
const actions = el("div", { className: "ui-modal__actions", children: opts.actions ?? [] });
const closeButton = createButton({
label: opts.closeLabel,
variant: "ghost",
onClick: () => void ask(),
});
const top = el("div", {
className: "ui-modal__top",
children: [el("h3", { text: opts.title }), closeButton],
});
const close = (): void => backdrop.remove();
const ask = async (): Promise<void> => {
if (!opts.beforeClose || (await opts.beforeClose())) close();
};
const dialog = el("div", {
className: `ui-modal ${opts.dialogClass ?? ""}`.trim(),
className: `ui-modal ${opts.wide ? "ui-modal--wide " : ""}${opts.dialogClass ?? ""}`.trim(),
attrs: { role: "dialog", "aria-label": opts.title },
children: [
el("div", {
className: "ui-modal__top",
children: [
el("h3", { text: opts.title }),
createButton({ label: opts.closeLabel, variant: "ghost", onClick: () => void ask() }),
],
}),
body,
],
children: [top, body],
});
const backdrop = el("div", {
className: `ui-modal__backdrop ${opts.backdropClass ?? ""}`.trim(),
@@ -43,7 +51,9 @@ export function openModal(opts: ModalOptions): { close: () => void } {
});
backdrop.addEventListener("click", (ev) => ev.target === backdrop && void ask());
backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && void ask());
opts.mount(body, close);
opts.mount(body, close, actions);
// 단추가 있을 때만 머리에 — [닫기] 는 머리 직속 그대로(B05 추천의 `.ui-modal__top > button` 갈아 끼우기)
if (actions.childElementCount) closeButton.before(actions);
document.body.append(backdrop);
dialog.tabIndex = -1;
// 몸의 첫 입력 칸에 커서 — mount 안의 focus() 는 붙기 전이라 안 먹음