feat(720-2): 최상위 폴더 만들기 — 폴더 · 문서 줄 메뉴 + 단가산출 · 일위대가 층 줄 「+」(구조물 조합과 같은 addFolder([]))

- 구조물 조합 나무 「〇〇로 복사…」 몸을 TreePanel_CopyTo 로 뗌(TreePanel 700 → 688 줄 · 동작 같음)
- 낡은 시험 test_m02_layer_screen(복사 대상 글 자리 = CopyTo) · test_m02_precombo_scope(층 줄 add 인자) 새 꼴로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-01 21:21:29 +09:00
co-authored by Claude Opus 5.5
parent 7621fff233
commit 194e1a9c39
8 changed files with 80 additions and 35 deletions
@@ -8,10 +8,11 @@
* 여럿(구조물 조합 나무 꼴 · `TreePanel_Pick`) — Ctrl · Shift 클릭 고름 · 오른쪽 클릭 · Ctrl+C · Ctrl+V ·
* Delete 가 고른 것 전부에 · 복사는 읽을 수 있는 층 어디서나 → 쓸 수 있는 층에 붙여넣기(새 키).
* 문서 누름 = 편집 칸 · 켜진 층 단추 다시 누름 = 목록표(페이지 몫).
* 최상위 폴더(720-2) — 층 단추 옆 「+」(맨 위 메뉴) · 폴더 · 문서 줄 [최상위 폴더 만들기] = 같은 `addFolder([])`.
* ========================================================================== */
import { isCtrlKey } from "../A00_Common/key_shortcut";
import { el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
import { createEmpty } from "@ui/ui_template_empty";
import { t as L } from "@ui/ui_template_locale";
import {
@@ -58,8 +59,8 @@ export interface LibraryScope {
export interface LibraryOptions {
kind: LibraryKind;
scope: () => LibraryScope;
/** 층 단추 줄(페이지가 만듦 · 그릴 때마다 새로 — 켜진 단추가 바뀜) */
bar: () => HTMLElement;
/** 층 단추 줄(페이지가 만듦 · 그릴 때마다 새로 — 켜진 단추가 바뀜) · `add` = 단추 옆 「+」(720-2) */
bar: (add: HTMLElement) => HTMLElement;
confirmLeave: () => Promise<boolean>;
onOpen: (sel: Selection | null) => void;
getOpen: () => Selection | null;
@@ -130,6 +131,15 @@ export function buildLibrarySection(opt: LibraryOptions): LibraryHandle {
className: "ui-tree m02-tree__body",
attrs: { "data-library-tree": kind, tabindex: "0" },
});
// 층 단추 옆 「+」(720-2 · 구조물 조합 꼴) — 맨 위 메뉴 · 읽기만 층 = 숨김(그릴 때)
const add = createButton({
label: "+",
variant: "ghost",
onClick: (event) => openTreeMenu(event, menuAt([])),
});
add.classList.add("m02-tree__add");
add.title = L("M02_TreeAddTop");
add.setAttribute("aria-label", L("M02_TreeAddTop"));
const leaves = new Map<string, HTMLElement>();
const toggled = new Map<string, boolean>(); // 손으로 펴고 접은 폴더 — 다시 그려도 그대로
const targets = new Map<string, Target>();
@@ -255,6 +265,7 @@ export function buildLibrarySection(opt: LibraryOptions): LibraryHandle {
item(opt.text.newHere, () => make(path)),
...(path.length
? [
item(L("M02_TreeAddTopFolder"), () => void addFolder([])),
item(L("M02_TreeRename"), () => void rename(path)),
item(L("M02_Delete"), () => void remove([target])),
{ label: L("M02_TreeCopy").replace("{n}", "1"), run: () => copy([target]) },
@@ -288,12 +299,14 @@ export function buildLibrarySection(opt: LibraryOptions): LibraryHandle {
const item = { key: row.이름, 열: row.이름, 경로: [...folder, label], 이름: label };
const target: Target = { kind: "item", item };
targets.set(id, target);
const off = (): string | undefined => (opt.scope().canWrite ? undefined : tc("ReadOnly"));
const single = (): TreeMenuItem[] => [
{ label: L("M02_TreeAddTopFolder"), run: () => void addFolder([]), disabledReason: off() },
{ label: L("M02_TreeCopy").replace("{n}", "1"), run: () => copy([target]) },
{
label: L("M02_Delete"),
run: () => void remove([target]),
disabledReason: opt.scope().canWrite ? undefined : tc("ReadOnly"),
disabledReason: off(),
},
...(opt.more?.(row.이름, label) ?? []),
];
@@ -424,7 +437,8 @@ export function buildLibrarySection(opt: LibraryOptions): LibraryHandle {
const refresh = async (keep = false): Promise<void> => {
const mine = ++seq;
const at = opt.scope();
barHost.replaceChildren(opt.bar());
add.hidden = !at.canWrite;
barHost.replaceChildren(opt.bar(add));
if (!keep) treeHost.replaceChildren(createEmpty({ text: tc("Loading"), small: true }));
const [got, tree] = await Promise.all([
// 못 받으면 빈 나무(목록표에서 까닭) · 폴더를 못 받으면 문서는 모두 뿌리