feat(M01): 화면 부품 공용화 — 드롭다운·찾기 칸·단추·확인창을 공용 템플릿으로 · 재료 컨테이너 차례
- createSelectField 에 선택 인자 더함(compact · ariaLabel · setOptions) — 기본 동작 그대로 - 인력 2단 거름 · 로직 편집 고르기 · 계산 입력 · 요소 찾기 창 그룹 select 를 공용으로 · m01-side__select 걷음 - 찾기 칸은 createInputField · 표 칸·모달 단추는 createButton · 버리기 확인은 showConfirmDialog - 재료 컨테이너 = 자재품목 · 유가 · 품셈재료 차례 · 안 쓰는 조달 문구 걷음 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
* 고친 로직은 저장 전 줄(`row`)을 같이 보내 메모리에서 셈(`POST /calc`) — 파일에 안 씀
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
runCalc,
|
||||
type CalcAnswer,
|
||||
@@ -31,22 +31,25 @@ const SUMS = ["노무비", "재료비", "경비", "계"];
|
||||
export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
const fields = (ctx.row.입력 ?? []).map((spec) => {
|
||||
const name = spec.이름;
|
||||
let control: HTMLInputElement | HTMLSelectElement;
|
||||
let control: HTMLElement;
|
||||
if (spec.고르기?.length) {
|
||||
control = el("select", { className: "m01-logic__input" });
|
||||
for (const option of ["", ...spec.고르기.map(String)]) {
|
||||
control.append(el("option", { text: option, attrs: { value: option } }));
|
||||
}
|
||||
const pick = createSelectField({
|
||||
options: ["", ...spec.고르기.map(String)].map((o) => ({ value: o, text: o })),
|
||||
value: ctx.values[name] ?? "",
|
||||
compact: true,
|
||||
onChange: (v) => (ctx.values[name] = v),
|
||||
});
|
||||
control = pick.root;
|
||||
} else {
|
||||
control = el("input", {
|
||||
const box = el("input", {
|
||||
className: "m01-logic__input",
|
||||
attrs: { type: "text", inputmode: "decimal" },
|
||||
});
|
||||
if (spec.범위) control.placeholder = `${spec.범위[0]} ∼ ${spec.범위[1]}`;
|
||||
if (spec.범위) box.placeholder = `${spec.범위[0]} ∼ ${spec.범위[1]}`;
|
||||
box.value = ctx.values[name] ?? "";
|
||||
box.addEventListener("input", () => (ctx.values[name] = box.value));
|
||||
control = box;
|
||||
}
|
||||
control.value = ctx.values[name] ?? "";
|
||||
control.addEventListener("input", () => (ctx.values[name] = control.value));
|
||||
control.addEventListener("change", () => (ctx.values[name] = control.value));
|
||||
const label = spec.단위 ? `${name} (${spec.단위})` : name;
|
||||
return el("label", {
|
||||
className: "m01-logic__field",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* 줄 더하기·지우기 · 요소 고르기처럼 모양이 바뀔 때만 `rerender`.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { createButton, createSelectField, el } from "@ui/ui_template_elements";
|
||||
import type {
|
||||
CalcLine,
|
||||
ElementBrief,
|
||||
@@ -70,14 +70,14 @@ function choice(
|
||||
options: string[],
|
||||
value: string | undefined,
|
||||
onPick: (v: string) => void,
|
||||
): HTMLSelectElement {
|
||||
const select = el("select", { className: "m01-logic__input" });
|
||||
for (const option of value && !options.includes(value) ? [value, ...options] : options) {
|
||||
select.append(el("option", { text: option, attrs: { value: option } }));
|
||||
}
|
||||
select.value = value ?? options[0];
|
||||
select.addEventListener("change", () => onPick(select.value));
|
||||
return select;
|
||||
): HTMLElement {
|
||||
const all = value && !options.includes(value) ? [value, ...options] : options;
|
||||
return createSelectField({
|
||||
options: all.map((o) => ({ value: o, text: o })),
|
||||
value: value ?? options[0],
|
||||
compact: true,
|
||||
onChange: onPick,
|
||||
}).root;
|
||||
}
|
||||
|
||||
function dropButton(onClick: () => void): HTMLButtonElement {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 거르기는 화면에서(목록은 한 번 받음 · 391 줄 남짓)
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { createInputField, el } from "@ui/ui_template_elements";
|
||||
import { renderTree, type MakeRow, type TreeNode } from "./M01_MasterData_UI_Tree";
|
||||
import type { LogicSummary } from "./M01_MasterData_UI_Logic_Api";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
@@ -40,10 +40,8 @@ export function buildList(onOpen: (item: ListItem) => void): ListHandle {
|
||||
/** 트리에서 고른 범위 — 원문 · 부문 · 장(비면 전체) */
|
||||
const pick = { book: "", division: "", chapter: "" };
|
||||
const tree = el("div", { className: "m01-logic__tree" });
|
||||
const search = el("input", {
|
||||
className: "m01-logic__input",
|
||||
attrs: { type: "search", placeholder: tx("List_Search") },
|
||||
});
|
||||
const searchField = createInputField({ type: "search", placeholder: tx("List_Search") });
|
||||
const search = searchField.input;
|
||||
const blockedOnly = el("input", { attrs: { type: "checkbox" } });
|
||||
const count = el("span", { className: "m01-logic__muted" });
|
||||
const list = el("ul", { className: "m01-logic__list" });
|
||||
@@ -178,7 +176,7 @@ export function buildList(onOpen: (item: ListItem) => void): ListHandle {
|
||||
el("div", {
|
||||
className: "m01-logic__filters",
|
||||
children: [
|
||||
search,
|
||||
searchField.root,
|
||||
el("label", {
|
||||
className: "m01-logic__check",
|
||||
children: [blockedOnly, el("span", { text: tx("List_BlockedOnly") })],
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
* 표 고르기(소요량·계수)는 값칸 단추를 눌러 `찾기(…).칸` 으로 수량에 넣음
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
createSelectField,
|
||||
el,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { searchElements, type ElementBrief } from "./M01_MasterData_UI_Logic_Api";
|
||||
import { formatNumber, type PickDone, type PickMode } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
@@ -16,13 +22,14 @@ const GROUPS: Record<PickMode, string[]> = {
|
||||
|
||||
export function openPicker(mode: PickMode, group: string, done: PickDone): void {
|
||||
const groups = GROUPS[mode];
|
||||
const select = el("select", { className: "m01-logic__input" });
|
||||
for (const g of groups) select.append(el("option", { text: g, attrs: { value: g } }));
|
||||
select.value = groups.includes(group) ? group : groups[0];
|
||||
const search = el("input", {
|
||||
className: "m01-logic__input",
|
||||
attrs: { type: "search", placeholder: tx("Pick_Search") },
|
||||
const groupField = createSelectField({
|
||||
options: groups.map((g) => ({ value: g, text: g })),
|
||||
value: groups.includes(group) ? group : groups[0],
|
||||
compact: true,
|
||||
});
|
||||
const select = groupField.select;
|
||||
const searchField = createInputField({ type: "search", placeholder: tx("Pick_Search") });
|
||||
const search = searchField.input;
|
||||
const count = el("span", { className: "m01-logic__muted" });
|
||||
const list = el("div", { className: "m01-logic__pick-list" });
|
||||
const close = (): void => backdrop.remove();
|
||||
@@ -37,7 +44,10 @@ export function openPicker(mode: PickMode, group: string, done: PickDone): void
|
||||
createButton({ label: tx("Pick_Close"), variant: "ghost", onClick: close }),
|
||||
],
|
||||
}),
|
||||
el("div", { className: "m01-logic__pick-bar", children: [select, search, count] }),
|
||||
el("div", {
|
||||
className: "m01-logic__pick-bar",
|
||||
children: [groupField.root, searchField.root, count],
|
||||
}),
|
||||
list,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -8,7 +8,13 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import "@ui/ui_template_workflow_layout.css";
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
el,
|
||||
showConfirmDialog,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { createWorkflowOverlays } from "@ui/ui_template_overlay";
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
@@ -42,10 +48,8 @@ function buildPage(): HTMLElement {
|
||||
|
||||
/* --- 우측: 머리(제목·찾기·저장) + 알림 + 본문 --- */
|
||||
const title = el("h2", { className: "m01-master__title", text: L("M01_PickFile") });
|
||||
const search = el("input", {
|
||||
className: "m01-master__search",
|
||||
attrs: { type: "search", placeholder: L("M01_Search") },
|
||||
});
|
||||
const searchField = createInputField({ type: "search", placeholder: L("M01_Search") });
|
||||
const search = searchField.input;
|
||||
const summary = el("span", { className: "m01-master__summary" });
|
||||
const save = createButton({ label: L("M01_Save"), variant: "filled" });
|
||||
const drop = createButton({ label: L("M01_Discard"), variant: "ghost" });
|
||||
@@ -53,7 +57,7 @@ function buildPage(): HTMLElement {
|
||||
const body = el("div", { className: "m01-master__body" });
|
||||
const head = el("div", {
|
||||
className: "m01-master__head",
|
||||
children: [title, search, summary, drop, save],
|
||||
children: [title, searchField.root, summary, drop, save],
|
||||
});
|
||||
const logicHost = el("div", { className: "m01-master__logic", attrs: { hidden: "" } });
|
||||
const elementView = el("div", {
|
||||
@@ -98,8 +102,8 @@ function buildPage(): HTMLElement {
|
||||
}, 300);
|
||||
});
|
||||
|
||||
drop.addEventListener("click", () => {
|
||||
if (!window.confirm(L("M01_DiscardConfirm"))) return;
|
||||
drop.addEventListener("click", async () => {
|
||||
if (!(await showConfirmDialog(L("M01_DiscardConfirm")))) return;
|
||||
discard();
|
||||
showNotice([]);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 고르기 모달 — 가격 연결(자재품목) · 준용 직종을 이름·규격으로 찾아 고르면 ref 를 돌려줌
|
||||
* ========================================================================== */
|
||||
|
||||
import { el, showToast } from "@ui/ui_template_elements";
|
||||
import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { fetchPick, type PickItem, type PickKind } from "./M01_MasterData_Api_Fetch";
|
||||
import { show } from "./M01_MasterData_UI_Cells";
|
||||
@@ -28,21 +28,15 @@ export interface PickOptions {
|
||||
cond?: { 이름: string; 규격: string; onCond: (c: Cond) => void };
|
||||
}
|
||||
|
||||
const button = (text: string): HTMLButtonElement =>
|
||||
el("button", { className: "m01-master__row-btn", text, attrs: { type: "button" } });
|
||||
const button = (label: string): HTMLButtonElement => createButton({ label, variant: "ghost" });
|
||||
|
||||
const input = (value: string, placeholder: string): HTMLInputElement => {
|
||||
const box = el("input", {
|
||||
className: "m01-master__search",
|
||||
attrs: { type: "search", placeholder },
|
||||
});
|
||||
box.value = value;
|
||||
return box;
|
||||
};
|
||||
const field = (value: string, placeholder: string): ReturnType<typeof createInputField> =>
|
||||
createInputField({ type: "search", placeholder, value });
|
||||
|
||||
export function openPickModal(opt: PickOptions): void {
|
||||
const close = (): void => back.remove();
|
||||
const search = input(opt.seed, L("M01_ProcureSearch"));
|
||||
const searchField = field(opt.seed, L("M01_ProcureSearch"));
|
||||
const search = searchField.input;
|
||||
const list = el("div", { className: "m01-procure__list" });
|
||||
const cut = button(L("M01_ProcureCut"));
|
||||
cut.disabled = !opt.current;
|
||||
@@ -56,8 +50,8 @@ export function openPickModal(opt: PickOptions): void {
|
||||
const condRow: HTMLElement[] = [];
|
||||
if (opt.cond) {
|
||||
const { onCond } = opt.cond;
|
||||
const name = input(opt.cond.이름, L("M01_PriceCondName"));
|
||||
const spec = input(opt.cond.규격, L("M01_PriceCondSpec"));
|
||||
const name = field(opt.cond.이름, L("M01_PriceCondName")).input;
|
||||
const spec = field(opt.cond.규격, L("M01_PriceCondSpec")).input;
|
||||
const go = button(L("M01_PriceCond"));
|
||||
go.addEventListener("click", () => {
|
||||
if (!name.value.trim()) return;
|
||||
@@ -72,7 +66,7 @@ export function openPickModal(opt: PickOptions): void {
|
||||
children: [
|
||||
el("h3", { text: opt.title }),
|
||||
el("p", { className: "m01-master__muted", text: opt.current }),
|
||||
search,
|
||||
searchField.root,
|
||||
list,
|
||||
...condRow,
|
||||
el("div", { className: "m01-procure__foot", children: [cut, shut] }),
|
||||
|
||||
@@ -250,19 +250,12 @@ export function renderRows(
|
||||
}
|
||||
|
||||
function actionCell(label: string, onClick: () => void): HTMLTableCellElement {
|
||||
const button = el("button", {
|
||||
className: "m01-master__row-btn",
|
||||
text: label,
|
||||
attrs: { type: "button" },
|
||||
});
|
||||
button.addEventListener("click", onClick);
|
||||
return el("td", { children: [button] });
|
||||
return el("td", { children: [createButton({ label, variant: "ghost", onClick })] });
|
||||
}
|
||||
|
||||
type Put = (row: Row) => void;
|
||||
|
||||
const rowBtn = (text: string): HTMLButtonElement =>
|
||||
el("button", { className: "m01-master__row-btn", text, attrs: { type: "button" } });
|
||||
const rowBtn = (label: string): HTMLButtonElement => createButton({ label, variant: "ghost" });
|
||||
|
||||
/** 품셈재료 — 가격 연결(자재품목 키 · 후보 조건 · 없음) · 고르기 모달. */
|
||||
function linkedCell(row: Row, cur: Row, put?: Put): HTMLTableCellElement {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 펼치면 하위 목록(거름 · 파일) · 환율/요율은 누르면 바로 표 · 로직은 안에 로직 목록
|
||||
* ========================================================================== */
|
||||
|
||||
import { el, showToast } from "@ui/ui_template_elements";
|
||||
import { createSelectField, el, showToast } from "@ui/ui_template_elements";
|
||||
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import {
|
||||
@@ -38,6 +38,8 @@ export interface SideHandle {
|
||||
const LABOR_FILTER = "m01.laborFilter";
|
||||
const LABOR_ID = "인력|";
|
||||
const MATERIAL_NAMES: Record<string, string> = { 나라장터자재: "나라장터", 오피넷유가: "유가" };
|
||||
/** 재료 컨테이너 차례 — 여기 없는 파일은 뒤에 */
|
||||
const MATERIAL_ORDER = ["자재품목", "오피넷유가", "품셈재료"];
|
||||
|
||||
/** 파일 이름 → 목록 글자 — 「소요량_건설품셈_10장_창호…」 → 「건설품셈_10장_창호…」 */
|
||||
export const fileLabel = (file: string): string =>
|
||||
@@ -114,64 +116,59 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si
|
||||
}
|
||||
})();
|
||||
const pickSub = list.find((k) => k.name === saved[0]);
|
||||
const choose = (name: string, values: string[], value: string): HTMLSelectElement => {
|
||||
const select = el("select", {
|
||||
className: "m01-side__select",
|
||||
attrs: { "aria-label": name },
|
||||
children: [L("M01_All"), ...values].map((v, i) =>
|
||||
el("option", { text: v, attrs: { value: i ? v : "" } }),
|
||||
),
|
||||
});
|
||||
select.value = value;
|
||||
return select;
|
||||
};
|
||||
const sub = choose(
|
||||
L("M01_LaborSub"),
|
||||
list.map((k) => k.name),
|
||||
pickSub?.name ?? "",
|
||||
);
|
||||
const detail = choose(L("M01_LaborDetail"), pickSub?.details ?? [], saved[1] ?? "");
|
||||
detail.disabled = !pickSub?.details.length;
|
||||
const opts = (values: string[]): { value: string; text: string }[] =>
|
||||
[L("M01_All"), ...values].map((v, i) => ({ value: i ? v : "", text: v }));
|
||||
const sub = createSelectField({
|
||||
options: opts(list.map((k) => k.name)),
|
||||
value: pickSub?.name ?? "",
|
||||
compact: true,
|
||||
label: L("M01_LaborSub"),
|
||||
});
|
||||
const detail = createSelectField({
|
||||
options: opts(pickSub?.details ?? []),
|
||||
value: saved[1] ?? "",
|
||||
compact: true,
|
||||
label: L("M01_LaborDetail"),
|
||||
disabled: !pickSub?.details.length,
|
||||
});
|
||||
const run = (): void => {
|
||||
try {
|
||||
sessionStorage.setItem(LABOR_FILTER, JSON.stringify([sub.value, detail.value]));
|
||||
sessionStorage.setItem(
|
||||
LABOR_FILTER,
|
||||
JSON.stringify([sub.select.value, detail.select.value]),
|
||||
);
|
||||
} catch {
|
||||
/* 기억 못 해도 거름은 됨 */
|
||||
}
|
||||
const label = [sub.value, detail.value].filter(Boolean).join(" · ") || L("M01_All");
|
||||
const label =
|
||||
[sub.select.value, detail.select.value].filter(Boolean).join(" · ") || L("M01_All");
|
||||
setActive(LABOR_ID);
|
||||
open(file, sub.value, label, detail.value);
|
||||
open(file, sub.select.value, label, detail.select.value);
|
||||
};
|
||||
sub.addEventListener("change", () => {
|
||||
const details = list.find((k) => k.name === sub.value)?.details ?? [];
|
||||
detail.replaceChildren(
|
||||
...[L("M01_All"), ...details].map((v, i) =>
|
||||
el("option", { text: v, attrs: { value: i ? v : "" } }),
|
||||
),
|
||||
);
|
||||
detail.disabled = details.length === 0;
|
||||
sub.select.addEventListener("change", () => {
|
||||
const details = list.find((k) => k.name === sub.select.value)?.details ?? [];
|
||||
detail.setOptions(opts(details), "");
|
||||
detail.select.disabled = details.length === 0;
|
||||
run();
|
||||
});
|
||||
detail.addEventListener("change", run);
|
||||
detail.select.addEventListener("change", run);
|
||||
const all = make({ id: LABOR_ID, label: L("M01_All"), count: file.rows, run }, "", 0, () => {
|
||||
sub.value = detail.value = "";
|
||||
sub.dispatchEvent(new Event("change"));
|
||||
sub.select.value = "";
|
||||
sub.select.dispatchEvent(new Event("change"));
|
||||
});
|
||||
return el("div", {
|
||||
children: [
|
||||
all,
|
||||
el("label", {
|
||||
className: "m01-side__filter",
|
||||
children: [el("span", { text: L("M01_LaborSub") }), sub],
|
||||
}),
|
||||
el("label", {
|
||||
className: "m01-side__filter",
|
||||
children: [el("span", { text: L("M01_LaborDetail") }), detail],
|
||||
}),
|
||||
el("div", { className: "m01-side__filter", children: [sub.root, detail.root] }),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const order = (file: string): number => {
|
||||
const i = MATERIAL_ORDER.indexOf(fileLabel(file));
|
||||
return i < 0 ? MATERIAL_ORDER.length : i;
|
||||
};
|
||||
|
||||
const drawGroup = (group: Group): void => {
|
||||
const files = lists.get(group) ?? [];
|
||||
const body = bodies.get(group);
|
||||
@@ -231,11 +228,18 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si
|
||||
}
|
||||
body.replaceChildren(
|
||||
...renderTree(
|
||||
files.map((f) => {
|
||||
const name = fileLabel(f.file);
|
||||
const label = group === "재료" ? (MATERIAL_NAMES[name] ?? name) : name;
|
||||
return { id: `${group}\n${f.file}`, label, count: f.rows, run: () => open(f, "", label) };
|
||||
}),
|
||||
[...files]
|
||||
.sort((a, b) => order(a.file) - order(b.file))
|
||||
.map((f) => {
|
||||
const name = fileLabel(f.file);
|
||||
const label = group === "재료" ? (MATERIAL_NAMES[name] ?? name) : name;
|
||||
return {
|
||||
id: `${group}\n${f.file}`,
|
||||
label,
|
||||
count: f.rows,
|
||||
run: () => open(f, "", label),
|
||||
};
|
||||
}),
|
||||
make,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -198,15 +198,10 @@
|
||||
padding: 2px var(--spacing-4);
|
||||
}
|
||||
|
||||
.m01-master__row-btn {
|
||||
/* 표 칸 안 공용 단추 — 줄 높이에 맞춰 작게 */
|
||||
.m01-master__grid .ui-btn {
|
||||
padding: 2px var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-buttons);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-body);
|
||||
font: inherit;
|
||||
font-size: var(--text-caption);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.m01-master__pager {
|
||||
@@ -302,12 +297,7 @@
|
||||
|
||||
.m01-side__filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
}
|
||||
|
||||
.m01-side__select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ export { el } from "./ui_template_elements_base";
|
||||
|
||||
/** 요소 생성 + 속성/클래스/자식 일괄 설정 헬퍼 */
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 1. 버튼 (Button) — design.md: Filled Brand / Ghost Outlined / Pill Nav
|
||||
* -------------------------------------------------------------------------- */
|
||||
@@ -135,11 +134,17 @@ export interface SelectFieldOptions {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
/** 좁은 자리(표 칸·좌측 패널)용 작은 여백 */
|
||||
compact?: boolean;
|
||||
/** 라벨이 없을 때 보조기기용 이름 */
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export interface SelectFieldHandle {
|
||||
root: HTMLDivElement;
|
||||
select: HTMLSelectElement;
|
||||
/** 선택지를 다시 채움 — value 가 목록에 없으면 첫 항목 */
|
||||
setOptions: (options: { value: string; text: string }[], value?: string) => void;
|
||||
}
|
||||
|
||||
export function createSelectField(opts: SelectFieldOptions): SelectFieldHandle {
|
||||
@@ -150,19 +155,17 @@ export function createSelectField(opts: SelectFieldOptions): SelectFieldHandle {
|
||||
}
|
||||
|
||||
const select = el("select", {
|
||||
className: "ui-select",
|
||||
className: opts.compact ? "ui-select ui-select--sm" : "ui-select",
|
||||
attrs: opts.ariaLabel ? { "aria-label": opts.ariaLabel } : {},
|
||||
});
|
||||
|
||||
for (const opt of opts.options) {
|
||||
const optEl = el("option", {
|
||||
attrs: { value: opt.value },
|
||||
text: opt.text,
|
||||
});
|
||||
if (opts.value !== undefined && opt.value === opts.value) {
|
||||
optEl.selected = true;
|
||||
}
|
||||
select.append(optEl);
|
||||
}
|
||||
const setOptions: SelectFieldHandle["setOptions"] = (options, value) => {
|
||||
select.replaceChildren(
|
||||
...options.map((opt) => el("option", { attrs: { value: opt.value }, text: opt.text })),
|
||||
);
|
||||
if (value !== undefined && options.some((opt) => opt.value === value)) select.value = value;
|
||||
};
|
||||
setOptions(opts.options, opts.value);
|
||||
|
||||
if (opts.disabled) select.disabled = true;
|
||||
|
||||
@@ -172,7 +175,7 @@ export function createSelectField(opts: SelectFieldOptions): SelectFieldHandle {
|
||||
|
||||
root.append(select);
|
||||
|
||||
return { root, select };
|
||||
return { root, select, setOptions };
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
@@ -360,4 +363,3 @@ export function createWorkflowShell(opts: WorkflowShellOptions): WorkflowShellHa
|
||||
* 7-1. 라인 차트 (Line Chart) — 시계열 데이터 SVG 렌더링
|
||||
* 외부 라이브러리 없이 인라인 SVG. 색상은 CSS 클래스 + theme.css 변수 참조.
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
|
||||
@@ -130,6 +130,10 @@ const BASE_CSS = `
|
||||
border-color: var(--color-focus-ring);
|
||||
box-shadow: 0 0 0 1px var(--color-focus-ring);
|
||||
}
|
||||
.ui-select--sm {
|
||||
padding: 4px 28px 4px 8px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.ui-select:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
|
||||
@@ -37,12 +37,9 @@ export const ui_locales_m1 = {
|
||||
"This file changed after you started editing — saving will be refused",
|
||||
],
|
||||
M01_CheckErrors: ["검사에 걸림", "Validation failed"],
|
||||
M01_ProcureFind: ["조달 찾기", "Find procurement"],
|
||||
M01_ProcureTitle: ["조달 자료 찾기", "Find procurement data"],
|
||||
M01_ProcureSearch: ["이름·규격 찾기", "Find by name or spec"],
|
||||
M01_ProcureCut: ["연결 끊기", "Unlink"],
|
||||
M01_ProcureClose: ["닫기", "Close"],
|
||||
M01_ProcureCol: ["조달 연결", "Procurement link"],
|
||||
M01_PriceFind: ["가격 연결", "Link price"],
|
||||
M01_PriceTitle: ["가격 연결 고르기", "Pick price source"],
|
||||
M01_PriceCol: ["가격 연결", "Price link"],
|
||||
|
||||
Reference in New Issue
Block a user