Merge remote-tracking branch 'origin/dev' into sub_laptop_6

# Conflicts:
#	A00_Common/spreadsheet/spreadsheet_grid.ts
#	A00_Common/spreadsheet/spreadsheet_links.ts
This commit is contained in:
2026-09-28 22:59:00 +09:00
63 changed files with 1547 additions and 1012 deletions
+1 -1
View File
@@ -23,7 +23,7 @@
.ui-sheet__hint {
margin: 0;
color: var(--color-text-muted);
color: var(--color-text-secondary); /* muted 는 작은 글에 대비 3:1 — 모자람 */
font-size: 0.8rem;
}
+4 -1
View File
@@ -23,6 +23,8 @@ import {
deleteColumn,
deleteRow,
isBlankPick,
isBoundColumn,
isCalcColumn,
KEY_PRICE,
KEY_UNIT,
navRows,
@@ -168,7 +170,8 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio
const col = state.sel ? colOf(state.sel.c) : undefined;
if (opts.mode === "master") syncLevels(!!col);
colDelete.disabled = !col || state.doc.열.length <= 1;
colHand.disabled = !col;
// 바인딩 · 계산 열은 손 입력이 안 먹음(프로젝트에서 잠김) — 켜지 못하게 · 이미 켜진 것은 끌 수 있게
colHand.disabled = !col || (!col.손 && (isBoundColumn(col) || isCalcColumn(col)));
colHand.classList.toggle("is-on", !!col?.손);
};
+13 -2
View File
@@ -72,6 +72,8 @@ export interface InputFieldOptions {
min?: number;
max?: number;
onInput?: (value: string) => void;
/** 좁은 자리(표 칸·좌측 패널)용 작은 크기 — 작은 고르기칸(compact)과 높이가 같음 */
compact?: boolean;
}
export interface InputFieldHandle {
@@ -90,7 +92,7 @@ export function createInputField(opts: InputFieldOptions): InputFieldHandle {
}
const input = el("input", {
className: "ui-input",
className: opts.compact ? "ui-input ui-input--sm" : "ui-input",
attrs: {
type: opts.type ?? "text",
...(opts.placeholder ? { placeholder: opts.placeholder } : {}),
@@ -318,17 +320,26 @@ export function showConfirmDialog(message: string, confirmLabel = "확인"): Pro
const panel = el("div", { className: "ui-confirm__panel" });
const text = el("p", { className: "ui-confirm__message", text: message });
const actions = el("div", { className: "ui-confirm__actions" });
const back = document.activeElement as HTMLElement | null; // 닫힐 때 초점을 돌려놓을 자리
const done = (result: boolean): void => {
overlay.remove();
if (back?.isConnected && back !== document.body) back.focus({ preventScroll: true });
resolve(result);
};
const cancel = createButton({ label: "취소", variant: "ghost", onClick: () => done(false) });
actions.append(
createButton({ label: "취소", variant: "ghost", onClick: () => done(false) }),
cancel,
createButton({ label: confirmLabel, variant: "danger", onClick: () => done(true) }),
);
overlay.addEventListener("keydown", (ev) => {
if (ev.key !== "Escape") return;
ev.stopPropagation(); // 뒤 창 · 페이지 Esc 까지 닫지 않음
done(false);
});
panel.append(text, actions);
overlay.append(panel);
document.body.append(overlay);
cancel.focus(); // 창에 초점 — 뒤 칸에 글이 들어가지 않고 Esc 가 먹음
});
}
@@ -99,6 +99,10 @@ const BASE_CSS = `
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.ui-input::placeholder { color: var(--color-text-muted); }
/* 입력칸 · 고르기칸 높이는 크기마다 한 값 — 같은 줄에 놓여도 위아래 · 글자 줄이 맞음(date 칸 포함) */
.ui-input:not(textarea), .ui-select { height: var(--control-height); line-height: 20px; }
.ui-input--sm:not(textarea), .ui-select--sm { height: var(--control-height-sm); line-height: 20px; }
.ui-input--sm { padding: 4px 8px; }
.ui-input:focus {
outline: none;
border-color: var(--color-focus-ring);
+4
View File
@@ -23,6 +23,10 @@ export const ui_locales_m2 = {
M02_Close: ["닫기", "Close"],
M02_NameNeeded: ["이름을 적을 것", "Enter a name"],
M02_DeleteConfirm: ["「{value}」 양식을 지울까?", "Delete template “{value}”?"],
M02_InUse: [
"쓰는 양식이라 못 지움 「{value}」 — {use}",
"“{value}” is in use and cannot be deleted — {use}",
],
M02_Deleted: ["지웠음", "Deleted"],
M02_Created: ["만들었음", "Created"],
M02_Saved: ["저장했음", "Saved"],
+10 -4
View File
@@ -13,11 +13,16 @@ export interface ModalOptions {
mount: (body: HTMLElement, close: () => void) => void;
dialogClass?: string;
backdropClass?: string;
/** 닫기 단추 · Esc · 바깥 누르기 전에 물음 — false 면 안 닫음(몸의 `close` 는 묻지 않고 닫음) */
beforeClose?: () => boolean | Promise<boolean>;
}
export function openModal(opts: ModalOptions): { close: () => void } {
const body = el("div", { className: "ui-modal__body" });
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(),
attrs: { role: "dialog", "aria-label": opts.title },
@@ -26,7 +31,7 @@ export function openModal(opts: ModalOptions): { close: () => void } {
className: "ui-modal__top",
children: [
el("h3", { text: opts.title }),
createButton({ label: opts.closeLabel, variant: "ghost", onClick: close }),
createButton({ label: opts.closeLabel, variant: "ghost", onClick: () => void ask() }),
],
}),
body,
@@ -36,11 +41,12 @@ export function openModal(opts: ModalOptions): { close: () => void } {
className: `ui-modal__backdrop ${opts.backdropClass ?? ""}`.trim(),
children: [dialog],
});
backdrop.addEventListener("click", (ev) => ev.target === backdrop && close());
backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close());
backdrop.addEventListener("click", (ev) => ev.target === backdrop && void ask());
backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && void ask());
opts.mount(body, close);
document.body.append(backdrop);
dialog.tabIndex = -1;
dialog.focus();
// 몸의 첫 입력 칸에 커서 — mount 안의 focus() 는 붙기 전이라 안 먹음
(body.querySelector<HTMLElement>("input, textarea, select") ?? dialog).focus();
return { close };
}
+4
View File
@@ -154,6 +154,10 @@
--radius-icons: var(--radius-lg);
--radius-large: var(--radius-3xl);
/* 입력칸 · 고르기칸 높이 — 크기마다 한 값(같은 줄이면 위아래가 맞음) */
--control-height: 42px;
--control-height-sm: 30px;
/* ---------------------------------------------------------------------------
* 6. 그림자 (Shadows) — 블루틴트, 서브틀 (최대 32px blur / 6% opacity)
* ------------------------------------------------------------------------ */