@iyulab/modern-app 0.19.3 → 0.19.4

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.19.4] - 2026-09-15
4
+
5
+ ### Fixed
6
+
7
+ - **`u-page-header`'s back link was a 41×17 pointer target**, under the 24×24 CSS px minimum of
8
+ WCAG 2.2 SC 2.5.8. It now carries block padding with an equal negative margin, so the target is
9
+ ~25px tall while the header's layout does not move (host, title and following content keep their
10
+ exact positions). A new browser gate measures every pointer target this package renders — nav
11
+ items expanded and compact, group headers, the sidebar logo and toggle, wizard steps, and the back
12
+ link — and fails when one is undersized or not actually hit at its own position.
13
+ - **A sidebar `logo` given as an icon name vanished when the name did not resolve** (a 404 or a
14
+ typo) — the icon drew nothing and the home target collapsed to 0×0, which in the slim sidebar left
15
+ only the toggle. It now falls back to the same default icon the navigation items use. Omitting
16
+ `logo` still draws no logo.
17
+ - **The sidebar logo could not be reached from the keyboard.** Every logo form (icon name, image,
18
+ render function) was a click-only element — no focus, not announced as a link. The logo is now a
19
+ `u-link` to `href` (or the base path, as before). An image logo is named by its `alt`; icon and
20
+ custom logos are named by the app `title`.
21
+
3
22
  ## [0.19.3] - 2026-09-13
4
23
 
5
24
  ### Fixed
package/README.md CHANGED
@@ -205,6 +205,20 @@ export function App() {
205
205
  }
206
206
  ```
207
207
 
208
+ ## Accessibility
209
+
210
+ The baseline is **WCAG 2.2**. The table lists what this package **measures in tests** — it is not a
211
+ conformance claim for the success criteria it does not list.
212
+
213
+ | Success criterion | Guarantee | Measured by |
214
+ |---|---|---|
215
+ | SC 2.5.8 Target Size (Minimum) | Every pointer target this package renders itself — sidebar links, buttons and group headers (expanded and compact), the sidebar logo and toggle, wizard steps, the page header's back link — is at least 24×24 CSS px or meets the spacing exception (24px between centers), and is actually hit at that position | `tests/browser/target-size.browser.test.ts` (real Chromium) |
216
+ | SC 2.1.1 Keyboard (pointer-cursor check) | Nothing this package renders shows a pointer cursor without being an interactive element — the sidebar logo is a link | `tests/browser/target-size.browser.test.ts` |
217
+
218
+ Buttons placed through slots or rendered as `u-button` without size overrides (wizard back/next, the
219
+ master-detail close button) follow `@iyulab/components`, which measures them in its own gate. Color
220
+ contrast likewise comes from that package's tokens.
221
+
208
222
  ## Documentation
209
223
 
210
224
  | Guide | Description |
@@ -47,6 +47,11 @@ var t = e`
47
47
  font-weight: var(--u-text-label-weight, 600);
48
48
  color: var(--u-link-txt-color, #1565C0);
49
49
  text-decoration: none;
50
+ /* 타깃 크기(WCAG 2.2 SC 2.5.8) — 글자 줄 상자만으로는 높이가 17px 남짓이라 패딩으로 누를 면을 24px 넘게
51
+ 키우고, 같은 크기의 음수 여백으로 그 공간을 돌려준다 — 배치는 움직이지 않는다(접미 아이콘과 같은 기법). */
52
+ display: inline-block;
53
+ padding-block: var(--u-space-2xs, 4px);
54
+ margin-block: calc(-1 * var(--u-space-2xs, 4px));
50
55
  }
51
56
  .back::before {
52
57
  content: '←';
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * 내비게이션 항목의 **기본 아이콘** — 아이콘이 없거나 해석에 실패했을 때 대신 그린다.
3
+ * 사이드바 로고도 **이름을 줬는데 해석에 실패한 경우**에 같은 폴백을 쓴다(로고를 아예 주지 않은 경우는 제외).
3
4
  *
4
5
  * 🔴**왜 필요한가 — «안 보인다»가 «누를 수 없다»가 되는 자리다.**
5
6
  * 사이드바가 접히면(`compact`) 라벨이 숨고 아이콘만 남는다. 그 아이콘이 없으면 그 메뉴는
@@ -40,8 +40,12 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
40
40
  /** 현재 경로와 패턴 매칭 여부 확인 */
41
41
  private isMatchedLink;
42
42
  /** 브랜드 로고 클릭 핸들러: `href` 지정 시 해당 경로로, 아니면 홈으로 이동 */
43
- private handleBrandLogoClick;
44
- /** 로고 렌더링: 아이콘명(문자열, 기존 동작) | 이미지({src,alt,href}) | 커스텀 렌더 함수 */
43
+ /**
44
+ * 로고 렌더링: 아이콘명(문자열, 기존 동작) | 이미지({src,alt,href}) | 커스텀 렌더 함수.
45
+ *
46
+ * 🔴로고는 홈(또는 `href`)으로 가는 **링크**다 — 종전엔 클릭만 받는 `img`·`span`·`u-icon` 이라 키보드로 닿지 않았고
47
+ * 보조기술에 링크로 드러나지 않았다. `u-link` 는 `href` 가 없으면 basepath 로 SPA 이동하므로 종전 `app.navigate('')` 와 같다.
48
+ */
45
49
  private renderLogo;
46
50
  /** 사이드바 토글 핸들러 */
47
51
  private handleToggleButtonClick;
@@ -5,32 +5,31 @@ import r from "../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorate.js
5
5
  import { StyledElement as i } from "../internals/StyledElement.js";
6
6
  import { getLocaleStrings as a } from "../internals/locale.js";
7
7
  import "../components/SidebarSection.js";
8
+ import { DEFAULT_NAV_ICON as o } from "../internals/nav-icon.js";
8
9
  import "../components/SidebarLink.js";
9
10
  import "../components/SidebarGroup.js";
10
11
  import "../components/SidebarButton.js";
11
- import { styles as o } from "./SidebarLayout.styles.js";
12
- import { html as s, nothing as c } from "lit";
13
- import { customElement as l, property as u, query as d, state as f } from "lit/decorators.js";
12
+ import { styles as s } from "./SidebarLayout.styles.js";
13
+ import { html as c, nothing as l } from "lit";
14
+ import { customElement as u, property as d, query as f, state as p } from "lit/decorators.js";
14
15
  import "@iyulab/components/dist/components/button/UButton.js";
15
16
  import "@iyulab/components/dist/components/icon/UIcon.js";
16
- import { unsafeHTML as p } from "lit/directives/unsafe-html.js";
17
- import { repeat as m } from "lit/directives/repeat.js";
18
- import { ifDefined as h } from "lit/directives/if-defined.js";
19
- import { createDevWarner as g } from "@iyulab/components/dist/utilities/devWarning.js";
20
- import { UProgressBar as _ } from "@iyulab/components/dist/components/progress-bar/UProgressBar.js";
17
+ import { unsafeHTML as m } from "lit/directives/unsafe-html.js";
18
+ import { repeat as h } from "lit/directives/repeat.js";
19
+ import { ifDefined as g } from "lit/directives/if-defined.js";
20
+ import { createDevWarner as _ } from "@iyulab/components/dist/utilities/devWarning.js";
21
+ import { UProgressBar as v } from "@iyulab/components/dist/components/progress-bar/UProgressBar.js";
21
22
  //#region src/layouts/SidebarLayout.ts
22
- var v = g("@iyulab/modern-app"), y = 0;
23
- function b(e) {
23
+ var y = _("@iyulab/modern-app"), b = 0;
24
+ function x(e) {
24
25
  let t = e.tagName;
25
26
  if (t === "INPUT" || t === "TEXTAREA" || t === "SELECT" || e.isContentEditable) return !0;
26
27
  let n = e.getAttribute("role");
27
28
  return n === "textbox" || n === "searchbox" || n === "combobox" || n === "spinbutton";
28
29
  }
29
- var x = class extends i {
30
+ var S = class extends i {
30
31
  constructor(...t) {
31
- super(...t), this.state = "default", this.locale = "", this.context = null, this.unsizedWarnKey = `unsized:${++y}`, this.isMatchedLink = (e) => !this.context || !e ? !1 : (e = typeof e == "string" ? new URLPattern(e, window.location.origin) : e, e.test(this.context.path, window.location.origin)), this.handleBrandLogoClick = (t) => () => {
32
- e.navigate(t ?? "");
33
- }, this.handleToggleButtonClick = () => {
32
+ super(...t), this.state = "default", this.locale = "", this.context = null, this.unsizedWarnKey = `unsized:${++b}`, this.isMatchedLink = (e) => !this.context || !e ? !1 : (e = typeof e == "string" ? new URLPattern(e, window.location.origin) : e, e.test(this.context.path, window.location.origin)), this.handleToggleButtonClick = () => {
34
33
  let t = e.screen ?? "large";
35
34
  t === "large" ? this.state = this.state === "default" ? "slim" : "default" : t === "medium" ? this.state = this.state === "slim" ? "modal" : "slim" : t === "small" ? this.state = this.state === "mobile" ? "mobile-open" : "mobile" : console.warn("Unknown screen size:", t);
36
35
  }, this.handleBackdropClick = () => {
@@ -51,7 +50,7 @@ var x = class extends i {
51
50
  }, 300);
52
51
  }, this._handleMainKeydown = (e) => {
53
52
  let t = e.composedPath()[0];
54
- if (t instanceof HTMLElement && b(t)) return;
53
+ if (t instanceof HTMLElement && x(t)) return;
55
54
  let n = this.shadowRoot?.querySelector(".main");
56
55
  if (!n) return;
57
56
  let r = n.clientHeight;
@@ -84,7 +83,7 @@ var x = class extends i {
84
83
  };
85
84
  }
86
85
  static {
87
- this.styles = [super.styles, o];
86
+ this.styles = [super.styles, s];
88
87
  }
89
88
  get mainElement() {
90
89
  return this.shadowRoot?.querySelector(".main") ?? null;
@@ -105,11 +104,11 @@ var x = class extends i {
105
104
  process.env.NODE_ENV !== "production" && requestAnimationFrame(() => {
106
105
  if (!this.isConnected) return;
107
106
  let e = this.getBoundingClientRect().height;
108
- e >= 200 || v(this.unsizedWarnKey, `u-sidebar-layout is only ${Math.round(e)}px tall — its height: 100% found no sized ancestor, so the shell sits at its own chrome height and the route area has almost no room. Give the root element a height (e.g. #app { height: 100vh } — app.load() does this for document.body).`);
107
+ e >= 200 || y(this.unsizedWarnKey, `u-sidebar-layout is only ${Math.round(e)}px tall — its height: 100% found no sized ancestor, so the shell sits at its own chrome height and the route area has almost no room. Give the root element a height (e.g. #app { height: 100vh } — app.load() does this for document.body).`);
109
108
  });
110
109
  }
111
110
  render() {
112
- return this.config ? s`
111
+ return this.config ? c`
113
112
  <!-- Mobile Header -->
114
113
  <div class="mobile-header" part="mobile-header" ?hidden="${!this.state.startsWith("mobile")}">
115
114
  ${this.renderLogo()}
@@ -146,13 +145,13 @@ var x = class extends i {
146
145
 
147
146
  <!-- Sidebar Navigation Menu -->
148
147
  <nav class="sidebar-main" part="sidebar-main" scrollable
149
- aria-label=${this.config.mainAriaLabel ?? c}>
150
- ${m(t(this.config.main ?? [], this.config.hasPermission), (e, t) => t, (e) => this.renderItem(e))}
148
+ aria-label=${this.config.mainAriaLabel ?? l}>
149
+ ${h(t(this.config.main ?? [], this.config.hasPermission), (e, t) => t, (e) => this.renderItem(e))}
151
150
  </nav>
152
151
 
153
152
  <!-- Sidebar Footer -->
154
153
  <div class="sidebar-footer" part="sidebar-footer">
155
- ${m(t(this.config.footer ?? [], this.config.hasPermission), (e, t) => t, (e) => this.renderItem(e))}
154
+ ${h(t(this.config.footer ?? [], this.config.hasPermission), (e, t) => t, (e) => this.renderItem(e))}
156
155
  </div>
157
156
  </aside>
158
157
 
@@ -167,17 +166,17 @@ var x = class extends i {
167
166
  <div class="backdrop" ?hidden="${this.state !== "modal"}"
168
167
  @click="${this.handleBackdropClick}"
169
168
  ></div>
170
- ` : c;
169
+ ` : l;
171
170
  }
172
171
  renderItem(e) {
173
- if (!e) return c;
172
+ if (!e) return l;
174
173
  if (e.type === "html") {
175
174
  let t = e.render(this.state);
176
- return typeof t == "string" ? p(t) : s`${t}`;
175
+ return typeof t == "string" ? m(t) : c`${t}`;
177
176
  }
178
- if (e.type === "button") return s`
177
+ if (e.type === "button") return c`
179
178
  <u-sidebar-button
180
- id=${h(e.id)}
179
+ id=${g(e.id)}
181
180
  ?compact=${this.state === "slim"}
182
181
  .icon="${e.icon}"
183
182
  .lib="${e.lib}"
@@ -188,7 +187,7 @@ var x = class extends i {
188
187
  `;
189
188
  if (e.type === "link") {
190
189
  let t = this.isMatchedLink(e.pattern || e.href);
191
- return s`
190
+ return c`
192
191
  <u-sidebar-link
193
192
  ?compact=${this.state === "slim"}
194
193
  ?selected=${t}
@@ -203,18 +202,18 @@ var x = class extends i {
203
202
  ></u-sidebar-link>
204
203
  `;
205
204
  }
206
- if (e.type === "section") return s`
205
+ if (e.type === "section") return c`
207
206
  <u-sidebar-section
208
207
  ?compact=${this.state === "slim"}
209
208
  .mainTitle="${e.title}"
210
209
  .subTitle="${e.subTitle}"
211
210
  .styles="${e.styles}">
212
- ${m(e.items, (e, t) => t, (e) => this.renderItem(e))}
211
+ ${h(e.items, (e, t) => t, (e) => this.renderItem(e))}
213
212
  </u-sidebar-section>
214
213
  `;
215
214
  if (e.type === "group") {
216
215
  let t = e.items.some((e) => this.isMatchedLink(e.pattern || e.href));
217
- return s`
216
+ return c`
218
217
  <u-sidebar-group
219
218
  ?compact=${this.state === "slim"}
220
219
  ?selected=${t}
@@ -223,40 +222,45 @@ var x = class extends i {
223
222
  .lib="${e.lib}"
224
223
  .label="${e.label}"
225
224
  .styles="${e.styles}">
226
- ${m(e.items, (e, t) => t, (e) => this.renderItem(e))}
225
+ ${h(e.items, (e, t) => t, (e) => this.renderItem(e))}
227
226
  </u-sidebar-group>
228
227
  `;
229
228
  }
230
- return c;
229
+ return l;
231
230
  }
232
231
  renderLogo() {
233
- let e = this.config?.logo;
234
- if (!e || typeof e == "string") return s`
235
- <u-icon class="logo"
236
- .name="${e}"
237
- @click=${this.handleBrandLogoClick()}
238
- ></u-icon>
239
- `;
232
+ let e = this.config?.logo, t = this.config?.title ?? "Home";
233
+ if (!e || typeof e == "string") return e ? c`
234
+ <u-link class="logo-link" aria-label=${t}>
235
+ <u-icon class="logo"
236
+ .name="${e}"
237
+ .fallback=${o}
238
+ ></u-icon>
239
+ </u-link>
240
+ ` : c`<u-icon class="logo"></u-icon>`;
240
241
  if (typeof e == "function") {
241
- let t = e(this.state);
242
- return s`
243
- <span class="logo" @click=${this.handleBrandLogoClick()}>
244
- ${typeof t == "string" ? p(t) : t}
245
- </span>
242
+ let n = e(this.state);
243
+ return c`
244
+ <u-link class="logo-link" aria-label=${t}>
245
+ <span class="logo">
246
+ ${typeof n == "string" ? m(n) : n}
247
+ </span>
248
+ </u-link>
246
249
  `;
247
250
  }
248
- return s`
249
- <img class="logo"
250
- src="${e.src}"
251
- alt="${e.alt ?? ""}"
252
- @click=${this.handleBrandLogoClick(e.href)}
253
- />
251
+ return c`
252
+ <u-link class="logo-link" .href=${e.href} aria-label=${e.alt ? l : t}>
253
+ <img class="logo"
254
+ src="${e.src}"
255
+ alt="${e.alt ?? ""}"
256
+ />
257
+ </u-link>
254
258
  `;
255
259
  }
256
260
  };
257
- r([u({
261
+ r([d({
258
262
  type: String,
259
263
  reflect: !0
260
- }), n("design:type", Object)], x.prototype, "state", void 0), r([u({ type: Object }), n("design:type", Object)], x.prototype, "config", void 0), r([u({ type: String }), n("design:type", Object)], x.prototype, "locale", void 0), r([d("u-progress-bar"), n("design:type", _ === void 0 ? Object : _)], x.prototype, "progressBarEl", void 0), r([f(), n("design:type", Object)], x.prototype, "context", void 0), x = r([l("u-sidebar-layout")], x);
264
+ }), n("design:type", Object)], S.prototype, "state", void 0), r([d({ type: Object }), n("design:type", Object)], S.prototype, "config", void 0), r([d({ type: String }), n("design:type", Object)], S.prototype, "locale", void 0), r([f("u-progress-bar"), n("design:type", v === void 0 ? Object : v)], S.prototype, "progressBarEl", void 0), r([p(), n("design:type", Object)], S.prototype, "context", void 0), S = r([u("u-sidebar-layout")], S);
261
265
  //#endregion
262
- export { x as SidebarLayout };
266
+ export { S as SidebarLayout };
@@ -28,6 +28,11 @@ var t = e`
28
28
  width: auto;
29
29
  object-fit: contain;
30
30
  }
31
+ /* 로고를 감싼 링크 — 인라인 기준선 여백이 생기지 않게 로고 상자에 딱 맞춘다. */
32
+ .logo-link {
33
+ display: inline-flex;
34
+ align-items: center;
35
+ }
31
36
 
32
37
  /*
33
38
  * ★단 배정은 «관측값 근사»가 아니라 «용도»로 한다 — 이 자리는 앱의 제목이다.
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.19.3",
4
+ "version": "0.19.4",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -60,7 +60,7 @@
60
60
  "typecheck": "tsc --noEmit"
61
61
  },
62
62
  "dependencies": {
63
- "@iyulab/router": "^0.12.0",
63
+ "@iyulab/router": "^0.13.0",
64
64
  "i18next": "^26.3.6",
65
65
  "lit": "^3.3.3"
66
66
  },
@@ -82,13 +82,13 @@
82
82
  "@types/node": "^26.1.1",
83
83
  "@types/react": "^19.2.14",
84
84
  "@types/react-dom": "^19.2.3",
85
- "@vitest/browser-playwright": "^4.1.10",
85
+ "@vitest/browser-playwright": "^5.0.0",
86
86
  "happy-dom": "^20.10.6",
87
87
  "react": "^19.2.7",
88
88
  "react-dom": "^19.2.7",
89
89
  "typescript": "^6.0.2",
90
90
  "vite": "^8.1.4",
91
91
  "vite-plugin-dts": "^5.0.3",
92
- "vitest": "^4.1.10"
92
+ "vitest": "^5.0.0"
93
93
  }
94
94
  }