refactor(794-1b): M01 흐름도 패널 걷음 — 로직 상세 오른쪽 = 원문만 · [신규 로직] = 빈 시트 줄 · 흐름 서버 길 · DataFlow 화면 지움
요구 651 · 653 — 계산 공식 참고용 흐름도(① M01 로직 상세 오른쪽 접는 패널)를 걷음. ② M02 일위대가 다이어그램은 그대로.
지움(64 파일 · 18,666 줄 · 되돌릴 길 = git 이력)
- M01 6 파일 1,064 줄 — Store_Flow.py · UI_LogicLab_Flow.ts/.css · FlowRows.ts · FlowView.ts/.css
- M02_MasterTemplete/DataFlow 43 파일 12,799 줄 — 흐름도 화면 · 셈(Compute · Convert · Lookup · Model · Eval …)
- 시험 15 파일 4,803 줄 — test_m01_logic_flow{view,_engine,_project,_save} · test_m02_df_*(값 찾기 values_old 뺌) — 지운 흐름 코드만 지키던 것
남김(바닥) — DataFlow M02_DF_UnitPick(일위대가 셋) · M02_DF_Values(M02_Item_Prices) · M02_DF_Round · _standard 표 ·
M02_DF_Router = GET /api/m02/df/standards 하나 · M02_DF_Api = cfStandards 하나 · CfRound 꼴은 Round 안으로
고침
- M01 Router — /save = Store_Make.save(개정 확인 · 자체 파일 새 줄 GX · added 그대로) · /calc /text /logic/auto = 옛 셈만 · /logic/flow 둘 지움
- master_formula — 흐름 훅 지움 · 줄에 「흐름」 이 남으면 「흐름도 로직은 걷음」 까닭 오류(저장된 흐름 로직 0 — 마스터 · storage 다시 확인)
- Sangeun_Calc._defaults = 옛 자동값 · M02_Logic_Lib_Read /logic/flow 지움 · main.py 주석
- 화면 — LogicLab(startFlow · scheduleFlow · 예시값 잇기 지움 · [신규 로직] 줄 = 입력 · 중간 · 호표 빈 목록 + 끝수 null) ·
Side(원문 패널 하나) · Source(머리 칸 이름 m01lab-src__top/__title · DF_Panel.css 안 씀) · Detail · View(nowInputs 지움) · Sheet(흐름 갈래 지움) · 글
- 시험 — 지운 파일을 가리키던 목록 넷(spreadsheet_size · 208_1 · 588 · input_prefs) · 717_1 흐름 길 두 줄 · values_old 흐름 셈 부분 지움
- test_713_3_list 기대 — 로직 1,461 → 1,515 · 묶음 307 → 335 · 작업량 21 → 65 · 부르는 506 → 512(782-1 · 2 · 3 건설 기초 로직 54 더함 ·
옛 키만 세면 307 · 21 · 506 그대로)
- 새 지킴 test_794_1b_flow_gone — 흐름 길 404 · 표준 표 남음 · 신규 로직 자체 GX · 흐름 남은 줄 까닭 오류
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,541 +0,0 @@
|
||||
/* =============================================================================
|
||||
* M02_DF_LinkPanel.ts
|
||||
* 상세 내용 판 — 선 규칙: 선을 고르면 어디서 → 어디로 · 규칙 고르기(없음 · 표에서 찾기 · 조건 통과 · 바꾸기 · 끝수) ·
|
||||
* 지나가기 전 값 → 지난 뒤 값 미리 보기 · 겹침/안 맞음 알림
|
||||
* 모양은 `docs/raw/M01_쉬운단가/흐름도_정함.md` 정함 7 · M02_DF_Model 의 CfRule 그대로 · 단추 · 칸은 공용 작은 크기
|
||||
* 글 [한, 영] 짝은 아래 TEXT
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import {
|
||||
gateConds,
|
||||
makeGate,
|
||||
portsOut,
|
||||
reaches,
|
||||
valueChoices,
|
||||
type CfGateCond,
|
||||
type CfGateOp,
|
||||
type CfLogic,
|
||||
type CfPort,
|
||||
type CfRound,
|
||||
type CfRule,
|
||||
} from "./M02_DF_Model";
|
||||
import {
|
||||
GATE_OPS,
|
||||
commas,
|
||||
SCALE_OPS,
|
||||
evalSafe,
|
||||
labelOf,
|
||||
linkNotes,
|
||||
portText,
|
||||
preview,
|
||||
unitOf,
|
||||
valueOf,
|
||||
type DetailOptions,
|
||||
} from "./M02_DF_Detail";
|
||||
import { suggestPlaces } from "./M02_DF_UnitPick";
|
||||
import "./M02_DF_LinkPanel.css";
|
||||
import "@ui/ui_template_text.css";
|
||||
import "@ui/ui_template_banner.css";
|
||||
import "@ui/ui_template_badge.css";
|
||||
|
||||
const TEXT = {
|
||||
Title: ["선 규칙", "Line rule"],
|
||||
Kinds: ["규칙 고르기", "Pick a rule"],
|
||||
K_none: ["없음", "None"],
|
||||
K_lookup: ["표에서 찾기", "Look up in table"],
|
||||
K_gate: ["조건 통과", "Pass if"],
|
||||
K_scale: ["바꾸기", "Change"],
|
||||
K_round: ["끝수", "Rounding"],
|
||||
Label: ["선 위 글", "Text on the line"],
|
||||
Before: ["지나가기 전 값", "Value before"],
|
||||
After: ["지난 뒤 값", "Value after"],
|
||||
Col: ["조건 칸", "Condition column"],
|
||||
Match: ["맞출 알약", "Pill to match"],
|
||||
Take: ["꺼낼 칸", "Column to take"],
|
||||
AddCond: ["+ 조건 칸", "+ Condition"],
|
||||
Remove: ["지움", "Remove"],
|
||||
NoHeader: [
|
||||
"보낸 쪽이 표가 아니거나 표를 아직 못 불러옴 — 표를 불러온 뒤 칸을 고름",
|
||||
"The sender is not a table or it is not loaded yet",
|
||||
],
|
||||
Pick: ["고르기", "pick"],
|
||||
GateHint: [
|
||||
"알약 값이 아래 조건을 지나면 보냄 — 아니면 넘기지 않음",
|
||||
"Sent only when the pill passes",
|
||||
],
|
||||
Value: ["값", "Value"],
|
||||
ScaleHint: ["지나가는 값을 바꿈", "Changes the value on its way"],
|
||||
Places: ["소수 {n}자리", "{n} decimals"],
|
||||
Suggest: ["표준 제안: 소수 {n}자리", "Standard suggests {n} decimals"],
|
||||
Apply: ["적용", "Apply"],
|
||||
WarnBoth: [
|
||||
"같은 알약에서 나간 조건 선 {n}개가 한꺼번에 통과함 — 겹침",
|
||||
"{n} conditional lines pass at once — overlap",
|
||||
],
|
||||
WarnNone: [
|
||||
"같은 알약에서 나간 조건 선 {n}개 중 어느 것도 안 맞음",
|
||||
"None of the {n} conditional lines match",
|
||||
],
|
||||
WarnLookup: [
|
||||
"조건 칸이 없음 — 표에서 줄을 못 고름",
|
||||
"No condition column — a row can't be picked",
|
||||
],
|
||||
WarnTake: ["꺼낼 칸을 고르지 않음", "No column to take"],
|
||||
Count: ["선", "Line"],
|
||||
And: ["그리고", "and"],
|
||||
Or: ["또는", "or"],
|
||||
If: ["조건", "If"],
|
||||
AddLine: ["+ 조건 줄", "+ Condition line"],
|
||||
PickOne: ["고르세요", "choose"],
|
||||
Join: ["잇는 말", "Join with"],
|
||||
OutRange: ["범위 {n} 밖", "Outside the range {n}"],
|
||||
Range: ["{n} (범위)", "{n} (range)"],
|
||||
} as const;
|
||||
|
||||
const tk = (key: keyof typeof TEXT, n?: number | string): string =>
|
||||
TEXT[key][currentLanguageIndex === 1 ? 1 : 0].replace("{n}", String(n ?? ""));
|
||||
|
||||
type Kind = "none" | CfRule["kind"];
|
||||
const KINDS: Kind[] = ["none", "lookup", "gate", "scale", "round"];
|
||||
const SEP = "\u0001";
|
||||
|
||||
interface Ctx {
|
||||
lg: CfLogic;
|
||||
linkId: string;
|
||||
readOnly: boolean;
|
||||
headers: string[];
|
||||
detail: DetailOptions;
|
||||
change(lg: CfLogic): void;
|
||||
}
|
||||
|
||||
const toKey = (p: CfPort): string => `${p.box}${SEP}${p.name}`;
|
||||
const fromKey = (k: string): CfPort => {
|
||||
const [box, ...rest] = k.split(SEP);
|
||||
return { box, name: rest.join(SEP) };
|
||||
};
|
||||
|
||||
function setRule(ctx: Ctx, rule: CfRule | undefined): void {
|
||||
ctx.change({
|
||||
...ctx.lg,
|
||||
links: ctx.lg.links.map((l) => {
|
||||
if (l.id !== ctx.linkId) return l;
|
||||
const { rule: _old, ...rest } = l;
|
||||
return rule ? { ...rest, rule } : rest;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function select(
|
||||
ctx: Ctx,
|
||||
options: { value: string; text: string }[],
|
||||
value: string,
|
||||
onPick: (v: string) => void,
|
||||
aria: string,
|
||||
): HTMLElement {
|
||||
const s = el("select", { className: "ui-select ui-select--sm", attrs: { "aria-label": aria } });
|
||||
for (const o of options) s.append(el("option", { attrs: { value: o.value }, text: o.text }));
|
||||
if (!options.some((o) => o.value === value))
|
||||
s.prepend(el("option", { attrs: { value }, text: value || tk("Pick") }));
|
||||
s.value = value;
|
||||
s.disabled = ctx.readOnly;
|
||||
s.addEventListener("change", () => onPick(s.value));
|
||||
return s;
|
||||
}
|
||||
|
||||
/** 숫자 칸 — 보일 때만 천 단위 쉼표 · 고치는 동안은 쉼표 없이 */
|
||||
function numberBox(
|
||||
ctx: Ctx,
|
||||
value: number | string,
|
||||
onCommit: (v: number | string) => void,
|
||||
aria: string,
|
||||
): HTMLElement {
|
||||
const i = el("input", {
|
||||
className: "ui-input ui-input--sm m02-dflink-num",
|
||||
attrs: { type: "text", inputmode: "decimal", "aria-label": aria },
|
||||
});
|
||||
const shown = (): string => (typeof value === "number" ? commas(value) : String(value));
|
||||
i.value = shown();
|
||||
i.readOnly = ctx.readOnly;
|
||||
i.addEventListener("focus", () => (i.value = String(value)));
|
||||
i.addEventListener("blur", () => (i.value = shown()));
|
||||
i.addEventListener("change", () => {
|
||||
const t = i.value.trim().replace(/,/g, "");
|
||||
onCommit(t !== "" && Number.isFinite(Number(t)) ? Number(t) : t);
|
||||
});
|
||||
return i;
|
||||
}
|
||||
|
||||
/** 맞출 알약 — 이 선이 닿는 상자와 그 뒤 상자(돌고 도는 선이 됨)는 뺌 */
|
||||
const pillOptions = (ctx: Ctx): { value: string; text: string }[] => {
|
||||
const to = ctx.lg.links.find((x) => x.id === ctx.linkId)?.to.box ?? "";
|
||||
return ctx.lg.boxes
|
||||
.filter((b) => b.id !== to && !reaches(ctx.lg, to, b.id))
|
||||
.flatMap((b) =>
|
||||
portsOut(b).map((p) => ({
|
||||
value: toKey({ box: b.id, name: p.name }),
|
||||
text: portText(ctx.lg, { box: b.id, name: p.name }),
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
const row = (...children: (HTMLElement | string)[]): HTMLElement =>
|
||||
el("div", { className: "m02-dflink-row", children });
|
||||
|
||||
/* ── 규칙마다 ─────────────────────────────────────────────────────────── */
|
||||
|
||||
function lookupEditor(ctx: Ctx, rule: Extract<CfRule, { kind: "lookup" }>): HTMLElement[] {
|
||||
// 꺼낼 칸 = 표 머리 칸만
|
||||
const cols = [...new Set([...ctx.headers, rule.col].filter(Boolean))];
|
||||
const colOpts = cols.map((c) => ({ value: c, text: c }));
|
||||
// 조건 칸 — 「이름·아래」 「이름·위」 짝은 「이름 (범위)」 한 줄만(고르면 칸 이름 = 이름 · Rule 이 아래 < 값 ≤ 위 로 찾음)
|
||||
const range = (c: string): string | null => {
|
||||
const m = /^(.+)·(아래|위)$/.exec(c);
|
||||
return m && cols.includes(`${m[1]}·아래`) && cols.includes(`${m[1]}·위`) ? m[1] : null;
|
||||
};
|
||||
const bases = [...new Set(cols.map(range).filter((x): x is string => !!x))];
|
||||
const condOpts = [
|
||||
...cols.filter((c) => !range(c) && !bases.includes(c)).map((c) => ({ value: c, text: c })),
|
||||
...bases.map((x) => ({ value: x, text: tk("Range", x) })),
|
||||
];
|
||||
const pills = pillOptions(ctx);
|
||||
const put = (next: Partial<typeof rule>): void => setRule(ctx, { ...rule, ...next });
|
||||
const rows = rule.conds.map((c, i) =>
|
||||
row(
|
||||
select(
|
||||
ctx,
|
||||
condOpts,
|
||||
c.col,
|
||||
(v) => put({ conds: rule.conds.map((x, j) => (j === i ? { ...x, col: v } : x)) }),
|
||||
tk("Col"),
|
||||
),
|
||||
"=",
|
||||
select(
|
||||
ctx,
|
||||
pills,
|
||||
toKey(c.from),
|
||||
(v) => put({ conds: rule.conds.map((x, j) => (j === i ? { ...x, from: fromKey(v) } : x)) }),
|
||||
tk("Match"),
|
||||
),
|
||||
ctx.readOnly
|
||||
? ""
|
||||
: createButton({
|
||||
label: tk("Remove"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => put({ conds: rule.conds.filter((_, j) => j !== i) }),
|
||||
}),
|
||||
),
|
||||
);
|
||||
const out: HTMLElement[] = [...rows];
|
||||
if (!ctx.readOnly) {
|
||||
out.push(
|
||||
row(
|
||||
createButton({
|
||||
label: tk("AddCond"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
disabled: !pills.length || !condOpts.length,
|
||||
onClick: () =>
|
||||
put({
|
||||
conds: [...rule.conds, { col: condOpts[0].value, from: fromKey(pills[0].value) }],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
out.push(
|
||||
row(
|
||||
el("span", { className: "m02-dflink-label", text: tk("Take") }),
|
||||
select(ctx, colOpts, rule.col, (v) => put({ col: v }), tk("Take")),
|
||||
),
|
||||
);
|
||||
if (!cols.length)
|
||||
out.push(el("p", { className: "ui-text ui-text--muted", text: tk("NoHeader") }));
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 보낸 알약이 입력 상자의 숫자 변수면 그 범위 */
|
||||
function rangeOf(ctx: Ctx, p: CfPort): [number, number] | null {
|
||||
const v = ctx.lg.boxes
|
||||
.find((b) => b.id === p.box)
|
||||
?.input?.variables.find((x) => x.name === p.name);
|
||||
return v?.type === "number" && v.range ? v.range : null;
|
||||
}
|
||||
|
||||
/** 값 칸 — 목록이 있으면 고르기 · 없으면 숫자 칸(범위 밖이면 한 줄 알림) */
|
||||
function gateValue(ctx: Ctx, c: CfGateCond, onPick: (v: number | string) => void): HTMLElement[] {
|
||||
const list = valueChoices(ctx.lg, c.from, { values: ctx.detail.values });
|
||||
if (list?.length) {
|
||||
const key = (v: string | number): string => `${typeof v}:${v}`;
|
||||
const known = list.some((v) => key(v) === key(c.value));
|
||||
const options = [
|
||||
...(known ? [] : [{ value: "", text: tk("PickOne") }]),
|
||||
...list.map((v) => ({ value: key(v), text: String(v) })),
|
||||
];
|
||||
return [
|
||||
select(
|
||||
ctx,
|
||||
options,
|
||||
known ? key(c.value) : "",
|
||||
(k) => {
|
||||
const hit = list.find((v) => key(v) === k);
|
||||
if (hit !== undefined) onPick(hit);
|
||||
},
|
||||
tk("Value"),
|
||||
),
|
||||
];
|
||||
}
|
||||
const range = rangeOf(ctx, c.from);
|
||||
const out: HTMLElement[] = [numberBox(ctx, c.value, onPick, tk("Value"))];
|
||||
const n = Number(c.value);
|
||||
if (range && Number.isFinite(n) && (n < range[0] || n > range[1])) {
|
||||
out.push(
|
||||
el("span", {
|
||||
className: "ui-text ui-text--warning",
|
||||
text: `⚠ ${tk("OutRange", `${range[0]} ~ ${range[1]}`)}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function gateEditor(ctx: Ctx, rule: Extract<CfRule, { kind: "gate" }>): HTMLElement[] {
|
||||
const g = gateConds(rule);
|
||||
const put = (conds: CfGateCond[], join = g.join): void => setRule(ctx, makeGate(conds, join));
|
||||
const edit = (i: number, patch: Partial<CfGateCond>): void =>
|
||||
put(g.conds.map((c, j) => (j === i ? { ...c, ...patch } : c)));
|
||||
const out: HTMLElement[] = [
|
||||
el("p", { className: "ui-text ui-text--muted", text: tk("GateHint") }),
|
||||
];
|
||||
g.conds.forEach((c, i) => {
|
||||
out.push(
|
||||
row(
|
||||
el("span", {
|
||||
className: "m02-dflink-label",
|
||||
text: i ? tk(g.join === "그리고" ? "And" : "Or") : tk("If"),
|
||||
}),
|
||||
select(
|
||||
ctx,
|
||||
pillOptions(ctx),
|
||||
toKey(c.from),
|
||||
(v) => {
|
||||
const from = fromKey(v);
|
||||
edit(i, {
|
||||
from,
|
||||
value: valueChoices(ctx.lg, from, { values: ctx.detail.values })?.[0] ?? 0,
|
||||
});
|
||||
},
|
||||
tk("Match"),
|
||||
),
|
||||
select(
|
||||
ctx,
|
||||
GATE_OPS.map((o) => ({ value: o, text: o })),
|
||||
c.op,
|
||||
(v) => edit(i, { op: v as CfGateOp }),
|
||||
tk("Kinds"),
|
||||
),
|
||||
...gateValue(ctx, c, (v) => edit(i, { value: v })),
|
||||
ctx.readOnly || g.conds.length < 2
|
||||
? ""
|
||||
: createButton({
|
||||
label: tk("Remove"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => put(g.conds.filter((_, j) => j !== i)),
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
if (!ctx.readOnly) {
|
||||
const first = g.conds[0];
|
||||
out.push(
|
||||
row(
|
||||
createButton({
|
||||
label: tk("AddLine"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
const from = first?.from ?? { box: "", name: "" };
|
||||
// 새 줄 기본 값 — 목록이 있는 알약이면 그 첫 값 · 아니면 0
|
||||
const value = valueChoices(ctx.lg, from, { values: ctx.detail.values })?.[0] ?? 0;
|
||||
put([...g.conds, { from, op: "=", value }]);
|
||||
},
|
||||
}),
|
||||
...(g.conds.length > 1
|
||||
? [
|
||||
el("span", { className: "m02-dflink-label", text: tk("Join") }),
|
||||
select(
|
||||
ctx,
|
||||
[
|
||||
{ value: "그리고", text: tk("And") },
|
||||
{ value: "또는", text: tk("Or") },
|
||||
],
|
||||
g.join,
|
||||
(v) => put(g.conds, v as "그리고" | "또는"),
|
||||
tk("Join"),
|
||||
),
|
||||
]
|
||||
: []),
|
||||
),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function scaleEditor(ctx: Ctx, rule: Extract<CfRule, { kind: "scale" }>): HTMLElement[] {
|
||||
return [
|
||||
el("p", { className: "ui-text ui-text--muted", text: tk("ScaleHint") }),
|
||||
row(
|
||||
select(
|
||||
ctx,
|
||||
SCALE_OPS.map((o) => ({ value: o, text: o })),
|
||||
rule.op,
|
||||
(v) => setRule(ctx, { ...rule, op: v as typeof rule.op }),
|
||||
tk("Kinds"),
|
||||
),
|
||||
numberBox(
|
||||
ctx,
|
||||
rule.value,
|
||||
(v) => setRule(ctx, { ...rule, value: typeof v === "number" ? v : rule.value }),
|
||||
tk("Value"),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function roundEditor(
|
||||
ctx: Ctx,
|
||||
rule: Extract<CfRule, { kind: "round" }>,
|
||||
unit: string,
|
||||
): HTMLElement[] {
|
||||
const put = (next: Partial<CfRound>): void => setRule(ctx, { ...rule, ...next });
|
||||
const places = Array.from({ length: 7 }, (_, n) => ({ value: String(n), text: tk("Places", n) }));
|
||||
const hint = el("span", { className: "m02-dflink-note" });
|
||||
void suggestPlaces(unit).then((n) => {
|
||||
if (n === null || n === rule.places || ctx.readOnly) return;
|
||||
hint.append(
|
||||
tk("Suggest", n),
|
||||
" ",
|
||||
createButton({
|
||||
label: tk("Apply"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => put({ places: n }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
return [
|
||||
row(
|
||||
select(ctx, places, String(rule.places), (v) => put({ places: Number(v) }), tk("K_round")),
|
||||
select(
|
||||
ctx,
|
||||
(["반올림", "올림", "버림"] as const).map((m) => ({ value: m, text: m })),
|
||||
rule.method,
|
||||
(v) => put({ method: v as CfRound["method"] }),
|
||||
tk("K_round"),
|
||||
),
|
||||
hint,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
const DEFAULT: Record<Exclude<Kind, "none">, (from: CfPort) => CfRule> = {
|
||||
lookup: () => ({ kind: "lookup", conds: [], col: "" }),
|
||||
gate: (from) => makeGate([{ from, op: "≥", value: 0 }]),
|
||||
scale: () => ({ kind: "scale", op: "×", value: 1 }),
|
||||
round: () => ({ kind: "round", places: 0, method: "반올림" }),
|
||||
};
|
||||
|
||||
/** 선 규칙 판 그리기 — 선을 고르면 부름 · 바뀐 lg 는 on.change 로만 알림(그리기는 부른 쪽이 다시) */
|
||||
export function renderLinkPanel(
|
||||
host: HTMLElement,
|
||||
lg: CfLogic,
|
||||
linkId: string,
|
||||
on: { change(lg: CfLogic): void },
|
||||
opts: DetailOptions & { readOnly?: boolean } = {},
|
||||
): void {
|
||||
const link = lg.links.find((l) => l.id === linkId);
|
||||
host.classList.add("m02-dflink");
|
||||
if (!link) return host.replaceChildren();
|
||||
const ev = evalSafe(lg, opts);
|
||||
const table = valueOf(ev, link.from);
|
||||
const headers =
|
||||
Array.isArray(table) && Array.isArray(table[0])
|
||||
? table[0].map((h) => String(h ?? "")).filter(Boolean)
|
||||
: [];
|
||||
const ctx: Ctx = {
|
||||
lg,
|
||||
linkId,
|
||||
readOnly: !!opts.readOnly,
|
||||
headers,
|
||||
detail: opts,
|
||||
change: on.change,
|
||||
};
|
||||
const rule = link.rule;
|
||||
const kind: Kind = rule?.kind ?? "none";
|
||||
const parts: HTMLElement[] = [
|
||||
el("div", {
|
||||
className: "m02-dflink-head",
|
||||
children: [
|
||||
el("strong", {
|
||||
text: `${portText(lg, link.from)} → ${rule ? `(${labelOf(rule)}) → ` : ""}${portText(lg, link.to)}`,
|
||||
}),
|
||||
el("span", { className: "m02-dflink-note", text: `(${unitOf(lg, link.from)})` }),
|
||||
],
|
||||
}),
|
||||
row(
|
||||
el("span", { className: "m02-dflink-label", text: tk("Kinds") }),
|
||||
...KINDS.map((k) =>
|
||||
createButton({
|
||||
label: tk(`K_${k}` as "K_none"),
|
||||
variant: k === kind ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
disabled: ctx.readOnly,
|
||||
onClick: () =>
|
||||
k !== kind && setRule(ctx, k === "none" ? undefined : DEFAULT[k](link.from)),
|
||||
}),
|
||||
),
|
||||
),
|
||||
];
|
||||
if (rule?.kind === "lookup") parts.push(...lookupEditor(ctx, rule));
|
||||
if (rule?.kind === "gate") parts.push(...gateEditor(ctx, rule));
|
||||
if (rule?.kind === "scale") parts.push(...scaleEditor(ctx, rule));
|
||||
if (rule?.kind === "round") parts.push(...roundEditor(ctx, rule, unitOf(lg, link.from)));
|
||||
|
||||
const pv = preview(link, ev);
|
||||
parts.push(
|
||||
row(
|
||||
el("span", { className: "m02-dflink-label", text: tk("Before") }),
|
||||
el("span", {
|
||||
className: "ui-badge ui-badge--pill ui-badge--neutral",
|
||||
attrs: { "data-size": "20" },
|
||||
text: pv.before,
|
||||
}),
|
||||
"→",
|
||||
el("span", { className: "m02-dflink-label", text: tk("After") }),
|
||||
el("span", {
|
||||
className: "ui-badge ui-badge--pill ui-badge--neutral",
|
||||
attrs: { "data-size": "20" },
|
||||
text: pv.after,
|
||||
}),
|
||||
),
|
||||
row(
|
||||
el("span", { className: "m02-dflink-label", text: tk("Label") }),
|
||||
el("span", { text: labelOf(rule) }),
|
||||
),
|
||||
);
|
||||
|
||||
const warns: string[] = linkNotes(link, ev);
|
||||
if (rule?.kind === "lookup") {
|
||||
if (!rule.conds.length) warns.push(tk("WarnLookup"));
|
||||
if (!rule.col) warns.push(tk("WarnTake"));
|
||||
}
|
||||
parts.push(
|
||||
...warns.map((w) =>
|
||||
el("p", { className: "ui-banner ui-banner--warning m02-dflink-warn", text: `⚠ ${w}` }),
|
||||
),
|
||||
);
|
||||
host.replaceChildren(...parts);
|
||||
}
|
||||
Reference in New Issue
Block a user