@iyulab/modern-app 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/dist/components/InfoField.d.ts +29 -1
- package/dist/components/InfoField.js +16 -5
- package/dist/components/InfoField.styles.js +27 -0
- package/dist/components/MasterDetailLayout.d.ts +70 -0
- package/dist/components/MasterDetailLayout.js +59 -0
- package/dist/components/MasterDetailLayout.styles.d.ts +1 -0
- package/dist/components/MasterDetailLayout.styles.js +72 -0
- package/dist/components/Wizard.d.ts +97 -0
- package/dist/components/Wizard.js +122 -0
- package/dist/components/Wizard.styles.d.ts +1 -0
- package/dist/components/Wizard.styles.js +114 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -2
- package/dist/internals/locale.d.ts +7 -0
- package/dist/internals/locale.js +5 -1
- package/dist/layouts/SidebarLayout.js +2 -2
- package/dist/layouts/SidebarLayout.styles.js +10 -1
- package/package.json +1 -1
- package/skills/modern-app/references/components/info-field.md +5 -1
- package/skills/modern-app/references/components/master-detail-layout.md +66 -0
- package/skills/modern-app/references/components/wizard.md +84 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.16.0] - 2026-08-11
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **`SidebarLayout`'s main content area now ships a default padding**
|
|
8
|
+
(`var(--u-space-3xl, 32px)`) instead of none. Every consumer previously had to add this
|
|
9
|
+
themselves to keep route content off the viewport/sidebar edge. Full-bleed layouts (e.g. a
|
|
10
|
+
dashboard whose table should reach the panel edge) can still opt back to `0` via
|
|
11
|
+
`styles.main`. This is a visible rendering change for any consumer that never set
|
|
12
|
+
`styles.main` — hence the minor bump rather than a patch.
|
|
13
|
+
|
|
14
|
+
## [0.15.0] - 2026-08-11
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`u-info-field` accepts `size` (`'default' | 'lg'`), `trend` (`'up' | 'down' | 'flat'`),
|
|
19
|
+
`trendLabel`, and `tone` (`'positive' | 'negative' | 'neutral'`).** `size="lg"` renders the
|
|
20
|
+
value at title type-scale for dashboard KPI tiles, composed inside the existing
|
|
21
|
+
`u-info-section` grid — no new component. `tone` resolves from `trend` when unset
|
|
22
|
+
(`up→positive`, `down→negative`, `flat→neutral`) but an explicit `tone` always wins, since some
|
|
23
|
+
metrics invert the usual direction-to-sentiment mapping. `trendLabel` wording is the consumer's
|
|
24
|
+
responsibility. New `trend` part.
|
|
25
|
+
- **`u-master-detail-layout`** — a responsive master›detail split-pane shell (default slot =
|
|
26
|
+
master, `slot="detail"` = detail; appears when filled, disappears when emptied). Below
|
|
27
|
+
`overlayBreakpoint` (self width, default 760px) the detail pane becomes a full overlay with a
|
|
28
|
+
close button that fires `detail-close`. `masterSize` sets the fixed master-pane width. Parts:
|
|
29
|
+
`master`, `divider`, `detail`, `detail-close`.
|
|
30
|
+
- **`u-wizard`** — a step indicator + panel + Back/Next for multi-step flows. `steps`/
|
|
31
|
+
`active`(controlled)/`orientation`/`linear` props; cancelable `step-change` event
|
|
32
|
+
(`{from, to}`) for validation gating; validation and save/resume stay outside the component.
|
|
33
|
+
Keyboard-operable end to end, focus moves to the panel on every step change, and an
|
|
34
|
+
`aria-live` region announces each transition. Parts: `indicator`, `step`, `panel`, `actions`.
|
|
35
|
+
|
|
3
36
|
## [0.14.0] - 2026-08-10
|
|
4
37
|
|
|
5
38
|
### Added
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { StyledElement } from '../internals/StyledElement.js';
|
|
2
|
-
type ElementParts = 'host' | 'label' | 'value';
|
|
2
|
+
type ElementParts = 'host' | 'label' | 'value' | 'trend';
|
|
3
3
|
export type InfoFieldFormat = 'number' | 'currency' | 'date';
|
|
4
|
+
export type InfoFieldSize = 'default' | 'lg';
|
|
5
|
+
export type InfoFieldTrend = 'up' | 'down' | 'flat';
|
|
6
|
+
export type InfoFieldTone = 'positive' | 'negative' | 'neutral';
|
|
4
7
|
/** 값이 "아직 없음"인가 — `0` 과 `false` 는 **값이다**. */
|
|
5
8
|
export declare function isBlank(v: unknown): boolean;
|
|
6
9
|
/**
|
|
@@ -51,6 +54,31 @@ export declare class InfoField extends StyledElement<ElementParts> {
|
|
|
51
54
|
* omitted, degrades to plain number formatting without a currency symbol (does not throw).
|
|
52
55
|
*/
|
|
53
56
|
currency?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Display size. `'lg'` renders the value at the `title` type-scale step
|
|
59
|
+
* (`--u-text-title-size`/`--u-text-title-weight`) — intended for dashboard KPI tiles
|
|
60
|
+
* composed inside `u-info-section`. Reflects to the `size` attribute so
|
|
61
|
+
* `:host([size="lg"])` styling works.
|
|
62
|
+
*/
|
|
63
|
+
size: InfoFieldSize;
|
|
64
|
+
/**
|
|
65
|
+
* Trend direction (optional). Renders a trend indicator when set. The directional glyph
|
|
66
|
+
* (▲/▼) is decorative (`aria-hidden`) — **pair `trend` with `trendLabel`** so the indicator
|
|
67
|
+
* has an accessible name; `trend` alone conveys direction by color only.
|
|
68
|
+
*/
|
|
69
|
+
trend?: InfoFieldTrend;
|
|
70
|
+
/**
|
|
71
|
+
* Trend copy, e.g. `"+12% vs last month"`. **Wording is the consumer's responsibility** — this
|
|
72
|
+
* component does not compose domain phrasing. Setting this alone (without `trend`) still shows
|
|
73
|
+
* the trend part, toned `neutral` unless `tone` is set.
|
|
74
|
+
*/
|
|
75
|
+
trendLabel?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Explicit tone override. When unset, resolves from `trend` (`up→positive`, `down→negative`,
|
|
78
|
+
* `flat`/unset→`neutral`). **Always wins over inference** — some metrics invert the usual
|
|
79
|
+
* direction-to-sentiment mapping (e.g. a falling "open tickets" count is `positive`).
|
|
80
|
+
*/
|
|
81
|
+
tone?: InfoFieldTone;
|
|
54
82
|
private get hasSlotted();
|
|
55
83
|
private formatValue;
|
|
56
84
|
render(): import('lit-html').TemplateResult<1>;
|
|
@@ -7,11 +7,14 @@ import { customElement as a, property as o } from "lit/decorators.js";
|
|
|
7
7
|
import { formatCurrency as s, formatDate as c, formatNumber as l } from "@iyulab/components";
|
|
8
8
|
//#region src/components/InfoField.ts
|
|
9
9
|
function u(e) {
|
|
10
|
+
return e === "up" ? "positive" : e === "down" ? "negative" : "neutral";
|
|
11
|
+
}
|
|
12
|
+
function d(e) {
|
|
10
13
|
return e == null || typeof e == "string" && e.trim() === "";
|
|
11
14
|
}
|
|
12
|
-
var
|
|
15
|
+
var f = class extends n {
|
|
13
16
|
constructor(...e) {
|
|
14
|
-
super(...e), this.label = "", this.blank = "—", this.numeric = !1;
|
|
17
|
+
super(...e), this.label = "", this.blank = "—", this.numeric = !1, this.size = "default";
|
|
15
18
|
}
|
|
16
19
|
static {
|
|
17
20
|
this.styles = [super.styles, r];
|
|
@@ -23,15 +26,23 @@ var d = class extends n {
|
|
|
23
26
|
return this.format === "currency" ? this.currency ? s(Number(this.value), this.currency) : l(Number(this.value)) : this.format === "number" ? l(Number(this.value)) : this.format === "date" ? c(this.value) : String(this.value);
|
|
24
27
|
}
|
|
25
28
|
render() {
|
|
26
|
-
let e = !this.hasSlotted &&
|
|
29
|
+
let e = !this.hasSlotted && d(this.value), t = this.numeric || this.format === "number" || this.format === "currency", n = this.trend !== void 0 || this.trendLabel !== void 0, r = this.tone ?? u(this.trend), a = this.trend === "up" ? "▲" : this.trend === "down" ? "▼" : "";
|
|
27
30
|
return i`
|
|
28
31
|
<div class="label" part="label">${this.label}</div>
|
|
29
32
|
<div class="value ${t ? "numeric" : ""} ${e ? "blank" : ""}" part="value">
|
|
30
33
|
${this.hasSlotted ? i`<slot></slot>` : e ? this.blank : this.formatValue()}
|
|
31
34
|
</div>
|
|
35
|
+
${n ? i`
|
|
36
|
+
<div class="trend tone-${r}" part="trend">
|
|
37
|
+
${a ? i`<span aria-hidden="true">${a}</span> ` : ""}${this.trendLabel ?? ""}
|
|
38
|
+
</div>
|
|
39
|
+
` : ""}
|
|
32
40
|
`;
|
|
33
41
|
}
|
|
34
42
|
};
|
|
35
|
-
t([o({ type: String }), e("design:type", Object)],
|
|
43
|
+
t([o({ type: String }), e("design:type", Object)], f.prototype, "label", void 0), t([o({ type: String }), e("design:type", Object)], f.prototype, "value", void 0), t([o({ type: String }), e("design:type", Object)], f.prototype, "blank", void 0), t([o({ type: Boolean }), e("design:type", Object)], f.prototype, "numeric", void 0), t([o({ type: String }), e("design:type", Object)], f.prototype, "format", void 0), t([o({ type: String }), e("design:type", String)], f.prototype, "currency", void 0), t([o({
|
|
44
|
+
type: String,
|
|
45
|
+
reflect: !0
|
|
46
|
+
}), e("design:type", Object)], f.prototype, "size", void 0), t([o({ type: String }), e("design:type", Object)], f.prototype, "trend", void 0), t([o({ type: String }), e("design:type", String)], f.prototype, "trendLabel", void 0), t([o({ type: String }), e("design:type", Object)], f.prototype, "tone", void 0), f = t([a("u-info-field")], f);
|
|
36
47
|
//#endregion
|
|
37
|
-
export {
|
|
48
|
+
export { f as InfoField, d as isBlank };
|
|
@@ -28,6 +28,11 @@ var t = e`
|
|
|
28
28
|
font-variant-numeric: tabular-nums;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
:host([size="lg"]) .value {
|
|
32
|
+
font-size: var(--u-text-title-size, 20px);
|
|
33
|
+
font-weight: var(--u-text-title-weight, 700);
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
/* "아직 없음"은 값보다 약하게 — 다만 **읽을 수 있어야 한다.**
|
|
32
37
|
⚠더 흐리게 만들고 싶은 유혹이 있는데, 이 자리가 안 읽히면 사용자는 빈 칸인지
|
|
33
38
|
로딩 중인지 구별하지 못한다. */
|
|
@@ -35,6 +40,28 @@ var t = e`
|
|
|
35
40
|
color: var(--u-txt-color-weak, #757575);
|
|
36
41
|
font-weight: var(--u-text-body-weight, 400);
|
|
37
42
|
}
|
|
43
|
+
|
|
44
|
+
/* size="lg" 보다 우선해야 한다 — blank 는 lg 모드에서도 계속 de-emphasize 상태를 유지해야 한다. */
|
|
45
|
+
:host([size="lg"]) .value.blank {
|
|
46
|
+
font-weight: var(--u-text-body-weight, 400);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.trend {
|
|
50
|
+
font-size: var(--u-text-caption-size, 12px);
|
|
51
|
+
margin-top: var(--u-space-3xs, 2px);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.trend.tone-positive {
|
|
55
|
+
color: var(--u-success-color-strong, #1B5E20);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.trend.tone-negative {
|
|
59
|
+
color: var(--u-danger-color-strong, #C62828);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.trend.tone-neutral {
|
|
63
|
+
color: var(--u-txt-color-weak, #757575);
|
|
64
|
+
}
|
|
38
65
|
`;
|
|
39
66
|
//#endregion
|
|
40
67
|
export { t as styles };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { PropertyValues } from 'lit';
|
|
2
|
+
import { StyledElement } from '../internals/StyledElement.js';
|
|
3
|
+
type ElementParts = 'host' | 'master' | 'divider' | 'detail' | 'detail-close';
|
|
4
|
+
/**
|
|
5
|
+
* master›detail 반응형 split-pane 셸.
|
|
6
|
+
*
|
|
7
|
+
* ★**Vaadin `MasterDetailLayout` 을 선례로 삼되 범위를 의도적으로 줄였다.** 드래그 가능한
|
|
8
|
+
* 스플리터는 v1 스코프 아웃(`masterSize` 는 고정 prop) — "얇은 구조" 원칙상 과하다.
|
|
9
|
+
*
|
|
10
|
+
* 기본 슬롯 = master 영역, `slot="detail"` = detail 영역. **detail 은 채워지면 나타나고
|
|
11
|
+
* 비우면 사라진다** — Vaadin 과 동일 계약. 선택 상태 관리(무엇을 detail 에 보여줄지)는
|
|
12
|
+
* 이 컴포넌트가 하지 않는다 — "그릇이지 디자인이 아니다"(`lob-layers.md` §3). 소비자가
|
|
13
|
+
* `u-rich-table` 의 `selection-change` 등으로 `slot="detail"` 자식을 직접 갈아끼운다.
|
|
14
|
+
*
|
|
15
|
+
* ```html
|
|
16
|
+
* <u-master-detail-layout>
|
|
17
|
+
* <u-rich-table @selection-change=${onSelect}>…</u-rich-table>
|
|
18
|
+
* <div slot="detail">…선택된 레코드 상세…</div>
|
|
19
|
+
* </u-master-detail-layout>
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* 좁은 화면(자기 폭 기준, `overlayBreakpoint` 미만)에서 detail 이 master 위 전체 오버레이로
|
|
23
|
+
* 전환된다 — 오버레이 모드에서만 보이는 작은 닫기 버튼이 `detail-close` 이벤트를 낸다.
|
|
24
|
+
* 이건 구조적 기능(레이아웃 역학)이라 도메인 로직이 아니므로 컴포넌트가 가져도 된다.
|
|
25
|
+
*
|
|
26
|
+
* ★**왜 `@container` 미디어 질의가 아니라 `ResizeObserver` 인가**: `overlayBreakpoint` 는
|
|
27
|
+
* prop 이라 인스턴스마다 다른 값을 가질 수 있는데, CSS `@container` 조건은 빌드 시점에
|
|
28
|
+
* 고정된 값만 받는다(런타임 커스텀 프로퍼티를 조건절에 못 쓴다) — 그래서 자기 폭을 직접
|
|
29
|
+
* 재서 `overlay` 속성을 반영한다. `masterSize`/`overlayBreakpoint` 가 고정 디자인값이
|
|
30
|
+
* 아니라 소비자가 화면마다 바꿀 구조 파라미터이기 때문에 생기는 필연적 차이이지,
|
|
31
|
+
* `PageHeader`/`GroupBox`/`ActionBar` 의 고정 중단점 관행을 벗어나려는 것이 아니다.
|
|
32
|
+
*
|
|
33
|
+
* 오버라이드: `part`(host·master·divider·detail·detail-close) + slot 치환.
|
|
34
|
+
*/
|
|
35
|
+
export declare class MasterDetailLayout extends StyledElement<ElementParts> {
|
|
36
|
+
static styles: import('lit').CSSResultGroup[];
|
|
37
|
+
/** master 영역의 고정 폭(CSS 길이). detail 은 나머지 공간을 채운다. */
|
|
38
|
+
masterSize: string;
|
|
39
|
+
/**
|
|
40
|
+
* 이 값(px, 자기 폭 기준) 미만이면 detail 이 전체 오버레이로 전환된다.
|
|
41
|
+
* `masterSize` + 최소한의 읽기 가능한 detail 폭을 더한 값보다 커야 두 패널이
|
|
42
|
+
* 동시에 설 자리가 있다.
|
|
43
|
+
*/
|
|
44
|
+
overlayBreakpoint: number;
|
|
45
|
+
/**
|
|
46
|
+
* 언어 태그. 비우면 `setDefaultLocale()` 값, 그것도 없으면 영어.
|
|
47
|
+
* 닫기 버튼의 접근성 라벨에만 쓰인다.
|
|
48
|
+
*/
|
|
49
|
+
locale: string;
|
|
50
|
+
/** detail 슬롯 배정 상태 — CSS `:has()` 로는 알 수 없다(`internals/slotted.ts` 참조). */
|
|
51
|
+
private hasDetail;
|
|
52
|
+
private resizeObserver?;
|
|
53
|
+
connectedCallback(): void;
|
|
54
|
+
disconnectedCallback(): void;
|
|
55
|
+
protected updated(changed: PropertyValues): void;
|
|
56
|
+
/**
|
|
57
|
+
* 오버레이 모드의 닫기 버튼이 눌렸을 때. detail 내용을 지우는 것은 소비자 몫이라
|
|
58
|
+
* 컴포넌트 내부에 취소로 막을 동작이 없다 — 그래서 취소 불가 이벤트다(`fire()` 기본값인
|
|
59
|
+
* `cancelable: true` 를 명시적으로 끈다).
|
|
60
|
+
*/
|
|
61
|
+
private handleDetailClose;
|
|
62
|
+
private handleDetailSlotChange;
|
|
63
|
+
render(): import('lit-html').TemplateResult<1>;
|
|
64
|
+
}
|
|
65
|
+
declare global {
|
|
66
|
+
interface HTMLElementTagNameMap {
|
|
67
|
+
'u-master-detail-layout': MasterDetailLayout;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
|
|
3
|
+
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
|
+
import { getLocaleStrings as r } from "../internals/locale.js";
|
|
5
|
+
import { slotHasContent as i } from "../internals/slotted.js";
|
|
6
|
+
import { styles as a } from "./MasterDetailLayout.styles.js";
|
|
7
|
+
import { html as o } from "lit";
|
|
8
|
+
import { customElement as s, property as c, state as l } from "lit/decorators.js";
|
|
9
|
+
import "@iyulab/components/dist/components/button/UButton.js";
|
|
10
|
+
import "@iyulab/components/dist/components/icon/UIcon.js";
|
|
11
|
+
//#region src/components/MasterDetailLayout.ts
|
|
12
|
+
var u = class extends n {
|
|
13
|
+
constructor(...e) {
|
|
14
|
+
super(...e), this.masterSize = "22rem", this.overlayBreakpoint = 760, this.locale = "", this.hasDetail = !1, this.handleDetailClose = () => {
|
|
15
|
+
this.fire("detail-close", { cancelable: !1 });
|
|
16
|
+
}, this.handleDetailSlotChange = (e) => {
|
|
17
|
+
this.hasDetail = i(e.target);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
static {
|
|
21
|
+
this.styles = [super.styles, a];
|
|
22
|
+
}
|
|
23
|
+
connectedCallback() {
|
|
24
|
+
super.connectedCallback(), this.resizeObserver = new ResizeObserver((e) => {
|
|
25
|
+
let t = e[0]?.contentRect.width ?? 0;
|
|
26
|
+
t !== 0 && this.toggleAttribute("overlay", t < this.overlayBreakpoint);
|
|
27
|
+
}), this.resizeObserver.observe(this);
|
|
28
|
+
}
|
|
29
|
+
disconnectedCallback() {
|
|
30
|
+
super.disconnectedCallback(), this.resizeObserver?.disconnect();
|
|
31
|
+
}
|
|
32
|
+
updated(e) {
|
|
33
|
+
super.updated(e), e.has("masterSize") && this.style.setProperty("--_master-size", this.masterSize);
|
|
34
|
+
}
|
|
35
|
+
render() {
|
|
36
|
+
return o`
|
|
37
|
+
<div class="master" part="master"><slot></slot></div>
|
|
38
|
+
<div class="divider ${this.hasDetail ? "" : "empty"}" part="divider"></div>
|
|
39
|
+
<div class="detail ${this.hasDetail ? "" : "empty"}" part="detail">
|
|
40
|
+
<u-button class="detail-close" part="detail-close" variant="ghost"
|
|
41
|
+
aria-label=${r(this.locale || void 0).detailClose}
|
|
42
|
+
@click=${this.handleDetailClose}
|
|
43
|
+
>
|
|
44
|
+
<u-icon lib="internal" name="x"></u-icon>
|
|
45
|
+
</u-button>
|
|
46
|
+
<slot name="detail" @slotchange=${this.handleDetailSlotChange}></slot>
|
|
47
|
+
</div>
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
t([c({
|
|
52
|
+
type: String,
|
|
53
|
+
attribute: "master-size"
|
|
54
|
+
}), e("design:type", Object)], u.prototype, "masterSize", void 0), t([c({
|
|
55
|
+
type: Number,
|
|
56
|
+
attribute: "overlay-breakpoint"
|
|
57
|
+
}), e("design:type", Object)], u.prototype, "overlayBreakpoint", void 0), t([c({ type: String }), e("design:type", Object)], u.prototype, "locale", void 0), t([l(), e("design:type", Object)], u.prototype, "hasDetail", void 0), u = t([s("u-master-detail-layout")], u);
|
|
58
|
+
//#endregion
|
|
59
|
+
export { u as MasterDetailLayout };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import('lit').CSSResult;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { css as e } from "lit";
|
|
2
|
+
//#region src/components/MasterDetailLayout.styles.ts
|
|
3
|
+
var t = e`
|
|
4
|
+
:host {
|
|
5
|
+
display: flex;
|
|
6
|
+
position: relative;
|
|
7
|
+
min-width: 0;
|
|
8
|
+
height: 100%;
|
|
9
|
+
/* 부모(페이지 셸)가 높이를 정해 준다는 전제 — GroupBox/ActionBar 와 달리 이 컴포넌트는
|
|
10
|
+
"화면을 채우는 셸"이 역할이라 자기 높이를 스스로 갖지 않는다. */
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.master {
|
|
14
|
+
flex: 0 0 var(--_master-size, 22rem);
|
|
15
|
+
min-width: 0;
|
|
16
|
+
overflow: auto;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* 좁을 때(자기 폭 기준, JS 로 잰 'overlay' 속성 — @container 아님, 클래스 상단 주석 참조):
|
|
20
|
+
master 가 전체 폭을 채운다. detail 은 그 위 오버레이로 뜬다. */
|
|
21
|
+
:host([overlay]) .master {
|
|
22
|
+
flex: 1 1 auto;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.divider {
|
|
26
|
+
flex: 0 0 auto;
|
|
27
|
+
width: 0;
|
|
28
|
+
border-left: 1px solid var(--u-border-color-weak, #EEEEEE);
|
|
29
|
+
}
|
|
30
|
+
/* detail 이 비어 있으면 나눌 것이 없다 — 슬롯 배정은 slotchange 로 추적한다
|
|
31
|
+
(internals/slotted.ts). ⚠CSS :has() 로는 못 한다: <slot> 자신이 자식이라 :has(*) 가
|
|
32
|
+
항상 참이다. */
|
|
33
|
+
.divider.empty,
|
|
34
|
+
:host([overlay]) .divider {
|
|
35
|
+
display: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.detail {
|
|
39
|
+
flex: 1 1 auto;
|
|
40
|
+
min-width: 0;
|
|
41
|
+
position: relative;
|
|
42
|
+
overflow: auto;
|
|
43
|
+
}
|
|
44
|
+
.detail.empty {
|
|
45
|
+
display: none;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* 오버레이 모드: detail 이 master 위에 뜬다. master 의 flex-basis 를 넘겨받으므로
|
|
49
|
+
master 를 가리는 만큼만 자리를 차지하면 된다 — inset:0 로 host 전체를 덮는다. */
|
|
50
|
+
:host([overlay]) .detail {
|
|
51
|
+
position: absolute;
|
|
52
|
+
inset: 0;
|
|
53
|
+
z-index: 1;
|
|
54
|
+
background-color: var(--u-panel-bg-color, #FFFFFF);
|
|
55
|
+
box-shadow: var(--u-shadow-lg, 0 4px 12px rgba(0, 0, 0, 0.16), 0 2px 4px rgba(0, 0, 0, 0.06));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* 작은 닫기 버튼 — 오버레이 모드에서만 보인다. 나란히 배치될 때는 detail 이 이미
|
|
59
|
+
제자리(패널)라 닫을 이유가 없다 — "구조적 기능"의 범위를 오버레이 전환 자체로 좁힌다. */
|
|
60
|
+
.detail-close {
|
|
61
|
+
display: none;
|
|
62
|
+
position: absolute;
|
|
63
|
+
top: var(--u-space-sm, 8px);
|
|
64
|
+
right: var(--u-space-sm, 8px);
|
|
65
|
+
z-index: 2;
|
|
66
|
+
}
|
|
67
|
+
:host([overlay]) .detail-close {
|
|
68
|
+
display: inline-flex;
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
//#endregion
|
|
72
|
+
export { t as styles };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { PropertyValues } from 'lit';
|
|
2
|
+
import { StyledElement } from '../internals/StyledElement.js';
|
|
3
|
+
type ElementParts = 'host' | 'indicator' | 'step' | 'panel' | 'actions';
|
|
4
|
+
/** 스텝 상태. `done`은 마디를 채워 표시, `error`는 위험색, `disabled`는 도달 불가로 만든다. */
|
|
5
|
+
export type WizardStepState = 'done' | 'error' | 'disabled';
|
|
6
|
+
/** 스텝 정의 — `steps` 배열의 순서가 곧 스텝 순서다. */
|
|
7
|
+
export interface WizardStep {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
state?: WizardStepState;
|
|
11
|
+
}
|
|
12
|
+
/** `step-change` 이벤트의 detail. */
|
|
13
|
+
export interface WizardStepChangeDetail {
|
|
14
|
+
from: number;
|
|
15
|
+
to: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 마법사/스테퍼 — 다단계 흐름의 스텝 인디케이터 + 패널 + 전진/후진 액션.
|
|
19
|
+
*
|
|
20
|
+
* ★**검증(validation)과 저장/재개는 의도적으로 컴포넌트 밖이다.** 도메인 상태 관리라
|
|
21
|
+
* `lob-layers.md` §4("도메인 변환은 소비앱의 어댑터 층에 산다")와 정면으로 맞물린다.
|
|
22
|
+
* `active` 는 controlled prop — 소비자가 외부에서 영속화·복원한다(`UExpander` 의
|
|
23
|
+
* `open` 과 같은 패턴: 컴포넌트가 스스로 갱신하되, 소비자도 직접 대입할 수 있다).
|
|
24
|
+
*
|
|
25
|
+
* 기본 슬롯에 스텝 개수만큼 자식을 넣으면(문서 순서 = `steps` 순서) 활성 스텝만 보이고
|
|
26
|
+
* 나머지는 `hidden`. `slot="actions"` 로 기본 Back/Next 버튼을 완전히 대체할 수 있다
|
|
27
|
+
* ("Submit"이냐 "Next"냐는 라이브러리가 정할 도메인 어휘가 아니다 — 소비자 책임).
|
|
28
|
+
*
|
|
29
|
+
* ```html
|
|
30
|
+
* <u-wizard .steps=${[
|
|
31
|
+
* { id: 'info', label: '기본 정보' },
|
|
32
|
+
* { id: 'payment', label: '결제', state: 'error' },
|
|
33
|
+
* { id: 'review', label: '확인' },
|
|
34
|
+
* ]} active=${1}
|
|
35
|
+
* @step-change=${(e: CustomEvent<WizardStepChangeDetail>) => {
|
|
36
|
+
* if (!isValid()) e.preventDefault();
|
|
37
|
+
* }}>
|
|
38
|
+
* <section>…info 패널…</section>
|
|
39
|
+
* <section>…payment 패널…</section>
|
|
40
|
+
* <section>…review 패널…</section>
|
|
41
|
+
* </u-wizard>
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* `linear`(기본 `true`)이면 인디케이터 클릭으로 미방문 스텝을 건너뛸 수 없다 — 이미 지나온
|
|
45
|
+
* 스텝(또는 `state="done"`으로 명시된 스텝)으로만 돌아갈 수 있다. `linear=false`("editable")면
|
|
46
|
+
* 비활성(`disabled`)이 아닌 스텝은 전부 클릭으로 바로 이동 가능.
|
|
47
|
+
*
|
|
48
|
+
* **접근성**: 키보드만으로 완주 가능(인디케이터는 화살표로 포커스 이동 + Enter/Space로
|
|
49
|
+
* 이동, `Home`/`End`로 처음/끝 도달 가능 스텝) · 스텝 전환 시 포커스가 패널로 이동 ·
|
|
50
|
+
* 상태 변화가 `aria-live="polite"` 영역으로 announce.
|
|
51
|
+
*
|
|
52
|
+
* 오버라이드: `part`(host·indicator·step·panel·actions) + `actions` slot 완전 치환.
|
|
53
|
+
*/
|
|
54
|
+
export declare class Wizard extends StyledElement<ElementParts> {
|
|
55
|
+
static styles: import('lit').CSSResultGroup[];
|
|
56
|
+
/** 스텝 정의 배열. 문서 순서로 기본 슬롯의 자식과 매칭된다. */
|
|
57
|
+
steps: WizardStep[];
|
|
58
|
+
/** 현재 스텝 인덱스(controlled) — 소비자가 외부에서 영속화·복원할 수 있다. */
|
|
59
|
+
active: number;
|
|
60
|
+
/** 인디케이터 방향. 대시보드형 앱엔 세로가 권장(리서치 근거는 계획 문서 §4). */
|
|
61
|
+
orientation: 'horizontal' | 'vertical';
|
|
62
|
+
/**
|
|
63
|
+
* 순차 강제 여부. `true`(기본)면 인디케이터 클릭으로 미방문 스텝을 건너뛸 수 없다 —
|
|
64
|
+
* 이미 지나온 스텝이나 `state="done"` 스텝으로만 돌아갈 수 있다. `false`면 `disabled`가
|
|
65
|
+
* 아닌 모든 스텝을 클릭으로 바로 이동 가능.
|
|
66
|
+
*/
|
|
67
|
+
linear: boolean;
|
|
68
|
+
/** 언어 태그. 기본 Back/Next 라벨과 live region 문구에 쓰인다. */
|
|
69
|
+
locale: string;
|
|
70
|
+
/** `aria-live` 영역에 낼 문구 — 스텝이 바뀔 때만 갱신된다. */
|
|
71
|
+
private announcement;
|
|
72
|
+
protected updated(changed: PropertyValues): void;
|
|
73
|
+
/** 기본 슬롯의 각 자식(문서 순서)을 `steps` 인덱스와 맞춰 활성 패널만 보이게 한다. */
|
|
74
|
+
private syncPanels;
|
|
75
|
+
private isReachable;
|
|
76
|
+
private nextReachable;
|
|
77
|
+
/** 다음 스텝으로. 도달 가능한 스텝이 없으면(마지막 스텝) 아무 일도 하지 않는다. */
|
|
78
|
+
next(): boolean;
|
|
79
|
+
/** 이전 스텝으로. */
|
|
80
|
+
back(): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* 임의 스텝으로 이동. `step-change`(취소 가능)가 막히면(`preventDefault()`) 이동하지
|
|
83
|
+
* 않고 `false`를 돌려준다.
|
|
84
|
+
*/
|
|
85
|
+
goTo(i: number): boolean;
|
|
86
|
+
private announceStep;
|
|
87
|
+
private moveFocusToPanel;
|
|
88
|
+
private handleTabKeydown;
|
|
89
|
+
private renderStep;
|
|
90
|
+
render(): import('lit-html').TemplateResult<1>;
|
|
91
|
+
}
|
|
92
|
+
declare global {
|
|
93
|
+
interface HTMLElementTagNameMap {
|
|
94
|
+
'u-wizard': Wizard;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
|
|
3
|
+
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
|
+
import { getLocaleStrings as r } from "../internals/locale.js";
|
|
5
|
+
import { styles as i } from "./Wizard.styles.js";
|
|
6
|
+
import { html as a, nothing as o } from "lit";
|
|
7
|
+
import { customElement as s, property as c, state as l } from "lit/decorators.js";
|
|
8
|
+
import "@iyulab/components/dist/components/button/UButton.js";
|
|
9
|
+
//#region src/components/Wizard.ts
|
|
10
|
+
var u = class extends n {
|
|
11
|
+
constructor(...e) {
|
|
12
|
+
super(...e), this.steps = [], this.active = 0, this.orientation = "vertical", this.linear = !0, this.locale = "", this.announcement = "", this.handleTabKeydown = (e, t) => {
|
|
13
|
+
let n = this.orientation === "horizontal", r = n ? "ArrowRight" : "ArrowDown", i = n ? "ArrowLeft" : "ArrowUp", a = -1;
|
|
14
|
+
if (e.key === r) a = this.nextReachable(t, 1);
|
|
15
|
+
else if (e.key === i) a = this.nextReachable(t, -1);
|
|
16
|
+
else if (e.key === "Home") a = this.nextReachable(-1, 1);
|
|
17
|
+
else if (e.key === "End") a = this.nextReachable(this.steps.length, -1);
|
|
18
|
+
else return;
|
|
19
|
+
e.preventDefault(), a !== -1 && this.renderRoot.querySelectorAll("[part=\"step\"]")[a]?.focus();
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
static {
|
|
23
|
+
this.styles = [super.styles, i];
|
|
24
|
+
}
|
|
25
|
+
updated(e) {
|
|
26
|
+
super.updated(e), (e.has("active") || e.has("steps")) && this.syncPanels();
|
|
27
|
+
}
|
|
28
|
+
syncPanels() {
|
|
29
|
+
Array.from(this.children).filter((e) => !e.hasAttribute("slot")).forEach((e, t) => {
|
|
30
|
+
e.hidden = t !== this.active;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
isReachable(e) {
|
|
34
|
+
let t = this.steps[e];
|
|
35
|
+
return !t || t.state === "disabled" ? !1 : !this.linear || e <= this.active + 1 || t.state === "done";
|
|
36
|
+
}
|
|
37
|
+
nextReachable(e, t) {
|
|
38
|
+
for (let n = e + t; n >= 0 && n < this.steps.length; n += t) if (this.isReachable(n)) return n;
|
|
39
|
+
return -1;
|
|
40
|
+
}
|
|
41
|
+
next() {
|
|
42
|
+
return this.goTo(this.active + 1);
|
|
43
|
+
}
|
|
44
|
+
back() {
|
|
45
|
+
return this.goTo(this.active - 1);
|
|
46
|
+
}
|
|
47
|
+
goTo(e) {
|
|
48
|
+
if (e < 0 || e >= this.steps.length || e === this.active || !this.isReachable(e)) return !1;
|
|
49
|
+
let t = this.active;
|
|
50
|
+
return this.fire("step-change", { detail: {
|
|
51
|
+
from: t,
|
|
52
|
+
to: e
|
|
53
|
+
} }) ? (this.active = e, this.announceStep(e), this.moveFocusToPanel(), !0) : !1;
|
|
54
|
+
}
|
|
55
|
+
announceStep(e) {
|
|
56
|
+
let t = this.steps[e], n = r(this.locale || void 0);
|
|
57
|
+
this.announcement = n.wizardStepAnnouncement(e + 1, this.steps.length, t?.label ?? "");
|
|
58
|
+
}
|
|
59
|
+
async moveFocusToPanel() {
|
|
60
|
+
await this.updateComplete, this.renderRoot.querySelector("[part=\"panel\"]")?.focus();
|
|
61
|
+
}
|
|
62
|
+
renderStep(e, t) {
|
|
63
|
+
let n = t === this.active, r = this.isReachable(t);
|
|
64
|
+
return a`
|
|
65
|
+
<button type="button"
|
|
66
|
+
class="step ${n ? "active" : ""} ${e.state ? `state-${e.state}` : ""}"
|
|
67
|
+
part="step"
|
|
68
|
+
role="tab"
|
|
69
|
+
id="tab-${t}"
|
|
70
|
+
aria-selected=${n ? "true" : "false"}
|
|
71
|
+
aria-controls="panel"
|
|
72
|
+
aria-disabled=${r ? o : "true"}
|
|
73
|
+
tabindex=${n ? "0" : "-1"}
|
|
74
|
+
?disabled=${e.state === "disabled"}
|
|
75
|
+
@click=${() => this.goTo(t)}
|
|
76
|
+
@keydown=${(e) => this.handleTabKeydown(e, t)}
|
|
77
|
+
>
|
|
78
|
+
<span class="marker" aria-hidden="true">${e.state === "done" ? "✓" : t + 1}</span>
|
|
79
|
+
<span class="label">${e.label}</span>
|
|
80
|
+
</button>
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
render() {
|
|
84
|
+
let e = r(this.locale || void 0), t = this.active >= this.steps.length - 1;
|
|
85
|
+
return a`
|
|
86
|
+
<div class="indicator" part="indicator" role="tablist" aria-orientation=${this.orientation}>
|
|
87
|
+
${this.steps.map((e, t) => this.renderStep(e, t))}
|
|
88
|
+
</div>
|
|
89
|
+
<div class="content">
|
|
90
|
+
<div class="panel" part="panel" tabindex="-1" id="panel" aria-labelledby="tab-${this.active}">
|
|
91
|
+
<slot @slotchange=${() => this.syncPanels()}></slot>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="actions" part="actions">
|
|
94
|
+
<slot name="actions">
|
|
95
|
+
<u-button variant="ghost" ?disabled=${this.active === 0} @click=${() => this.back()}>
|
|
96
|
+
${e.wizardBack}
|
|
97
|
+
</u-button>
|
|
98
|
+
${t ? o : a`
|
|
99
|
+
<u-button color="primary" @click=${() => this.next()}>${e.wizardNext}</u-button>
|
|
100
|
+
`}
|
|
101
|
+
</slot>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="announcement" aria-live="polite" role="status">${this.announcement}</div>
|
|
104
|
+
</div>
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
t([c({
|
|
109
|
+
type: Array,
|
|
110
|
+
attribute: !1
|
|
111
|
+
}), e("design:type", Array)], u.prototype, "steps", void 0), t([c({
|
|
112
|
+
type: Number,
|
|
113
|
+
reflect: !0
|
|
114
|
+
}), e("design:type", Object)], u.prototype, "active", void 0), t([c({
|
|
115
|
+
type: String,
|
|
116
|
+
reflect: !0
|
|
117
|
+
}), e("design:type", String)], u.prototype, "orientation", void 0), t([c({
|
|
118
|
+
type: Boolean,
|
|
119
|
+
reflect: !0
|
|
120
|
+
}), e("design:type", Object)], u.prototype, "linear", void 0), t([c({ type: String }), e("design:type", Object)], u.prototype, "locale", void 0), t([l(), e("design:type", Object)], u.prototype, "announcement", void 0), u = t([s("u-wizard")], u);
|
|
121
|
+
//#endregion
|
|
122
|
+
export { u as Wizard };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import('lit').CSSResult;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { css as e } from "lit";
|
|
2
|
+
//#region src/components/Wizard.styles.ts
|
|
3
|
+
var t = e`
|
|
4
|
+
:host {
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: row;
|
|
7
|
+
gap: var(--u-space-xl, 20px);
|
|
8
|
+
min-width: 0;
|
|
9
|
+
}
|
|
10
|
+
:host([orientation="horizontal"]) {
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.indicator {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
gap: var(--u-space-sm, 8px);
|
|
18
|
+
flex: 0 0 auto;
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding: 0;
|
|
21
|
+
}
|
|
22
|
+
:host([orientation="horizontal"]) .indicator {
|
|
23
|
+
flex-direction: row;
|
|
24
|
+
flex-wrap: wrap;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.content {
|
|
28
|
+
flex: 1 1 auto;
|
|
29
|
+
min-width: 0;
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
gap: var(--u-space-lg, 16px);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.step {
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: var(--u-space-sm, 8px);
|
|
39
|
+
background: none;
|
|
40
|
+
border: none;
|
|
41
|
+
padding: var(--u-space-2xs, 4px) 0;
|
|
42
|
+
font: inherit;
|
|
43
|
+
color: var(--u-txt-color-weak, #757575);
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
text-align: left;
|
|
46
|
+
}
|
|
47
|
+
.step[disabled] {
|
|
48
|
+
color: var(--u-txt-color-disabled, #BDBDBD);
|
|
49
|
+
cursor: not-allowed;
|
|
50
|
+
}
|
|
51
|
+
/* 도달은 못 하지만(linear 모드에서 아직 안 지나온 스텝) disabled 는 아니다 —
|
|
52
|
+
소비자가 미리 그 존재는 보여주고 싶을 수 있어 시각적으로 흐리되 마우스 커서는
|
|
53
|
+
"막혔다"가 아니라 "여기는 아직"으로 남긴다. */
|
|
54
|
+
.step[aria-disabled="true"]:not([disabled]) {
|
|
55
|
+
cursor: default;
|
|
56
|
+
opacity: 0.6;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.marker {
|
|
60
|
+
flex: 0 0 auto;
|
|
61
|
+
display: inline-flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
justify-content: center;
|
|
64
|
+
width: 1.5em;
|
|
65
|
+
height: 1.5em;
|
|
66
|
+
border-radius: 50%;
|
|
67
|
+
border: 1px solid var(--u-border-color-strong, #BDBDBD);
|
|
68
|
+
font-size: var(--u-text-caption-size, 12px);
|
|
69
|
+
font-weight: var(--u-text-label-weight, 600);
|
|
70
|
+
}
|
|
71
|
+
.step.active .marker {
|
|
72
|
+
border-color: var(--u-primary-color, #1976D2);
|
|
73
|
+
color: var(--u-primary-color, #1976D2);
|
|
74
|
+
}
|
|
75
|
+
.step.state-done .marker {
|
|
76
|
+
background-color: var(--u-primary-color, #1976D2);
|
|
77
|
+
border-color: var(--u-primary-color, #1976D2);
|
|
78
|
+
color: var(--u-txt-color-inverse, #FFFFFF);
|
|
79
|
+
}
|
|
80
|
+
.step.state-error .marker {
|
|
81
|
+
border-color: var(--u-danger-color, #D32F2F);
|
|
82
|
+
color: var(--u-danger-color, #D32F2F);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.label {
|
|
86
|
+
font-size: var(--u-text-label-size, 13px);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.panel {
|
|
90
|
+
min-width: 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.actions {
|
|
94
|
+
display: flex;
|
|
95
|
+
gap: var(--u-space-sm, 8px);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* live region — 화면엔 안 보이지만 스크린리더는 읽는다. 폭/높이는 디자인 토큰이 아니라
|
|
99
|
+
"존재는 하되 렌더 크기는 0에 가까워야" 하는 구조값이라 var() 로 감싼다(리터럴 스캔
|
|
100
|
+
예외 — 시트 자체와 무관한 값이라 시트에 실을 이유가 없다). */
|
|
101
|
+
.announcement {
|
|
102
|
+
position: absolute;
|
|
103
|
+
overflow: hidden;
|
|
104
|
+
clip: rect(0, 0, 0, 0);
|
|
105
|
+
white-space: nowrap;
|
|
106
|
+
width: var(--_a11y-unit, 1px);
|
|
107
|
+
height: var(--_a11y-unit, 1px);
|
|
108
|
+
margin: calc(-1 * var(--_a11y-unit, 1px));
|
|
109
|
+
padding: 0;
|
|
110
|
+
border: 0;
|
|
111
|
+
}
|
|
112
|
+
`;
|
|
113
|
+
//#endregion
|
|
114
|
+
export { t as styles };
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export { InfoSection } from './components/InfoSection.js';
|
|
|
10
10
|
export { InfoField, isBlank } from './components/InfoField.js';
|
|
11
11
|
export { EmptyState } from './components/EmptyState.js';
|
|
12
12
|
export { ActionBar } from './components/ActionBar.js';
|
|
13
|
+
export { MasterDetailLayout } from './components/MasterDetailLayout.js';
|
|
14
|
+
export { Wizard } from './components/Wizard.js';
|
|
15
|
+
export type { WizardStep, WizardStepState, WizardStepChangeDetail } from './components/Wizard.js';
|
|
13
16
|
export { registerLocale, setDefaultLocale, getLocaleStrings, getDefaultLocale, } from './internals/locale.js';
|
|
14
17
|
export type { ModernAppLocaleStrings } from './internals/locale.js';
|
|
15
18
|
export { app };
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,9 @@ import { InfoSection as c } from "./components/InfoSection.js";
|
|
|
7
7
|
import { InfoField as l, isBlank as u } from "./components/InfoField.js";
|
|
8
8
|
import { EmptyState as d } from "./components/EmptyState.js";
|
|
9
9
|
import { ActionBar as f } from "./components/ActionBar.js";
|
|
10
|
+
import { MasterDetailLayout as p } from "./components/MasterDetailLayout.js";
|
|
11
|
+
import { Wizard as m } from "./components/Wizard.js";
|
|
10
12
|
//#region src/index.ts
|
|
11
|
-
var
|
|
13
|
+
var h = e;
|
|
12
14
|
//#endregion
|
|
13
|
-
export { f as ActionBar, d as EmptyState, s as GroupBox, l as InfoField, c as InfoSection, o as PageHeader, e as app,
|
|
15
|
+
export { f as ActionBar, d as EmptyState, s as GroupBox, l as InfoField, c as InfoSection, p as MasterDetailLayout, o as PageHeader, m as Wizard, e as app, h as default, t as filterSidebarItems, n as getDefaultLocale, r as getLocaleStrings, u as isBlank, i as registerLocale, a as setDefaultLocale };
|
|
@@ -26,6 +26,13 @@ export interface ModernAppLocaleStrings {
|
|
|
26
26
|
/** Empty state — records exist but the current query matched none. */
|
|
27
27
|
noResultsTitle: string;
|
|
28
28
|
noResultsDescription: string;
|
|
29
|
+
/** Master-detail layout — accessible label for the overlay-mode close button. */
|
|
30
|
+
detailClose: string;
|
|
31
|
+
/** Wizard — default Back/Next action labels ("Submit" wording is the consumer's call). */
|
|
32
|
+
wizardBack: string;
|
|
33
|
+
wizardNext: string;
|
|
34
|
+
/** Wizard — live-region announcement read out on every step change. */
|
|
35
|
+
wizardStepAnnouncement: (index: number, total: number, label: string) => string;
|
|
29
36
|
}
|
|
30
37
|
/**
|
|
31
38
|
* Register a locale. Partial overrides are merged with the English defaults,
|
package/dist/internals/locale.js
CHANGED
|
@@ -4,7 +4,11 @@ var e = {
|
|
|
4
4
|
noDataTitle: "Nothing here yet",
|
|
5
5
|
noDataDescription: "",
|
|
6
6
|
noResultsTitle: "No matching results",
|
|
7
|
-
noResultsDescription: "Try changing your search terms or filters."
|
|
7
|
+
noResultsDescription: "Try changing your search terms or filters.",
|
|
8
|
+
detailClose: "Close",
|
|
9
|
+
wizardBack: "Back",
|
|
10
|
+
wizardNext: "Next",
|
|
11
|
+
wizardStepAnnouncement: (e, t, n) => `Step ${e} of ${t}: ${n}`
|
|
8
12
|
}, t = /* @__PURE__ */ new Map(), n;
|
|
9
13
|
function r(n, r) {
|
|
10
14
|
t.set(n.toLowerCase(), {
|
|
@@ -10,10 +10,10 @@ import "../components/SidebarButton.js";
|
|
|
10
10
|
import { styles as a } from "./SidebarLayout.styles.js";
|
|
11
11
|
import { html as o, nothing as s } from "lit";
|
|
12
12
|
import { customElement as c, property as l, query as u, state as d } from "lit/decorators.js";
|
|
13
|
+
import "@iyulab/components/dist/components/button/UButton.js";
|
|
14
|
+
import "@iyulab/components/dist/components/icon/UIcon.js";
|
|
13
15
|
import { unsafeHTML as f } from "lit/directives/unsafe-html.js";
|
|
14
16
|
import { repeat as p } from "lit/directives/repeat.js";
|
|
15
|
-
import "@iyulab/components/dist/components/icon/UIcon.js";
|
|
16
|
-
import "@iyulab/components/dist/components/button/UButton.js";
|
|
17
17
|
import { UProgressBar as m } from "@iyulab/components/dist/components/progress-bar/UProgressBar.js";
|
|
18
18
|
//#region src/layouts/SidebarLayout.ts
|
|
19
19
|
function h(e) {
|
|
@@ -165,10 +165,19 @@ var t = e`
|
|
|
165
165
|
display: none;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
/* Main Content
|
|
168
|
+
/* Main Content
|
|
169
|
+
* ★기본 패딩을 준다 — 라우트 콘텐츠가 뷰포트/사이드바 경계에 그대로 맞닿아 모든
|
|
170
|
+
* 소비자가 같은 배선을 재구현하던 문제였다. 값은 이 라이브러리의 spacing 스케일
|
|
171
|
+
* 상단 근처(3xl)에서 골랐다 — LOB 화면의 표준 여백으로 쓰기에 과하지 않은 최대값.
|
|
172
|
+
* ⚠«패딩 박스» 기준 절대배치는 이 padding 의 영향을 받지 않는다 — 바로 아래
|
|
173
|
+
* u-progress-bar 가 top/left/right:0 으로 여전히 테두리에 꽉 차게 붙는 이유다
|
|
174
|
+
* (CSS 2.1 §10.6.4: 절대배치 자손의 containing block 은 가장 가까운 positioned
|
|
175
|
+
* 조상의 «패딩 박스»이고, 그 경계는 조상 자신의 padding 값에 밀리지 않는다).
|
|
176
|
+
* 풀블리드를 원하는 소비자는 layout.styles.main = { padding: '0' } 로 되돌린다. */
|
|
169
177
|
.main {
|
|
170
178
|
position: relative;
|
|
171
179
|
flex: 1;
|
|
180
|
+
padding: var(--u-space-3xl, 32px);
|
|
172
181
|
background: var(--u-bg-color, #FFFFFF);
|
|
173
182
|
overflow: auto;
|
|
174
183
|
outline: none;
|
package/package.json
CHANGED
|
@@ -36,6 +36,10 @@ and those are different states of the business. So the component owns the rule.
|
|
|
36
36
|
| `numeric` | `boolean` | `false` | | Right-align with tabular figures so digits line up vertically |
|
|
37
37
|
| `format` | `'number'\|'currency'\|'date'` | — | | Renders `value` through `@iyulab/components`' `formatNumber`/`formatCurrency`/`formatDate`; unset falls back to plain `String(value)` |
|
|
38
38
|
| `currency` | `string` | — | | Currency code for `format="currency"` (e.g. `'KRW'`); omitted degrades to plain number formatting, does not throw |
|
|
39
|
+
| `size` | `'default'\|'lg'` | `'default'` | ✓ | `'lg'` renders the value at the `title` type-scale step — for dashboard KPI tiles composed inside `u-info-section` |
|
|
40
|
+
| `trend` | `'up'\|'down'\|'flat'` | — | | Trend direction; renders a trend indicator when set, alongside `trendLabel`; pair with trendLabel for an accessible name — trend alone conveys direction by color only |
|
|
41
|
+
| `trendLabel` | `string` | — | | Trend copy, e.g. `"+12% vs last month"` — wording is the consumer's responsibility |
|
|
42
|
+
| `tone` | `'positive'\|'negative'\|'neutral'` | — | | Explicit tone override; resolves from `trend` when unset (`up→positive`, `down→negative`, `flat`/unset→`neutral`) but always wins over inference |
|
|
39
43
|
|
|
40
44
|
⚠ `numeric` earns its keep in **tables**, where columns of figures are compared by eye. On a
|
|
41
45
|
lone field it just pushes the value away from its label.
|
|
@@ -48,4 +52,4 @@ lone field it just pushes the value away from its label.
|
|
|
48
52
|
|
|
49
53
|
| Part | Description |
|
|
50
54
|
|------|-------------|
|
|
51
|
-
| `label` · `value` | The
|
|
55
|
+
| `label` · `value` · `trend` | The label, the value, and the (optional) trend indicator |
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# u-master-detail-layout
|
|
2
|
+
|
|
3
|
+
**Tag:** `u-master-detail-layout`
|
|
4
|
+
|
|
5
|
+
A responsive master›detail split-pane shell. Modeled on Vaadin's `MasterDetailLayout`, with a
|
|
6
|
+
deliberately smaller surface — no draggable splitter (`masterSize` is a fixed prop, not a v1
|
|
7
|
+
feature).
|
|
8
|
+
|
|
9
|
+
The default slot is the master area. `slot="detail"` is the detail area, and it **appears when
|
|
10
|
+
filled and disappears when emptied** — the same contract as Vaadin's. Selection state (what to
|
|
11
|
+
show in the detail pane) is not this component's job — it's a shell, not a design decision.
|
|
12
|
+
Swap the `detail` slot's content when the selection changes:
|
|
13
|
+
|
|
14
|
+
```html
|
|
15
|
+
<u-master-detail-layout>
|
|
16
|
+
<u-rich-table @selection-change=${e => (detail = renderRecord(e.detail.selected))}>
|
|
17
|
+
…
|
|
18
|
+
</u-rich-table>
|
|
19
|
+
<div slot="detail">${detail}</div>
|
|
20
|
+
</u-master-detail-layout>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
On a narrow container (self width, not viewport — see below) the detail pane switches to a
|
|
24
|
+
full overlay above the master pane, with a small close button that fires `detail-close`.
|
|
25
|
+
Clearing the detail slot yourself (in response to the event) is what actually dismisses it —
|
|
26
|
+
the component does not own the detail content, so it does not clear it for you.
|
|
27
|
+
|
|
28
|
+
## Why `ResizeObserver`, not `@container`
|
|
29
|
+
|
|
30
|
+
Every other primitive in this package (`u-page-header`, `u-group-box`, `u-action-bar`) folds at
|
|
31
|
+
a **fixed** self-width breakpoint via CSS `@container`. This component's breakpoint
|
|
32
|
+
(`overlayBreakpoint`) is a per-instance **prop** instead, because two panels sharing a screen
|
|
33
|
+
need more room than either alone — the right threshold depends on what the consumer puts in
|
|
34
|
+
each pane. CSS container-query conditions cannot read a runtime custom property, so honoring a
|
|
35
|
+
configurable breakpoint means measuring the host's own width in JavaScript and reflecting an
|
|
36
|
+
`overlay` attribute — same self-width philosophy, different mechanism, for a fixed reason.
|
|
37
|
+
|
|
38
|
+
## Slots
|
|
39
|
+
|
|
40
|
+
| Name | Description |
|
|
41
|
+
|------|-------------|
|
|
42
|
+
| *(default)* | Master area |
|
|
43
|
+
| `detail` | Detail area — present ⇒ shown, empty ⇒ hidden |
|
|
44
|
+
|
|
45
|
+
## Properties
|
|
46
|
+
|
|
47
|
+
| Property | Type | Default | Reflect | Description |
|
|
48
|
+
|----------|------|---------|---------|-------------|
|
|
49
|
+
| `masterSize` | `string` | `'22rem'` | | Fixed width of the master pane, a CSS length (`master-size`) |
|
|
50
|
+
| `overlayBreakpoint` | `number` | `760` | | Below this self width in px, detail becomes a full overlay (`overlay-breakpoint`) |
|
|
51
|
+
| `locale` | `string` | `''` | | Language tag for the close button's accessible label |
|
|
52
|
+
|
|
53
|
+
## Events
|
|
54
|
+
|
|
55
|
+
| Event | Detail | Cancelable | Description |
|
|
56
|
+
|-------|--------|------------|--------------|
|
|
57
|
+
| `detail-close` | — | No | Overlay-mode close button clicked. Clear the `detail` slot's content in response — the component does not do this for you |
|
|
58
|
+
|
|
59
|
+
## CSS Parts
|
|
60
|
+
|
|
61
|
+
| Part | Description |
|
|
62
|
+
|------|-------------|
|
|
63
|
+
| `master` | Master pane |
|
|
64
|
+
| `divider` | Hairline between panes — hidden when there's no detail, or in overlay mode |
|
|
65
|
+
| `detail` | Detail pane |
|
|
66
|
+
| `detail-close` | Overlay-mode close button — hidden outside overlay mode |
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# u-wizard
|
|
2
|
+
|
|
3
|
+
**Tag:** `u-wizard`
|
|
4
|
+
|
|
5
|
+
A step indicator + panel + Back/Next actions for multi-step flows.
|
|
6
|
+
|
|
7
|
+
Validation and save/resume are deliberately outside this component — that's domain state
|
|
8
|
+
management, the consumer's adapter layer owns it. `active` is a controlled prop (same pattern
|
|
9
|
+
as `u-expander`'s `open`): the component advances it itself when a transition isn't blocked,
|
|
10
|
+
but a consumer can also assign it directly to restore a saved step.
|
|
11
|
+
|
|
12
|
+
```html
|
|
13
|
+
<u-wizard .steps=${[
|
|
14
|
+
{ id: 'info', label: 'Basic info' },
|
|
15
|
+
{ id: 'payment', label: 'Payment' },
|
|
16
|
+
{ id: 'review', label: 'Review' },
|
|
17
|
+
]} active=${1}
|
|
18
|
+
@step-change=${(e) => { if (!isValid()) e.preventDefault(); }}>
|
|
19
|
+
<section>…info panel…</section>
|
|
20
|
+
<section>…payment panel…</section>
|
|
21
|
+
<section>…review panel…</section>
|
|
22
|
+
</u-wizard>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The default slot holds one child per step, matched to the `steps` array by **document order** —
|
|
26
|
+
only the active one is visible, the rest get the `hidden` attribute. `slot="actions"` fully
|
|
27
|
+
replaces the built-in Back/Next buttons — whether the last step says "Next" or "Submit" is
|
|
28
|
+
consumer wording, not something the library decides.
|
|
29
|
+
|
|
30
|
+
## `linear` reachability
|
|
31
|
+
|
|
32
|
+
With `linear` (default `true`), clicking a step in the indicator can't skip ahead past unvisited
|
|
33
|
+
steps — only steps you've already passed, or ones explicitly marked `state: 'done'`, are directly
|
|
34
|
+
reachable that way. `next()` can still always advance exactly one step forward regardless — linear
|
|
35
|
+
mode blocks *skipping*, not progression. Set `linear={false}` to let any non-`disabled'` step be
|
|
36
|
+
clicked directly (a non-linear/"editable" stepper).
|
|
37
|
+
|
|
38
|
+
## Accessibility
|
|
39
|
+
|
|
40
|
+
- Keyboard-operable end to end: indicator tabs use roving `tabindex` (arrow keys move focus among
|
|
41
|
+
reachable steps, `Home`/`End` jump to the first/last reachable one; `Enter`/`Space` activate via
|
|
42
|
+
native `<button>` semantics).
|
|
43
|
+
- Focus moves to the panel after every step change.
|
|
44
|
+
- An `aria-live="polite"` region announces `"Step N of M: <label>"` on every change.
|
|
45
|
+
|
|
46
|
+
## Slots
|
|
47
|
+
|
|
48
|
+
| Name | Description |
|
|
49
|
+
|------|-------------|
|
|
50
|
+
| *(default)* | Step panels, one per `steps` entry, matched by document order |
|
|
51
|
+
| `actions` | Replaces the default Back/Next buttons entirely |
|
|
52
|
+
|
|
53
|
+
## Properties
|
|
54
|
+
|
|
55
|
+
| Property | Type | Default | Reflect | Description |
|
|
56
|
+
|----------|------|---------|---------|-------------|
|
|
57
|
+
| `steps` | `WizardStep[]` | `[]` | | `{ id, label, state? }[]`; `state` is `'done'\|'error'\|'disabled'` |
|
|
58
|
+
| `active` | `number` | `0` | ✓ | Current step index (controlled) |
|
|
59
|
+
| `orientation` | `'horizontal'\|'vertical'` | `'vertical'` | ✓ | Indicator layout |
|
|
60
|
+
| `linear` | `boolean` | `true` | ✓ | See "linear reachability" above |
|
|
61
|
+
| `locale` | `string` | `''` | | Language tag for default Back/Next labels and the live-region announcement |
|
|
62
|
+
|
|
63
|
+
## Methods
|
|
64
|
+
|
|
65
|
+
| Method | Description |
|
|
66
|
+
|--------|-------------|
|
|
67
|
+
| `next()` | Advance one step. No-op (returns `false`) on the last step or if `step-change` is canceled |
|
|
68
|
+
| `back()` | Go back one step. No-op on the first step |
|
|
69
|
+
| `goTo(i)` | Jump to step `i` if reachable and `step-change` isn't canceled |
|
|
70
|
+
|
|
71
|
+
## Events
|
|
72
|
+
|
|
73
|
+
| Event | Detail | Cancelable | Description |
|
|
74
|
+
|-------|--------|------------|--------------|
|
|
75
|
+
| `step-change` | `{ from: number, to: number }` | Yes | Fires before any transition (Back/Next, indicator click, or `goTo()`). `preventDefault()` blocks it — this is where validation runs |
|
|
76
|
+
|
|
77
|
+
## CSS Parts
|
|
78
|
+
|
|
79
|
+
| Part | Description |
|
|
80
|
+
|------|-------------|
|
|
81
|
+
| `indicator` | The step list (`role="tablist"`) |
|
|
82
|
+
| `step` | Each step button (`role="tab"`) |
|
|
83
|
+
| `panel` | Wrapper around the active step's slotted content (`role="tabpanel"`) |
|
|
84
|
+
| `actions` | Back/Next (or overridden) action row |
|