import { CSSProperties } from 'react'; import { CSSResultGroup } from 'lit'; import { default as default_2 } from 'react'; import { JSX } from 'react'; import { PropertyValues } from 'lit'; import { ReactNode } from 'react'; import { ReactWebComponent } from '@lit/react'; import { TemplateResult } from 'lit-html'; import { UElement } from '@iyulab/components/dist/components/UElement.js'; /** * L2 폼 행 — 기본 2열, `full` 이면 1열. * * 오버라이드 계약(docs/lob-layers.md §2)에 따라 `className`·`style` 을 받아 병합한다. * `columns` 는 2열 고정이 맞지 않는 소비자가 **컴포넌트를 복제하지 않고** 조정하는 경로다 * — 복제가 시작되면 이 계층은 쓰기 전보다 나쁜 상태를 만든다. * * 🔴**트랙은 `minmax(0, 1fr)` 이지 `1fr` 이 아니다 — 순수 `1fr` 은 «균등» 을 약속하고** * **지키지 않는다.** 그리드 아이템의 기본 `min-width: auto` 는 내용의 min-content 아래로 * 줄어들기를 거부하므로, 한 칸에 긴 내용이 들어오면 그 칸만 부풀고 나머지가 찌그러진다. * 실측(400px 컨테이너 · 한 칸에 긴 불가분 문자열): 2열이 **425/8**, 3열이 **425/8/8** 이었고 * 행 자체가 컨테이너를 넘쳤다. `minmax(0, 1fr)` 은 트랙의 최소를 0 으로 만들어 **자식을** * **건드리지 않고** 이를 고친다 — 같은 실측에서 **196/196** · **128/128/128**. * * ⚠**긴 불가분 내용 자체의 넘침은 별개 축이고 이 컴포넌트의 몫이 아니다.** 칸이 균등해져도 * 그 안의 긴 문자열은 여전히 자기 칸을 넘는다 — 그것은 셀의 `overflow-wrap` 이 답이다 * (`@iyulab/components` 의 엘리먼트는 이미 그 값을 갖는다). 같은 실측에서 `overflow-wrap` 을 * 주면 행의 넘침이 0 이 됐다. * * 계약은 `tests/form-layout-contract.test.ts` 가 고정한다 — 다만 그 파일은 **«선언» 을 재지** * **«배치» 를 재지 않는다**(이 패키지엔 레이아웃을 계산하는 테스트 자리가 없다). 위 수치는 * 형제 패키지의 브라우저 프로젝트를 계측기로 빌린 일회성 탐침으로 쟀다. */ export declare function FormRow({ children, full, columns, className, style, }: { children: ReactNode; /** 한 행 전체를 한 칸으로 쓴다. `columns` 보다 우선한다. */ full?: boolean; /** 열 수. 기본 2. */ columns?: number; className?: string; style?: CSSProperties; }): JSX.Element; /** * L2 폼 블록 — 제목이 붙은 필드 묶음. * * 🔴**제목에 `text-transform: uppercase` 와 `letter-spacing` 을 걸지 않는다**(0.7.0 에서 제거). * ⑴ 한글에는 대문자가 없어 `uppercase` 가 **아무 효과가 없고**, 제목에 영문이 섞이면 * 그것만 커져 오히려 어수선해진다. * ⑵ 양수 `letter-spacing` 은 **한글 가독성을 떨어뜨린다** — 라틴 소문자 조판 관례를 * 그대로 옮기면 안 되는 자리다. * ⇒ 위계는 **크기·굵기·색** 세 신호로만 만든다(타입 스케일의 `label` 단). * 영문 전용 UI 에서 눈썹 텍스트 느낌이 필요하면 `titleStyle` 로 `--u-text-overline-*` * 단을 직접 지정한다 — 그 단은 영문·숫자 라벨을 전제하고 양수 자간을 갖는다. * * 오버라이드 계약(docs/lob-layers.md §2): 구조는 두고 **children 치환 + prop** 으로 바꾼다. * ⚠기본 스타일은 병합 가능한 형태로만 둔다 — 호출자의 `style` 이 뒤에 오므로 필요한 * 항목만 골라 덮을 수 있다. 인라인으로 굳혀 두면 소비자 CSS 가 이길 수 없어(`!important` * 외) 계약이 원천적으로 무효가 된다. */ export declare function FormSection({ title, children, className, style, titleStyle, }: { title: ReactNode; children: ReactNode; className?: string; style?: CSSProperties; /** 제목 줄만 따로 조정할 때. 블록을 통째로 갈아엎지 않기 위한 훅이다. */ titleStyle?: CSSProperties; }): JSX.Element; /** * React wrapper for ``. `source` is a property (an object), which a raw custom element in JSX * stringifies under React 18 — the wrapper assigns it as a property on both React 18 and 19. * The skeleton fires no events of its own; listen to the view (`row-activate`, `selection-change`) directly. */ export declare const ListPage: ReactWebComponent; /** 외형 — `plain`(기본: 영역이 간격만 두고 쌓인다) · `card`(툴바 · 뷰 · 페이저를 한 면에 — 테두리 하나, 영역 사이 선). */ declare type ListPageAppearance = 'plain' | 'card'; export declare type ListPageProps = default_2.ComponentProps; /** 목록이 지금 보이는 상태 — 호스트의 `status` 속성으로 반영된다. */ declare type ListPageStatus = 'loading' | 'error' | 'empty' | 'ready'; /** * 목록 화면의 골격 — 머리 · 필터 · 툴바 · 뷰 · 페이저 · 빈/오류 영역을 슬롯으로 두고, 프레임워크 중립 소스를 뷰와 페이저에 * 묶는다(`bindSource`). * * 골격은 **표를 그리지 않는다.** 무엇을 그릴지는 `slot="view"` 에 놓인 요소가 정한다 — «뷰 어휘»(`data` · `loading` · `error` · * `sortCriteria` · `page` · `pageSize` · `totalCount` 를 받고 `sort-change` · `page-change` 를 내는 것)를 말하는 요소면 무엇이든 * (flex-table · u-rich-table · u-data-view). 페이저(`slot="pager"`, 예: `u-pagination`)도 같은 소스에 묶인다. * * - **뷰 전환**: 뷰 요소마다 `view-name` 을 주고 골격의 `view` 에 보일 이름을 준다 — 나머지 뷰에는 골격이 `hidden` 을 건다 * (뷰 요소의 `hidden` 은 골격이 소유한다). `view` 가 없으면 첫 뷰가 보인다. 숨은 뷰도 소스에 묶인 채라 전환이 즉시다. * - **검색**: 머리 · 필터 · 툴바 영역의 요소가 낸 `search {query}` 를 소스의 `setSearch` 로 돌린다. 검색 가능한 선택 * (`u-select searchable`)이 선택지를 거르며 내는 같은 이름의 이벤트는 목록 검색어가 아니다 — 필터에 그대로 둔다. * - **빈·오류**: 소스가 오류면 `slot="error"`, 결과가 비면 `slot="empty"` 를 뷰와 페이저 대신 보인다. 그 슬롯이 비어 있으면 * 뷰가 자기 표시(두 표가 이미 그리는 오류·빈 문구)를 그대로 보인다. * - **상태**: 호스트 `status` 속성(`loading` · `error` · `empty` · `ready`) — 소비자 CSS 와 시험이 읽는다. * - **적용된 조건**: `slot="applied"` 에 지금 걸린 조건(예: `u-chip removable`)을 둔다 — 필터 아래 한 줄로 감기고, * 마지막 칩이 빠지면 영역째 접힌다. 칩을 지웠을 때 조건을 푸는 것은 화면의 몫이다(칩의 `remove` 를 듣는다). * - **카드**: `appearance="card"` 면 툴바 · 뷰(또는 빈·오류) · 페이저가 한 면에 놓인다 — 테두리 하나, 영역 사이 선. * 뷰가 자기 테두리를 그리면(`flex-table` · `u-rich-table`) 면 안에서는 그 테두리를 걷는다. * * - **선택 상태**: 보이는 뷰가 `selection-change {selectedIds}` 로 하나라도 골랐다고 말하는 동안 `:state(selected)` 가 켜진다 — * 카드 툴바를 일괄 동작 막대로 칠하는 손잡이다(`u-list-page:state(selected)::part(toolbar)`). 숨은 뷰의 선택은 세지 않는다. * * 선택과 행 열기는 묶지 않는다 — 소스의 것이 아니라 화면의 것이라, 뷰에서 직접 듣는다(골격은 선택을 읽어 상태로만 낸다). * 레이아웃이 전혀 다르면 이 요소 없이 `bindSource` 만 쓴다. * * @slot header - 화면 머리(예: `u-page-header` — 그 `actions` 슬롯에 화면 고유 버튼) * @slot filters - 화면 고유 검색 조건 — `search {query}` 를 내면 소스 검색어가 된다 * @slot applied - 지금 걸린 조건(칩) — 비면 영역째 접힌다 * @slot toolbar - 뷰 위의 동작 줄 * @slot view - 뷰 요소(여럿이면 `view-name` 으로 고른다) — 뷰를 감싼 레이아웃도 된다(소스는 그 안의 첫 뷰에) * @slot pager - 페이저(예: `u-pagination`) — 소스에 묶인다 * @slot empty - 결과가 없을 때 뷰 대신 보일 것 * @slot error - 소스가 오류일 때 뷰 대신 보일 것 * * @csspart region - 각 영역의 상자(영역 이름 part 와 함께 붙는다) * @csspart header * @csspart filters * @csspart applied * @csspart surface - 툴바 · 뷰 · 빈·오류 · 페이저를 담는 상자(`appearance="card"` 의 면 — `plain` 에서는 상자를 만들지 않는다) * @csspart toolbar * @csspart view * @csspart pager * @csspart status - 빈·오류 영역 * * @cssprop --list-page-gap - 영역 사이 간격(기본 `var(--u-space-md)`) */ declare class UListPage extends UElement { static styles: CSSResultGroup[]; /** 목록의 데이터 소스 — `createODataSource` · `createArraySource`(flex-table)의 모양(`ViewSource`). */ source?: ViewSource; /** 보일 뷰의 `view-name`. 없으면 첫 뷰. */ view?: string; /** 외형 — `plain`(기본) · `card`(툴바 · 뷰 · 페이저를 한 면에). */ appearance: ListPageAppearance; /** 목록 상태 — 소스에서 도출되며 속성으로 반영된다(읽기 전용으로 쓴다). */ status: ListPageStatus; /** 채워진 영역. */ private filled; private bound; private unsubscribe?; /** 뷰마다 마지막으로 말한 선택 수 — `selection-change {selectedIds}` 에서. 보이는 뷰의 것만 `:state(selected)` 를 켠다. */ private selections; /** 지금 보이는 뷰(`applyView`). */ private activeView?; /** * `:state(selected)` 을 싣는 자리 — 보이는 뷰에 선택이 있는 동안(`u-select:state(open)` 과 같은 관례). * ⚠생성자에서 한 번만 붙인다 — 같은 인스턴스에 두 번 부르면 `NotSupportedError` 다. */ private readonly internals; constructor(); connectedCallback(): void; disconnectedCallback(): void; /** 자식이 들어오거나 `slot` 이 바뀌면 — 골격이 그리지 않는 자식을 잰다. */ private childWatch; /** * 골격은 이름 있는 영역만 그린다(기본 슬롯이 없다) — `slot` 이 없거나 모르는 이름인 자식은 화면에 나오지 않고 오류도 없다. * 골격 안에 슬롯 없이 둔 서랍 · 대화상자는 `open` 이 되어도 보이지 않는다. 요소마다 한 번 말한다. */ private warnUnslotted; protected willUpdate(changed: PropertyValues): void; protected updated(changed: PropertyValues): void; render(): TemplateResult<1>; /** 뷰 대신 보일 영역 — 그 상태이고 그 슬롯이 채워졌을 때만. */ private panel; private assigned; private onSlotChange; /** 뷰 요소의 `hidden` — 보일 뷰 하나만 연다. */ private applyView; /** 보이는 뷰의 선택 수 → `:state(selected)`. */ private applySelected; /** * 뷰 영역의 요소가 낸 `selection-change {selectedIds}` → 그 뷰의 선택 수. 선택을 소스에 묶지 않는다 — 읽어서 상태로만 낸다. * 뷰 밖(머리 · 필터 · 툴바)에서 난 같은 이름의 이벤트는 목록의 선택이 아니다. * ⚠뷰 영역은 슬롯 배정이 아니라 자식의 `slot` 으로 가른다 — 골격이 처음 그려지기 전(슬롯이 아직 없을 때) 뷰가 알린 초기 선택도 * 잃지 않는다. 직계 자식의 `slot="view"` 가 곧 그 슬롯의 배정이다. */ private onSelectionChange; private attachSource; private detachSource; /** 소스 상태 → `status`. 처음 불러오는 동안만 `loading` — 이미 행이 있으면 새로 고치는 동안에도 `ready` 다. */ private readStatus; /** 뷰·페이저 영역의 요소를 소스에 묶는다 — 새로 온 것은 묶고 떠난 것은 푼다. */ private syncBindings; /** * 소스를 꽂을 요소 — 슬롯 요소 자신이 뷰 어휘를 말하면 그것, 아니면 그 안(라이트 DOM)의 첫 뷰. 레이아웃이 뷰를 감싸고 슬롯에 * 들어온 구성(`` 안의 표 + `slot="detail"` 미리보기)에서 미리보기가 필터 줄이 아니라 * 뷰의 높이부터 선다. 뷰는 쓸 수 있는 `data`, 페이저는 `page`. * @returns 찾은 요소 · `null`(없다 — 경고) · `undefined`(아직 정의되지 않은 커스텀 요소가 있어 판정하지 않는다) */ private bindTarget; /** 요소와 그 안에서 아직 정의되지 않은 커스텀 요소 이름. */ private undefinedTags; /** * 슬롯 요소에도 그 안에도 뷰 어휘가 없다 — 소스가 아무것도 쓰지 못해 표는 빈 채로, 페이저는 건수를 말한 채로 남는다. 오류가 * 없어 아무도 모르므로 한 번 말한다. */ private warnVocabulary; /** * 머리 · 필터 · 툴바에서 온 `search {query}` → 소스 검색어. 뷰·페이저의 것은 그 묶음이 이미 듣고, 선택지 질의 * (`OPTION_QUERY_EMITTERS`)는 목록 검색이 아니다 — 영역의 자식 자신이든 그 안쪽(예: `u-field` 가 감싼 선택)이든. */ private onSearch; } /** * `bindSource` — 프레임워크 중립 데이터 소스를 «뷰 어휘» 를 말하는 요소에 묶는다. * * 두 표(`flex-table` · `u-rich-table`) · 카드 뷰(`u-data-view`) · 페이저(`u-pagination`)가 같은 이름으로 상태를 받고 * 같은 모양으로 이벤트를 낸다(루트 `table-vocab-check` 가 그 어휘를 잰다). 그러므로 «목록 화면» 의 배선은 뷰마다 다르지 * 않다 — 소스 상태를 뷰의 속성에 쓰고, 뷰의 이벤트를 소스 조작으로 돌린다. 그 한 가지를 여기서 한다. * * 소스는 구조로만 안다(`ViewSource`) — `@iyulab/flex-table/odata` 의 `createODataSource`·`createArraySource` 가 그 모양이지만 * 이 패키지는 flex-table 에 의존하지 않는다. 뷰도 구조로만 안다 — 요소가 **가진** 속성만 쓰고(카드 뷰에는 정렬이 없다), * 이벤트를 내지 않는 요소에는 리스너가 그냥 아무 일도 하지 않는다. */ /** 정렬 기준 — 두 표와 소스가 주고받는 모양. */ declare interface ViewSortCriterion { key: string; direction: 'asc' | 'desc'; } /** `bindSource` 가 읽고 조작하는 소스 — flex-table 의 두 소스가 이 모양이다. */ declare interface ViewSource { getState(): ViewSourceState; subscribe(listener: () => void): () => void; setPage(page: number): void; setPageSize(size: number): void; setSort(criteria: ViewSortCriterion[]): void; setSearch(term: string): void; } /** 뷰에 쓰는 소스 상태 — `createODataSource(...).getState()` 와 같은 모양(구조적). */ declare interface ViewSourceState { data: T[]; totalCount: number; loading: boolean; error: { message: string; } | null; page: number; pageSize: number; sortCriteria: ViewSortCriterion[]; search: string; } export { } declare global { interface HTMLElementTagNameMap { 'u-list-page': import('@iyulab/enterprise/list-page').UListPage; } }