@iyulab/modern-app 0.42.2 → 0.44.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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.44.0] - 2026-10-07
4
+
5
+ ### Added
6
+
7
+ - **`u-empty-state variant="error"`** — the list could not be loaded, a third fact next to `no-data` (create one)
8
+ and `no-results` (change the query); its next step is trying again. Its own default wording (English and Korean
9
+ built in, `errorTitle` / `errorDescription` for other languages), and its title is announced (`role="alert"`) —
10
+ the other two variants are not alarms and stay quiet. `variant` now reflects to the attribute.
11
+
12
+ ## [0.43.0] - 2026-10-07
13
+
14
+ ### Added
15
+
16
+ - **Sidebar toggle buttons** — `SidebarButtonConfig.pressed` (`boolean` or a function). The button carries
17
+ `aria-pressed` and, when on, a soft surface and an accented icon (`--app-sidebar-pressed-bg`,
18
+ `--app-sidebar-pressed-icon-color`), visible in the slim sidebar and distinct from the current page's link. A
19
+ setting such as a field or dark mode no longer has to rename itself "turn on / turn off" to show its state. The
20
+ shell reads `pressed` on every render and re-renders after the button's `onClick`.
21
+
3
22
  ## [0.42.2] - 2026-10-07
4
23
 
5
24
  ### Fixed
@@ -15,6 +15,10 @@ type ElementParts = 'host' | 'icon' | 'title' | 'description' | 'actions';
15
15
  * </u-empty-state>
16
16
  *
17
17
  * <u-empty-state variant="no-results"></u-empty-state>
18
+ *
19
+ * <u-empty-state variant="error" .description=${error.message}>
20
+ * <u-button slot="actions" @click=${retry}>Try again</u-button>
21
+ * </u-empty-state>
18
22
  * ```
19
23
  *
20
24
  * ⚠**기본 문구는 영어다** — 이 패키지는 범용 층이라 특정 언어를 기본값으로 가질 수 없다.
@@ -27,8 +31,11 @@ type ElementParts = 'host' | 'icon' | 'title' | 'description' | 'actions';
27
31
  */
28
32
  export declare class EmptyState extends StyledElement<ElementParts> {
29
33
  static styles: import('lit').CSSResultGroup[];
30
- /** `no-data` = 아직 없음 / `no-results` = 조건에 맞는 것이 없음. */
31
- variant: 'no-data' | 'no-results';
34
+ /**
35
+ * `no-data` = 아직 없음 / `no-results` = 조건에 맞는 것이 없음 / `error` = 불러오지 못함.
36
+ * 셋은 다음 행동이 다르다(만들기 · 조건 바꾸기 · 다시 시도) — `error` 의 제목은 보조기기에 알린다(`role="alert"`).
37
+ */
38
+ variant: 'no-data' | 'no-results' | 'error';
32
39
  /** 제목. 비우면 variant 기본 문구. */
33
40
  title: string;
34
41
  /** 보조 설명. 비우면 variant 기본 문구. */
@@ -16,7 +16,11 @@ var d = class extends n {
16
16
  }
17
17
  get defaults() {
18
18
  let e = r(this.locale || void 0);
19
- return this.variant === "no-results" ? {
19
+ return this.variant === "error" ? {
20
+ icon: "⚠️",
21
+ title: e.errorTitle,
22
+ description: e.errorDescription
23
+ } : this.variant === "no-results" ? {
20
24
  icon: "🔍",
21
25
  title: e.noResultsTitle,
22
26
  description: e.noResultsDescription
@@ -32,7 +36,7 @@ var d = class extends n {
32
36
  <div class="icon" part="icon" aria-hidden="true">
33
37
  <slot name="icon">${e.icon}</slot>
34
38
  </div>
35
- <p class="title" part="title">${this.title || e.title}</p>
39
+ <p class="title" part="title" role=${this.variant === "error" ? "alert" : s}>${this.title || e.title}</p>
36
40
  ${t ? o`<p class="description" part="description">${t}</p>` : s}
37
41
  <div class="actions ${this.hasActions ? "" : "empty"}" part="actions">
38
42
  <slot name="actions"
@@ -42,6 +46,9 @@ var d = class extends n {
42
46
  `;
43
47
  }
44
48
  };
45
- t([l({ type: String }), e("design:type", String)], d.prototype, "variant", void 0), t([l({ type: String }), e("design:type", Object)], d.prototype, "title", void 0), t([l({ type: String }), e("design:type", Object)], d.prototype, "description", void 0), t([l({ type: String }), e("design:type", Object)], d.prototype, "locale", void 0), t([u(), e("design:type", Object)], d.prototype, "hasActions", void 0), d = t([c("u-empty-state")], d);
49
+ t([l({
50
+ type: String,
51
+ reflect: !0
52
+ }), e("design:type", String)], d.prototype, "variant", void 0), t([l({ type: String }), e("design:type", Object)], d.prototype, "title", void 0), t([l({ type: String }), e("design:type", Object)], d.prototype, "description", void 0), t([l({ type: String }), e("design:type", Object)], d.prototype, "locale", void 0), t([u(), e("design:type", Object)], d.prototype, "hasActions", void 0), d = t([c("u-empty-state")], d);
46
53
  //#endregion
47
54
  export { d as EmptyState };
@@ -12,6 +12,13 @@ export interface SidebarButtonConfig extends SidebarPermissionGuard {
12
12
  lib?: string;
13
13
  label?: string | DirectiveResult;
14
14
  styles?: StyleMap<ElementParts>;
15
+ /**
16
+ * 켬/끔 버튼(토글)의 지금 상태 — 지정하면 안쪽 버튼이 `aria-pressed` 를 싣고 켜졌을 때 눌린 모양(옅은 면 · 강조 아이콘)으로
17
+ * 그려진다(슬림 모드에서도 아이콘으로 보인다). 이름은 상태와 무관하게 하나로 둔다(«현장 모드» — «켜기/끄기» 가 아니라).
18
+ * 셸은 렌더마다 읽는다: 값이면 설정 객체의 그 값을, 함수면 그 결과를. 그 버튼의 `onClick` 뒤에는 셸이 다시 그리므로,
19
+ * 클릭이 상태를 바꾸는 흔한 경우는 따로 할 일이 없다. 미지정이면 일반 버튼이다.
20
+ */
21
+ pressed?: boolean | (() => boolean);
15
22
  onClick?: (event?: Event) => void;
16
23
  }
17
24
  /**
@@ -25,6 +32,8 @@ export declare class SidebarButton extends StyledElement<ElementParts> {
25
32
  icon?: string;
26
33
  /** `icon`을 해석할 등록 라이브러리 이름 */
27
34
  lib?: string;
35
+ /** 켬/끔 상태 — 지정되면 토글 버튼(`aria-pressed`). 미지정이면 일반 버튼. */
36
+ pressed?: boolean;
28
37
  /** 버튼 텍스트 라벨 */
29
38
  label?: string | DirectiveResult;
30
39
  /** 이 버튼이 여는 팝업의 종류 — 안쪽 `<button>` 의 `aria-haspopup` 으로 간다(셸의 `type: 'menu'` 트리거). */
@@ -22,7 +22,8 @@ var l = class extends n {
22
22
  return a`
23
23
  <button part="base" ?compact=${this.compact}
24
24
  aria-haspopup=${c(this.haspopup)}
25
- aria-expanded=${c(this.haspopup ? String(this.expanded) : void 0)}>
25
+ aria-expanded=${c(this.haspopup ? String(this.expanded) : void 0)}
26
+ aria-pressed=${c(this.pressed === void 0 ? void 0 : String(this.pressed))}>
26
27
  <u-icon part="icon"
27
28
  .lib=${this.lib}
28
29
  .name=${this.icon}
@@ -38,6 +39,9 @@ var l = class extends n {
38
39
  t([s({
39
40
  type: Boolean,
40
41
  reflect: !0
41
- }), e("design:type", Object)], l.prototype, "compact", void 0), t([s({ type: String }), e("design:type", String)], l.prototype, "icon", void 0), t([s({ type: String }), e("design:type", String)], l.prototype, "lib", void 0), t([s({ type: String }), e("design:type", Object)], l.prototype, "label", void 0), t([s({ type: String }), e("design:type", String)], l.prototype, "haspopup", void 0), t([s({ type: Boolean }), e("design:type", Object)], l.prototype, "expanded", void 0), l = t([o("u-sidebar-button")], l);
42
+ }), e("design:type", Object)], l.prototype, "compact", void 0), t([s({ type: String }), e("design:type", String)], l.prototype, "icon", void 0), t([s({ type: String }), e("design:type", String)], l.prototype, "lib", void 0), t([s({
43
+ type: Boolean,
44
+ reflect: !0
45
+ }), e("design:type", Boolean)], l.prototype, "pressed", void 0), t([s({ type: String }), e("design:type", Object)], l.prototype, "label", void 0), t([s({ type: String }), e("design:type", String)], l.prototype, "haspopup", void 0), t([s({ type: Boolean }), e("design:type", Object)], l.prototype, "expanded", void 0), l = t([o("u-sidebar-button")], l);
42
46
  //#endregion
43
47
  export { l as SidebarButton };
@@ -18,6 +18,14 @@ var t = e`
18
18
  :host(:active) {
19
19
  background-color: var(--u-bg-color-active, #EEEEEE);
20
20
  }
21
+ /* 켜진 토글 — «현재 페이지»(활성 링크의 진한 면)와 구별되는 옅은 면 + 강조 아이콘. 위치와 설정이 같은 모양이면 섞인다.
22
+ 아이콘 색이 상태를 나르므로 라벨이 숨는 슬림 모드에서도 보인다. */
23
+ :host([pressed]) {
24
+ background-color: var(--app-sidebar-pressed-bg, var(--u-bg-color-active, #EEEEEE));
25
+ }
26
+ :host([pressed]) u-icon {
27
+ color: var(--app-sidebar-pressed-icon-color, var(--u-primary-color, #1976D2));
28
+ }
21
29
 
22
30
  button {
23
31
  all: unset;
@@ -7,6 +7,9 @@ export interface ModernAppLocaleStrings {
7
7
  /** Empty state — records exist but the current query matched none. */
8
8
  noResultsTitle: string;
9
9
  noResultsDescription: string;
10
+ /** Empty state — the list could not be loaded (`variant="error"`). */
11
+ errorTitle: string;
12
+ errorDescription: string;
10
13
  /** Accessible label for an overlay-mode close button (master-detail layout, sidebar layout overlay). */
11
14
  detailClose: string;
12
15
  /** Sidebar layout — accessible label for the mobile-header menu toggle button. */
@@ -6,6 +6,8 @@ var t = {
6
6
  noDataDescription: "",
7
7
  noResultsTitle: "No matching results",
8
8
  noResultsDescription: "Try changing your search terms or filters.",
9
+ errorTitle: "Couldn’t load this list",
10
+ errorDescription: "Something went wrong while loading. Try again.",
9
11
  detailClose: "Close",
10
12
  toggleMobileMenu: "Toggle menu",
11
13
  toggleSidebar: "Toggle sidebar",
@@ -22,6 +24,8 @@ n.register("en", t), n.register("ko", {
22
24
  noDataDescription: "",
23
25
  noResultsTitle: "일치하는 결과가 없습니다",
24
26
  noResultsDescription: "검색어나 필터를 바꿔 보세요.",
27
+ errorTitle: "목록을 불러오지 못했습니다",
28
+ errorDescription: "불러오는 중 문제가 생겼습니다. 다시 시도해 주세요.",
25
29
  detailClose: "닫기",
26
30
  toggleMobileMenu: "메뉴 열고 닫기",
27
31
  toggleSidebar: "사이드바 열고 닫기",
@@ -56,6 +60,8 @@ function l(e) {
56
60
  noDataDescription: t("noDataDescription"),
57
61
  noResultsTitle: t("noResultsTitle"),
58
62
  noResultsDescription: t("noResultsDescription"),
63
+ errorTitle: t("errorTitle"),
64
+ errorDescription: t("errorDescription"),
59
65
  detailClose: t("detailClose"),
60
66
  toggleMobileMenu: t("toggleMobileMenu"),
61
67
  toggleSidebar: t("toggleSidebar"),
@@ -97,6 +97,11 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
97
97
  private renderItem;
98
98
  /** 현재 경로와 패턴 매칭 여부 확인 */
99
99
  private isMatchedLink;
100
+ /**
101
+ * 버튼 항목 클릭 — 소비자의 `onClick` 을 부른 뒤 다시 그린다. 켬/끔 버튼(`pressed`)은 그 클릭이 상태를 바꾸는 것이
102
+ * 흔하고, 설정 객체의 값이나 함수의 결과는 셸이 렌더할 때만 읽힌다.
103
+ */
104
+ private handleButtonItemClick;
100
105
  /** 브랜드 로고 클릭 핸들러: `href` 지정 시 해당 경로로, 아니면 홈으로 이동 */
101
106
  /**
102
107
  * 로고 렌더링: 아이콘명(문자열, 기존 동작) | 이미지({src,alt,href}) | 커스텀 렌더 함수.
@@ -328,7 +328,8 @@ var M = class extends i {
328
328
  .lib="${e.lib}"
329
329
  .label="${e.label}"
330
330
  .styles="${e.styles}"
331
- @click="${e.onClick}"
331
+ .pressed=${typeof e.pressed == "function" ? e.pressed() : e.pressed}
332
+ @click=${(t) => this.handleButtonItemClick(e, t)}
332
333
  ></u-sidebar-button>
333
334
  `;
334
335
  if (e.type === "menu") return this.renderMenu(e);
@@ -376,6 +377,10 @@ var M = class extends i {
376
377
  }
377
378
  return d;
378
379
  }
380
+ handleButtonItemClick(e, t) {
381
+ let n = e.onClick?.(t);
382
+ this.requestUpdate(), n instanceof Promise && n.finally(() => this.requestUpdate());
383
+ }
379
384
  static {
380
385
  this.DEFAULT_ICONS = {
381
386
  lib: "internal",
@@ -403,6 +408,7 @@ var M = class extends i {
403
408
  return u`
404
409
  <u-link class="logo-link" aria-label=${t}>
405
410
  <span class="logo">
411
+ <!-- html-sink: documented HTML contract — a logo function returns markup the developer writes -->
406
412
  ${typeof n == "string" ? g(n) : n}
407
413
  </span>
408
414
  </u-link>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iyulab/modern-app",
3
3
  "description": "web-framework by iyulab based on lit-element",
4
- "version": "0.42.2",
4
+ "version": "0.44.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -15,6 +15,11 @@ for both and a user with a filter still applied reads it as *"my data disappeare
15
15
  </u-empty-state>
16
16
 
17
17
  <u-empty-state variant="no-results"></u-empty-state>
18
+
19
+ <!-- The list could not be loaded — the next step is trying again. The title is announced (role="alert"). -->
20
+ <u-empty-state variant="error" description="Request failed (503)">
21
+ <u-button slot="actions">Try again</u-button>
22
+ </u-empty-state>
18
23
  ```
19
24
 
20
25
  ## Slots
@@ -28,7 +33,7 @@ for both and a user with a filter still applied reads it as *"my data disappeare
28
33
 
29
34
  | Property | Type | Default | Reflect | Description |
30
35
  |----------|------|---------|---------|-------------|
31
- | `variant` | `'no-data'\|'no-results'` | `'no-data'` | | Which fact is being shown; changes the default wording |
36
+ | `variant` | `'no-data'\|'no-results'\|'error'` | `'no-data'` | ✓ | Which fact is being shown; changes the default wording. `error` (could not load) announces its title |
32
37
  | `title` | `string` | `''` | | Override the default title |
33
38
  | `description` | `string` | `''` | | Override the default description |
34
39
  | `locale` | `string` | `''` | | Locale tag override for built-in strings |
@@ -181,6 +181,7 @@ interface SidebarButtonConfig {
181
181
  label: string | DirectiveResult;
182
182
  onClick: () => void;
183
183
  styles?: StyleMap<string>;
184
+ pressed?: boolean | (() => boolean); // a toggle: aria-pressed + the pressed look
184
185
  }
185
186
  ```
186
187
 
@@ -190,6 +191,21 @@ Example:
190
191
  { type: 'button', icon: 'logout', label: 'Sign Out', onClick: () => auth.signOut() }
191
192
  ```
192
193
 
194
+ A setting the person turns on and off (dark mode, a field or compact mode, notifications) is a toggle: give it
195
+ `pressed`. The button then carries `aria-pressed`, and when on it shows a soft surface and an accented icon
196
+ (`--app-sidebar-pressed-bg`, `--app-sidebar-pressed-icon-color`) — visible when the sidebar is slim, and distinct
197
+ from the current page's link. Keep one name for both states. The shell reads `pressed` on every render — a value
198
+ from the config object, or a function's result — and re-renders after the button's `onClick`, so a click that
199
+ flips the state needs nothing more:
200
+
201
+ ```typescript
202
+ const fieldMode = {
203
+ type: 'button', icon: 'hand', label: 'Field mode',
204
+ pressed: () => density.isField(),
205
+ onClick: () => density.toggleField(),
206
+ };
207
+ ```
208
+
193
209
  For a button that opens a menu, use `type: 'menu'` below — not a button plus your own popover.
194
210
 
195
211
  ---