@iyulab/modern-app 0.19.2 → 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,36 @@
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
+
22
+ ## [0.19.3] - 2026-09-13
23
+
24
+ ### Fixed
25
+
26
+ - **The "shell has no sized ancestor" dev-mode warning was a complete no-op** — its own guard used
27
+ `import.meta.env.DEV`, which Vite resolves statically at this package's own build time, so the
28
+ published dist always shipped with the check baked to `false` and the warning removed entirely
29
+ by dead-code elimination. Replaced with `process.env.NODE_ENV !== 'production'`. The
30
+ `@iyulab/components` peer floor moves to `>=1.40.3`, which fixes the identical defect in the
31
+ shared `createDevWarner` helper this warning calls into — installing 0.19.3 without that
32
+ components fix would still produce no warning.
33
+
3
34
  ## [0.19.2] - 2026-09-13
4
35
 
5
36
  ### Changed
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,33 +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
- g("@iyulab/modern-app");
23
- var v = 0;
24
- function y(e) {
23
+ var y = _("@iyulab/modern-app"), b = 0;
24
+ function x(e) {
25
25
  let t = e.tagName;
26
26
  if (t === "INPUT" || t === "TEXTAREA" || t === "SELECT" || e.isContentEditable) return !0;
27
27
  let n = e.getAttribute("role");
28
28
  return n === "textbox" || n === "searchbox" || n === "combobox" || n === "spinbutton";
29
29
  }
30
- var b = class extends i {
30
+ var S = class extends i {
31
31
  constructor(...t) {
32
- super(...t), this.state = "default", this.locale = "", this.context = null, this.unsizedWarnKey = `unsized:${++v}`, 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) => () => {
33
- e.navigate(t ?? "");
34
- }, 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 = () => {
35
33
  let t = e.screen ?? "large";
36
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);
37
35
  }, this.handleBackdropClick = () => {
@@ -52,7 +50,7 @@ var b = class extends i {
52
50
  }, 300);
53
51
  }, this._handleMainKeydown = (e) => {
54
52
  let t = e.composedPath()[0];
55
- if (t instanceof HTMLElement && y(t)) return;
53
+ if (t instanceof HTMLElement && x(t)) return;
56
54
  let n = this.shadowRoot?.querySelector(".main");
57
55
  if (!n) return;
58
56
  let r = n.clientHeight;
@@ -85,7 +83,7 @@ var b = class extends i {
85
83
  };
86
84
  }
87
85
  static {
88
- this.styles = [super.styles, o];
86
+ this.styles = [super.styles, s];
89
87
  }
90
88
  get mainElement() {
91
89
  return this.shadowRoot?.querySelector(".main") ?? null;
@@ -102,9 +100,15 @@ var b = class extends i {
102
100
  firstUpdated(e) {
103
101
  super.firstUpdated(e), this.warnIfUnsized();
104
102
  }
105
- warnIfUnsized() {}
103
+ warnIfUnsized() {
104
+ process.env.NODE_ENV !== "production" && requestAnimationFrame(() => {
105
+ if (!this.isConnected) return;
106
+ let e = this.getBoundingClientRect().height;
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).`);
108
+ });
109
+ }
106
110
  render() {
107
- return this.config ? s`
111
+ return this.config ? c`
108
112
  <!-- Mobile Header -->
109
113
  <div class="mobile-header" part="mobile-header" ?hidden="${!this.state.startsWith("mobile")}">
110
114
  ${this.renderLogo()}
@@ -141,13 +145,13 @@ var b = class extends i {
141
145
 
142
146
  <!-- Sidebar Navigation Menu -->
143
147
  <nav class="sidebar-main" part="sidebar-main" scrollable
144
- aria-label=${this.config.mainAriaLabel ?? c}>
145
- ${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))}
146
150
  </nav>
147
151
 
148
152
  <!-- Sidebar Footer -->
149
153
  <div class="sidebar-footer" part="sidebar-footer">
150
- ${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))}
151
155
  </div>
152
156
  </aside>
153
157
 
@@ -162,17 +166,17 @@ var b = class extends i {
162
166
  <div class="backdrop" ?hidden="${this.state !== "modal"}"
163
167
  @click="${this.handleBackdropClick}"
164
168
  ></div>
165
- ` : c;
169
+ ` : l;
166
170
  }
167
171
  renderItem(e) {
168
- if (!e) return c;
172
+ if (!e) return l;
169
173
  if (e.type === "html") {
170
174
  let t = e.render(this.state);
171
- return typeof t == "string" ? p(t) : s`${t}`;
175
+ return typeof t == "string" ? m(t) : c`${t}`;
172
176
  }
173
- if (e.type === "button") return s`
177
+ if (e.type === "button") return c`
174
178
  <u-sidebar-button
175
- id=${h(e.id)}
179
+ id=${g(e.id)}
176
180
  ?compact=${this.state === "slim"}
177
181
  .icon="${e.icon}"
178
182
  .lib="${e.lib}"
@@ -183,7 +187,7 @@ var b = class extends i {
183
187
  `;
184
188
  if (e.type === "link") {
185
189
  let t = this.isMatchedLink(e.pattern || e.href);
186
- return s`
190
+ return c`
187
191
  <u-sidebar-link
188
192
  ?compact=${this.state === "slim"}
189
193
  ?selected=${t}
@@ -198,18 +202,18 @@ var b = class extends i {
198
202
  ></u-sidebar-link>
199
203
  `;
200
204
  }
201
- if (e.type === "section") return s`
205
+ if (e.type === "section") return c`
202
206
  <u-sidebar-section
203
207
  ?compact=${this.state === "slim"}
204
208
  .mainTitle="${e.title}"
205
209
  .subTitle="${e.subTitle}"
206
210
  .styles="${e.styles}">
207
- ${m(e.items, (e, t) => t, (e) => this.renderItem(e))}
211
+ ${h(e.items, (e, t) => t, (e) => this.renderItem(e))}
208
212
  </u-sidebar-section>
209
213
  `;
210
214
  if (e.type === "group") {
211
215
  let t = e.items.some((e) => this.isMatchedLink(e.pattern || e.href));
212
- return s`
216
+ return c`
213
217
  <u-sidebar-group
214
218
  ?compact=${this.state === "slim"}
215
219
  ?selected=${t}
@@ -218,40 +222,45 @@ var b = class extends i {
218
222
  .lib="${e.lib}"
219
223
  .label="${e.label}"
220
224
  .styles="${e.styles}">
221
- ${m(e.items, (e, t) => t, (e) => this.renderItem(e))}
225
+ ${h(e.items, (e, t) => t, (e) => this.renderItem(e))}
222
226
  </u-sidebar-group>
223
227
  `;
224
228
  }
225
- return c;
229
+ return l;
226
230
  }
227
231
  renderLogo() {
228
- let e = this.config?.logo;
229
- if (!e || typeof e == "string") return s`
230
- <u-icon class="logo"
231
- .name="${e}"
232
- @click=${this.handleBrandLogoClick()}
233
- ></u-icon>
234
- `;
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>`;
235
241
  if (typeof e == "function") {
236
- let t = e(this.state);
237
- return s`
238
- <span class="logo" @click=${this.handleBrandLogoClick()}>
239
- ${typeof t == "string" ? p(t) : t}
240
- </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>
241
249
  `;
242
250
  }
243
- return s`
244
- <img class="logo"
245
- src="${e.src}"
246
- alt="${e.alt ?? ""}"
247
- @click=${this.handleBrandLogoClick(e.href)}
248
- />
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>
249
258
  `;
250
259
  }
251
260
  };
252
- r([u({
261
+ r([d({
253
262
  type: String,
254
263
  reflect: !0
255
- }), n("design:type", Object)], b.prototype, "state", void 0), r([u({ type: Object }), n("design:type", Object)], b.prototype, "config", void 0), r([u({ type: String }), n("design:type", Object)], b.prototype, "locale", void 0), r([d("u-progress-bar"), n("design:type", _ === void 0 ? Object : _)], b.prototype, "progressBarEl", void 0), r([f(), n("design:type", Object)], b.prototype, "context", void 0), b = r([l("u-sidebar-layout")], b);
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);
256
265
  //#endregion
257
- export { b 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.2",
4
+ "version": "0.19.4",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -60,12 +60,12 @@
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
  },
67
67
  "peerDependencies": {
68
- "@iyulab/components": ">=1.40.2",
68
+ "@iyulab/components": ">=1.40.3",
69
69
  "@lit/react": ">=1.0.8",
70
70
  "react": ">=18.0.0"
71
71
  },
@@ -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
  }