Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1

This commit is contained in:
2026-10-05 12:34:54 +09:00
9 changed files with 104 additions and 71 deletions
@@ -147,7 +147,7 @@ export function renderInputPanel(
children: [...GROUPS, "다른 로직" as const, "변수" as const].map((k) =>
createButton({
label: k === "변수" ? t("Variable") : k === "다른 로직" ? t("Logic") : t(k),
variant: k === kind ? "filled" : "pill",
variant: k === kind ? "filled" : "ghost",
size: "sm",
onClick: () => k !== kind && on.change(setKind(lg, boxId, k)),
}),
@@ -193,7 +193,7 @@ function renderGroup(
...(["fixed", "rule"] as const).map((m) =>
createButton({
label: m === "fixed" ? t("ModeFixed") : t("ModeRule"),
variant: mode === m ? "filled" : "pill",
variant: mode === m ? "filled" : "ghost",
size: "sm",
onClick: () => ((mode = m), drawChips(), draw()),
}),
@@ -445,7 +445,7 @@ export function renderMaterial(
...(["fixed", "choice", "rule"] as CfMaterialMode[]).map((m) =>
createButton({
label: m === "fixed" ? t("Fixed") : m === "choice" ? t("Choice") : t("Rule"),
variant: mode === m ? "filled" : "pill",
variant: mode === m ? "filled" : "ghost",
size: "sm",
onClick: () => ((mode = m), drawChips(), drawBody()),
}),
@@ -92,18 +92,20 @@ export function buildPicker(opt: PickerOptions): { root: HTMLElement } {
let seq = 0;
const buttons = tabs.map((tab) => {
const b = el("button", {
className: "m02-layers__btn",
text: tab.label,
attrs: { type: "button", "data-source": tab.id },
});
b.addEventListener("click", () => void show(tab));
const b = createButton({ label: tab.label, variant: "ghost", onClick: () => void show(tab) });
b.classList.add("m02-layers__btn");
b.dataset.source = tab.id;
return b;
});
async function show(tab: Source): Promise<void> {
const mine = ++seq;
buttons.forEach((b) => b.classList.toggle("is-active", b.dataset.source === tab.id));
buttons.forEach((b) => {
const on = b.dataset.source === tab.id;
b.classList.toggle("is-active", on);
b.classList.toggle("ui-btn--filled", on);
b.classList.toggle("ui-btn--ghost", !on);
});
people.root.hidden = tab.id !== "people";
list.message();
try {
@@ -42,28 +42,6 @@
gap: var(--spacing-4);
}
.m02-layers__btn {
padding: var(--spacing-4) var(--spacing-8);
border: 1px solid var(--color-border);
border-radius: var(--radius-buttons);
background: var(--color-surface);
color: var(--color-text);
font: inherit;
font-size: var(--text-body-sm);
cursor: pointer;
}
.m02-layers__btn:hover {
background: var(--color-mist-violet);
}
.m02-layers__btn.is-active {
border-color: var(--color-accent);
background: var(--color-mist-violet);
color: var(--color-accent);
font-weight: 600;
}
/* 층 단추 줄 오른쪽 끝 — 「N개 고름」 · 「+」(PLAN 30-2) */
.m02-tree__tools {
display: inline-flex;
@@ -94,28 +72,6 @@
background: var(--color-mist-violet);
}
/* 층 단추 옆 「+」 — 나무 맨 위 폴더 · 항목 더하기(PLAN 29-6) */
.m02-tree__add {
min-width: 1.75rem;
padding: var(--spacing-4) var(--spacing-8);
border: 1px dashed var(--color-border);
border-radius: var(--radius-buttons);
background: none;
color: var(--color-text-secondary);
font: inherit;
font-weight: 600;
cursor: pointer;
}
.m02-tree__add:hover {
border-color: var(--color-accent);
color: var(--color-accent);
}
.m02-tree__add[hidden] {
display: none;
}
/* 남의 회사 「개인」 사람 나무 — 고른 사람 줄 아래 그 사람 라이브러리(PLAN 29-6) */
.m02-people__panel {
padding-left: var(--spacing-16);
@@ -10,7 +10,13 @@
* ========================================================================== */
import "./M02_MasterTemplete_TreePanel.css";
import { createInputField, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
import {
createButton,
createInputField,
el,
showConfirmDialog,
showToast,
} from "@ui/ui_template_elements";
import { loadingError, showLoading } from "@ui/ui_template_loading";
import { t as L } from "@ui/ui_template_locale";
import {
@@ -141,13 +147,15 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
const root = el("div", { className: "m02-tree", children: [body] });
search.root.classList.add("m02-tree__search");
// 층 단추 옆 「+」 — 맨 위에 폴더 · 항목 더하기(뿌리 줄 오른쪽 클릭을 갈음)
const add = el("button", {
className: "m02-tree__add",
text: "+",
attrs: { type: "button", title: L("M02_TreeAddTop"), "aria-label": L("M02_TreeAddTop") },
const add = createButton({
label: "+",
variant: "ghost",
onClick: (event) => openTreeMenu(event, topMenu()),
});
add.classList.add("m02-tree__add");
add.title = L("M02_TreeAddTop");
add.setAttribute("aria-label", L("M02_TreeAddTop"));
add.hidden = !opt.canEdit;
add.addEventListener("click", (event) => openTreeMenu(event, topMenu()));
// 「N개 고름」 · 「+」 — 층 단추 줄 오른쪽 끝(30-2 · 부르는 쪽이 붙임)
const status = el("span", { className: "m02-tree__picked", attrs: { hidden: "" } });
const tools = el("span", { className: "m02-tree__tools", children: [status, add] });
@@ -9,7 +9,7 @@
* 누르면 `go`(부르는 쪽이 해시로 다시 엶).
* ========================================================================== */
import { el } from "@ui/ui_template_elements";
import { createButton, el } from "@ui/ui_template_elements";
import { t as L } from "@ui/ui_template_locale";
import { createTreeRow } from "@ui/ui_template_tree";
import type { SessionUser } from "../A06_Login/A06_Login_Api_Fetch";
@@ -67,15 +67,17 @@ export function createLayerBar(opt: LayerBarOptions): HTMLElement {
: "M02_PickerPersonal",
);
const current = root === active.root && (root !== "personal" || people || !active.owner);
const b = el("button", {
className: `m02-layers__btn${current ? " is-active" : ""}`,
text: readOnly ? `${name} · ${L("M02_RootReadOnly")}` : name,
attrs: { type: "button", "data-root": root, "aria-pressed": String(current) },
});
b.addEventListener("click", () => {
if (!current) opt.go(root, owner);
else opt.reselect?.();
const b = createButton({
label: readOnly ? `${name} · ${L("M02_RootReadOnly")}` : name,
variant: current ? "filled" : "ghost",
onClick: () => {
if (!current) opt.go(root, owner);
else opt.reselect?.();
},
});
b.classList.add("m02-layers__btn");
b.dataset.root = root;
b.setAttribute("aria-pressed", String(current));
return b;
}
+1 -1
View File
@@ -282,7 +282,7 @@ def test_켜진_층_단추는_목록으로_복사_끌기():
"""30-4 — 켜진 층 단추를 다시 누르면 그 층 목록(구조물 = 집계표 · 일위대가 단추 벌은 39-2 에서 지움) ·
30-2 · 39-6 「〇〇로 복사…」 = 쓸 수 있는 층(지금 층 뺌 · 폴더 고름) · 끌어다 옮기기는 쓸 수 있는 층만."""
roots = _text(M02 / "M02_MasterTemplete_TreePanel_Roots.ts")
assert "if (!current) opt.go(root, owner);\n else opt.reselect?.();" in roots
assert "if (!current) opt.go(root, owner);\n else opt.reselect?.();" in roots
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer))' in page
# 39-6 — 쓸 수 있는 층 전부(볼 회사 주인까지 · 지금 층 뺌) · 대상 폴더 고르는 창
@@ -0,0 +1,64 @@
/* 45-1 — M02 단추 = 공용 기본 단추(알약 꼴 아님) · 알약은 그대로 알약
* 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 ·
* 나무에 item1(구조물 조합)이 있음
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
* 알약 꼴 = 모서리 반지름 ≥ 높이 / 2 - 1(캡슐) · 진짜 알약 = 알약 · 칩 · 이름표 클래스(.ss-link · *__pill · *-chip · .ui-tag)
* 기대 수치:
* - pillButtons = [](M02 안 보이는 단추 중 알약 꼴 0 · 머리 공용 메뉴는 M02 밖이라 뺌)
* - layers = 층 단추가 ui-btn · 켜진 층은 filled · add = 「+」 가 ui-btn--ghost
* - smRadius = 4(작은 단추 모서리 = 8 / 2) · buttons ≥ 20
* - pillOn: [알약] 켬 → realPills > 0 · 모두 알약 꼴(realRound = realPills) → 끔으로 되돌림(pressedAfter = "false")
*/
(async () => {
const w = (ms) => new Promise((r) => setTimeout(r, ms));
const until = async (fn, ms = 6000) => {
for (let t = 0; t < ms; t += 150) {
const v = fn();
if (v) return v;
await w(150);
}
return null;
};
const vis = (b) => b && !b.hidden && b.offsetParent !== null;
const round = (e) => {
const r = e.getBoundingClientRect();
return parseFloat(getComputedStyle(e).borderTopLeftRadius) >= r.height / 2 - 1;
};
const REAL = ".ss-link, [class*='__pill'], [class*='-chip'], [class*='__chip'], .ui-tag";
const scope = () =>
[...document.querySelectorAll("button")].filter(
(b) => vis(b) && !b.closest("header, .app-header") && !b.matches(REAL),
);
const out = {};
const node = await until(() => document.querySelector('[data-node="i|item1"]'));
if (!node) return JSON.stringify({ error: "item1 없음 — 기슭막이 폴더를 펼쳐 둘 것" });
node.click();
await until(() => document.querySelector(".m02-struct__card-tools .ui-btn"));
await w(1500);
const all = scope();
out.buttons = all.length;
out.pillButtons = all
.filter((b) => round(b) && b.getBoundingClientRect().height > 0)
.map((b) => `${b.innerText.trim().slice(0, 12)} | ${b.className}`);
out.layers = [...document.querySelectorAll(".m02-layers__btn")].map(
(b) => `${b.innerText.trim()}:${b.classList.contains("ui-btn--filled") ? "filled" : b.classList.contains("ui-btn--ghost") ? "ghost" : b.className}`,
);
out.add = document.querySelector(".m02-tree__add")?.className ?? null;
const sm = all.find((b) => b.classList.contains("ui-btn--sm"));
out.smRadius = sm ? parseFloat(getComputedStyle(sm).borderTopLeftRadius) : null;
// [알약] 켬 → 진짜 알약은 알약 꼴 그대로 → 끔으로 되돌림
const pillBtn = [...document.querySelectorAll(".m02-struct__card-tools button")].find(
(b) => vis(b) && b.innerText.trim() === "알약",
);
if (pillBtn) {
const before = pillBtn.getAttribute("aria-pressed");
if (before !== "true") pillBtn.click();
await w(600);
const real = [...document.querySelectorAll(".ss-link")].filter(vis);
out.pillOn = { realPills: real.length, realRound: real.filter(round).length };
if (before !== "true") pillBtn.click();
await w(300);
out.pillOn.pressedAfter = pillBtn.getAttribute("aria-pressed");
} else out.pillOn = "알약 단추 없음";
return JSON.stringify(out);
})()
+2 -1
View File
@@ -33,10 +33,11 @@ const BASE_CSS = `
.ui-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.ui-btn__icon { display: inline-flex; width: 16px; height: 16px; }
/* 작은 크기 — 다섯 모양 모두에 먹음(padding · 글자만 바꿈) */
/* 작은 크기 — 다섯 모양 모두에 먹음(padding · 글자 · 모서리) · 낮은 키에 8px 모서리면 알약 꼴이라 반으로 */
.ui-btn--sm {
padding: 2px var(--spacing-8);
font-size: var(--text-caption);
border-radius: calc(var(--radius-buttons) / 2);
}
.ui-btn--filled {