@iyulab/modern-app 0.18.16 → 0.18.19

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,46 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.18.19] - 2026-09-04
4
+
5
+ ### Added
6
+
7
+ - **`SidebarLayout` had no way for a consumer to control scroll position on route
8
+ change, and no official way to reach its scroll container at all** — only an
9
+ undocumented shadow-DOM `part="main"` selector, a style-only CSS hook rather than a
10
+ JS access contract. Added `layout.scrollBehavior(context, main)`, called on every
11
+ `route-done` before focus moves to the container, and a public `mainElement`
12
+ accessor on the element for reading/writing scroll position outside the hook (e.g.
13
+ saving a position from a `route-begin` listener). Unset `scrollBehavior` does
14
+ nothing, matching Vue Router's own default for an unset `scrollBehavior`.
15
+
16
+ ## [0.18.18] - 2026-09-04
17
+
18
+ ### Fixed
19
+
20
+ - **The React JSX declaration for `<u-info-field>`'s `value` prop was narrowed to
21
+ `string`, while the component's own class field type (`unknown`) and documented
22
+ behavior explicitly accept `null` and numbers.** TypeScript-strict consumers hit
23
+ `TS2322` passing a nullable or numeric value — a very common shape for
24
+ API-sourced data — even though rendering already coerces whatever is passed via
25
+ `String()`/the format helpers. Widened the JSX type to `unknown` to match. Added
26
+ a compile-only regression fixture (`tests/types/react-consumption.tsx`) covering
27
+ the `string | null` and `number | undefined` shapes that previously failed.
28
+
29
+ ## [0.18.17] - 2026-09-03
30
+
31
+ ### Fixed
32
+
33
+ - **`AppConfig.auth` (the boot-time auth gate) and its `AuthGateConfig`/`AuthGateContext`
34
+ types were entirely undocumented in both reference docs** — the feature has a full worked
35
+ example in the README but no type reference anywhere a consumer would look one up. Also
36
+ documented `app.user` (the authenticated-user getter), and added `enter`/`initialLoad`/
37
+ `useIntercept` — three more real `AppConfig` fields missing specifically from
38
+ `skills/modern-app/references/api.md`.
39
+ - **`FallbackRouteConfig.title` was dropped by `0.18.13`**, which only touched the `render`
40
+ callback's context type and didn't notice it removed the field. Restored in both docs.
41
+ Found by a new internal tool (`type-doc-check.js`) that diffs hand-copied TS interface doc
42
+ snippets against source.
43
+
3
44
  ## [0.18.16] - 2026-09-03
4
45
 
5
46
  ### Fixed
@@ -98,7 +98,9 @@ declare module 'react' {
98
98
  interface IntrinsicElements {
99
99
  'u-info-field': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
100
100
  label?: string;
101
- value?: string;
101
+ /** 클래스 필드와 동일하게 `unknown` — 렌더 로직이 `String(value)`/포맷터로 무엇이
102
+ * 오든 처리하므로 `null`·숫자를 그대로 넘길 수 있다(위 클래스 필드 JSDoc 참조). */
103
+ value?: unknown;
102
104
  blank?: string;
103
105
  numeric?: boolean;
104
106
  format?: InfoFieldFormat;
@@ -14,6 +14,13 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
14
14
  progressBarEl: UProgressBar;
15
15
  /** 현재 라우터 컨텍스트 */
16
16
  context: RouteContext | null;
17
+ /**
18
+ * 라우트 컨텐츠의 실제 스크롤 컨테이너(섀도 DOM `[part="main"]`). `scrollTop`을 읽어
19
+ * 위치를 저장하거나, 써서 복원한다 — `part="main"`은 스타일링용 CSS 훅일 뿐 JS 접근
20
+ * 계약이 아니었으므로, 소비자가 이 컨테이너에 안정적으로 접근할 공식 수단으로 신설.
21
+ * 아직 렌더 전이면 `null`.
22
+ */
23
+ get mainElement(): HTMLElement | null;
17
24
  connectedCallback(): void;
18
25
  disconnectedCallback(): void;
19
26
  protected willUpdate(changedProperties: PropertyValues): void;
@@ -40,7 +40,9 @@ var v = class extends i {
40
40
  }, this.handleRouteDone = (e) => {
41
41
  this.progressBarEl.value = 100, setTimeout(() => {
42
42
  this.progressBarEl.removeAttribute("visible");
43
- }, 300), (this.shadowRoot?.querySelector(".main"))?.focus({ preventScroll: !0 });
43
+ }, 300);
44
+ let t = this.shadowRoot?.querySelector(".main");
45
+ t && (this.config?.scrollBehavior?.(e.context, t), t.focus({ preventScroll: !0 }));
44
46
  }, this.handleRouteError = (e) => {
45
47
  this.progressBarEl.status = "error", this.progressBarEl.value = 100, setTimeout(() => {
46
48
  this.progressBarEl.removeAttribute("visible"), this.progressBarEl.status = "default";
@@ -82,6 +84,9 @@ var v = class extends i {
82
84
  static {
83
85
  this.styles = [super.styles, o];
84
86
  }
87
+ get mainElement() {
88
+ return this.shadowRoot?.querySelector(".main") ?? null;
89
+ }
85
90
  connectedCallback() {
86
91
  super.connectedCallback(), window.addEventListener("route-begin", this.handleRouteBegin), window.addEventListener("route-done", this.handleRouteDone), window.addEventListener("route-progress", this.handleRouteProgress), window.addEventListener("route-error", this.handleRouteError), window.addEventListener("screen-resize", this.handleScreenResize);
87
92
  }
@@ -1,4 +1,5 @@
1
1
  import { TemplateResult } from 'lit';
2
+ import { RouteContext } from '@iyulab/router';
2
3
  import { StyleMap } from '../internals/StyledElement';
3
4
  import { SidebarLinkConfig } from '../components/SidebarLink';
4
5
  import { SidebarSectionConfig } from '../components/SidebarSection';
@@ -55,6 +56,14 @@ export interface SidebarLayoutConfig {
55
56
  * 미지정 시 필터링하지 않는다(모든 항목 표시). 보통 `@iyulab/enterprise` 의 `hasPermission` 을 넘긴다.
56
57
  */
57
58
  hasPermission?: (code: string) => boolean;
59
+ /**
60
+ * 라우트 전환이 끝날 때(`route-done`, 포커스가 메인 스크롤 컨테이너로 옮겨지기 직전)
61
+ * 호출된다 — 새 라우트의 `RouteContext`와 그 스크롤 컨테이너(`SidebarLayout.mainElement`와
62
+ * 동일 엘리먼트)를 받는다. 스크롤 위치를 리셋·저장·복원하는 로직은 이 훅 안에서 직접
63
+ * 구현한다(예: 목록→상세로 갔다가 돌아올 때 스크롤 위치 복원). 미지정 시 기본값은
64
+ * "아무것도 안 함" — Vue Router의 `scrollBehavior` 미지정 기본값과 동일하다(breaking 아님).
65
+ */
66
+ scrollBehavior?: (context: RouteContext, main: HTMLElement) => void;
58
67
  /** 사이드바 스타일 맵 */
59
68
  styles?: StyleMap<SidebarParts>;
60
69
  }
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.18.16",
4
+ "version": "0.18.19",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -19,6 +19,18 @@ interface AppConfig {
19
19
  /** Fallback rendered on 404 or unhandled errors. */
20
20
  fallback?: FallbackRouteConfig;
21
21
 
22
+ /**
23
+ * Global auth/authorization guard, called before every navigation.
24
+ * `string` = redirect, `false` = cancel (403), `true`/undefined = proceed.
25
+ */
26
+ enter?: (ctx: RouteContext) => Promise<string | boolean> | string | boolean;
27
+
28
+ /** Auto-navigate to the current URL on load. Default: true */
29
+ initialLoad?: boolean;
30
+
31
+ /** Intercept `<a>` tag clicks for client-side routing. Default: true */
32
+ useIntercept?: boolean;
33
+
22
34
  /** Layout configuration. Currently only 'sidebar' is supported. */
23
35
  layout: LayoutConfig;
24
36
 
@@ -27,6 +39,12 @@ interface AppConfig {
27
39
 
28
40
  /** i18next options plus optional plugins array. Omit to skip i18n. */
29
41
  i18n?: I18nInitOptions;
42
+
43
+ /**
44
+ * Boot-time auth gate. When set, resolves the session via `me()` before the app shell
45
+ * is built. Omit for full backward compatibility (no gate). See `AuthGateConfig` below.
46
+ */
47
+ auth?: AuthGateConfig;
30
48
  }
31
49
  ```
32
50
 
@@ -162,6 +180,9 @@ interface RouteContext {
162
180
 
163
181
  ```typescript
164
182
  interface FallbackRouteConfig {
183
+ /** Sets `document.title` when the fallback renders. */
184
+ title?: string;
185
+
165
186
  render: (context: RouteContext & { error: RouteError }) => RenderResult | Promise<RenderResult>;
166
187
  }
167
188
  ```
@@ -196,6 +217,37 @@ interface NotificationOptions {
196
217
 
197
218
  ---
198
219
 
220
+ ## `AuthGateConfig` / `AuthGateContext`
221
+
222
+ The framework owns only the orchestration (check → branch → reload) — session lookup/login itself
223
+ (HTTP), the user/permission shape, and mid-session 401 handling belong to the app (or
224
+ `@iyulab/enterprise`'s `createAuthClient`/`createODataService`).
225
+
226
+ ```typescript
227
+ interface AuthGateConfig {
228
+ /** Resolve the current session. Return a value for authenticated, `null`/`undefined` for not. */
229
+ me: () => Promise<unknown | null | undefined> | unknown | null | undefined;
230
+
231
+ /**
232
+ * Renders login UI into `context.root` when unauthenticated. Call `context.onSuccess()` on
233
+ * success. Return a cleanup function to have it called on app load/`unload`.
234
+ */
235
+ renderLogin: (context: AuthGateContext) => (() => void) | void;
236
+
237
+ /** Called once authenticated, right before the app shell is built. */
238
+ onAuthenticated?: (user: unknown) => void | Promise<void>;
239
+ }
240
+
241
+ interface AuthGateContext {
242
+ /** Root element to render the login UI into (same as `AppConfig.root`, default `document.body`). */
243
+ root: Element;
244
+ /** Call on successful login — the app (re)loads and the shell appears. */
245
+ onSuccess: () => void;
246
+ }
247
+ ```
248
+
249
+ ---
250
+
199
251
  ## `app` singleton methods
200
252
 
201
253
  | Method | Signature | Description |
@@ -216,5 +268,6 @@ interface NotificationOptions {
216
268
  | `config` | `AppConfig \| undefined` | Current config passed to `load()` |
217
269
  | `router` | `Router \| undefined` | Underlying `@iyulab/router` instance |
218
270
  | `screen` | `ScreenSize \| undefined` | Current responsive screen size |
271
+ | `user` | `unknown` | Authenticated user when the `auth` boot gate is used; `undefined` if unauthenticated or unused |
219
272
  | `theme` | `Theme` (static) | Theme utility (`get`, `set`, `isInitialized`) |
220
273
  | `i18n` | `i18next` | Raw i18next instance |
@@ -24,6 +24,9 @@ interface SidebarLayoutConfig {
24
24
  /** Permission filter — hides items (and emptied section/groups) whose requirement fails. Unset shows everything. See "권한 기반 메뉴 필터" below. */
25
25
  hasPermission?: (code: string) => boolean;
26
26
 
27
+ /** Called on `route-done` (before focus moves to the container) with the new route's `RouteContext` and the scroll container itself — implement reset/save/restore here. Unset does nothing (same default as Vue Router's unset `scrollBehavior`). Also reachable outside the hook via the element's `.mainElement` accessor. */
28
+ scrollBehavior?: (context: RouteContext, main: HTMLElement) => void;
29
+
27
30
  /** Per-part style overrides (CSS custom properties / inline styles). */
28
31
  styles?: StyleMap<SidebarParts>;
29
32
  }