@iyulab/modern-app 0.46.0 → 0.47.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 +21 -0
- package/README.md +4 -3
- package/dist/App.js +52 -51
- package/dist/components/EmptyState.d.ts +11 -4
- package/dist/components/EmptyState.js +9 -4
- package/dist/components/MasterDetailLayout.js +4 -4
- package/dist/components/PageHeader.js +6 -6
- package/dist/index.js +10 -10
- package/dist/internals/default-fallback.d.ts +16 -0
- package/dist/internals/default-fallback.js +16 -0
- package/dist/internals/locale.d.ts +6 -0
- package/dist/internals/locale.js +9 -0
- package/dist/layouts/SidebarLayout.js +23 -23
- package/dist/types/AppConfigs.d.ts +2 -0
- package/package.json +1 -1
- package/skills/modern-app/references/api.md +3 -0
- package/skills/modern-app/references/components/empty-state.md +31 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.47.0] - 2026-10-08
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **An app with no `fallback` draws a failed route as `<u-empty-state>`**, inside the shell: a route `enter` guard's
|
|
8
|
+
refusal (403) as `no-access`, an address with no route (404) as the new `not-found`, and any other failure (the
|
|
9
|
+
route could not be loaded or rendered) as `error`, titled "Couldn’t open this page" with the error message as its
|
|
10
|
+
description. Before, the router's own error page (a large status code) was drawn — outside this package's look, and
|
|
11
|
+
a refusal looked like an outage. A `fallback` you pass is used as before and replaces the default entirely.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`u-empty-state variant="not-found"`** — nothing at this address; the next step is checking the address or going
|
|
16
|
+
back. Not announced as an alert. Default wording in English and Korean (`notFoundTitle`/`notFoundDescription` in
|
|
17
|
+
`modernAppLocale`), and `routeErrorTitle` for the default fallback's error screen.
|
|
18
|
+
|
|
19
|
+
### Documentation
|
|
20
|
+
|
|
21
|
+
- The empty-state reference's blocked-routes example passes a complete app config (the published 0.46.0 skill
|
|
22
|
+
document left out the required `layout`).
|
|
23
|
+
|
|
3
24
|
## [0.46.0] - 2026-10-08
|
|
4
25
|
|
|
5
26
|
### Added
|
package/README.md
CHANGED
|
@@ -38,13 +38,14 @@ await app.load({
|
|
|
38
38
|
{ path: 'users', render: () => html`<users-page></users-page>` },
|
|
39
39
|
{ path: 'users/:id', render: (ctx) => html`<user-detail .userId=${ctx.params.id}></user-detail>` },
|
|
40
40
|
],
|
|
41
|
-
fallback: {
|
|
42
|
-
render: (ctx) => html`<error-page .error=${ctx.error}></error-page>`,
|
|
43
|
-
},
|
|
44
41
|
theme: { default: 'system' },
|
|
45
42
|
});
|
|
46
43
|
```
|
|
47
44
|
|
|
45
|
+
A route that fails is drawn inside the shell as an empty state: a guard's refusal (403) as
|
|
46
|
+
`<u-empty-state variant="no-access">`, an unknown address (404) as `variant="not-found"`, anything else as
|
|
47
|
+
`variant="error"` with the error message. Pass `fallback: { render: (ctx) => … }` to draw your own instead.
|
|
48
|
+
|
|
48
49
|
### Icons
|
|
49
50
|
|
|
50
51
|
A menu item's `icon` is a name resolved by `<u-icon>` from `@iyulab/components`:
|
package/dist/App.js
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import { ScreenObserver as e } from "./internals/ScreenObserver.js";
|
|
2
|
-
import t from "
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
2
|
+
import { defaultFallback as t } from "./internals/default-fallback.js";
|
|
3
|
+
import n from "i18next";
|
|
4
|
+
import { Router as r } from "@iyulab/router";
|
|
5
|
+
import { setDefaultBaseUrl as i } from "@iyulab/components/dist/utilities/icons.js";
|
|
6
|
+
import { Theme as a } from "@iyulab/components/dist/utilities/Theme.js";
|
|
7
|
+
import { Toast as o } from "@iyulab/components/dist/utilities/Toast.js";
|
|
8
|
+
import { Locale as s } from "@iyulab/components/dist/utilities/Locale.js";
|
|
8
9
|
//#region src/App.ts
|
|
9
|
-
var
|
|
10
|
-
function
|
|
11
|
-
if (!
|
|
10
|
+
var c = "\n:where(body) { margin: 0; }\n@media screen {\n :where(body) { width: 100vw; height: 100vh; }\n}\n", l;
|
|
11
|
+
function u() {
|
|
12
|
+
if (!l) {
|
|
12
13
|
if ("adoptedStyleSheets" in document && typeof CSSStyleSheet < "u" && typeof CSSStyleSheet.prototype.replaceSync == "function") {
|
|
13
14
|
let e = new CSSStyleSheet();
|
|
14
|
-
e.replaceSync(
|
|
15
|
+
e.replaceSync(c), document.adoptedStyleSheets = [...document.adoptedStyleSheets, e], l = e;
|
|
15
16
|
} else {
|
|
16
17
|
let e = document.createElement("style");
|
|
17
|
-
e.textContent =
|
|
18
|
+
e.textContent = c, document.head.prepend(e), l = e;
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
|
-
var
|
|
22
|
+
var d = class c {
|
|
22
23
|
constructor() {}
|
|
23
24
|
static get instance() {
|
|
24
|
-
return
|
|
25
|
+
return c._instance ||= new c(), c._instance;
|
|
25
26
|
}
|
|
26
27
|
get config() {
|
|
27
28
|
return this._config;
|
|
@@ -36,22 +37,22 @@ var u = class s {
|
|
|
36
37
|
return this._user;
|
|
37
38
|
}
|
|
38
39
|
get theme() {
|
|
39
|
-
return
|
|
40
|
+
return a;
|
|
40
41
|
}
|
|
41
42
|
get i18n() {
|
|
42
|
-
return
|
|
43
|
+
return n;
|
|
43
44
|
}
|
|
44
|
-
async load(
|
|
45
|
-
if (this.unload(), this._config =
|
|
46
|
-
for (let e of
|
|
47
|
-
await
|
|
45
|
+
async load(o) {
|
|
46
|
+
if (this.unload(), this._config = o, await a.init(o.theme), o.iconBasepath && i(o.iconBasepath), o.i18n) {
|
|
47
|
+
for (let e of o.i18n.plugins || []) n.use(e);
|
|
48
|
+
await n.init(o.i18n), this.syncLanguage();
|
|
48
49
|
}
|
|
49
|
-
if (
|
|
50
|
-
let e =
|
|
51
|
-
this.load(
|
|
50
|
+
if (o.auth) {
|
|
51
|
+
let e = o.root || document.body, t = () => {
|
|
52
|
+
this.load(o);
|
|
52
53
|
}, n;
|
|
53
54
|
try {
|
|
54
|
-
n = await
|
|
55
|
+
n = await o.auth.me();
|
|
55
56
|
} catch (e) {
|
|
56
57
|
n = {
|
|
57
58
|
status: "unknown",
|
|
@@ -59,8 +60,8 @@ var u = class s {
|
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
62
|
if (n?.status === "unknown") {
|
|
62
|
-
if (!
|
|
63
|
-
let r =
|
|
63
|
+
if (!o.auth.renderUnavailable) throw n.error;
|
|
64
|
+
let r = o.auth.renderUnavailable({
|
|
64
65
|
root: e,
|
|
65
66
|
error: n.error,
|
|
66
67
|
retry: t
|
|
@@ -69,7 +70,7 @@ var u = class s {
|
|
|
69
70
|
return;
|
|
70
71
|
}
|
|
71
72
|
if (n?.status === "anonymous") {
|
|
72
|
-
let n =
|
|
73
|
+
let n = o.auth.renderLogin({
|
|
73
74
|
root: e,
|
|
74
75
|
onSuccess: t
|
|
75
76
|
});
|
|
@@ -77,32 +78,32 @@ var u = class s {
|
|
|
77
78
|
return;
|
|
78
79
|
}
|
|
79
80
|
if (n?.status !== "authenticated") throw TypeError("auth.me() must return { status: 'authenticated', user } | { status: 'anonymous' } | { status: 'unknown', error }");
|
|
80
|
-
this._user = n.user, await
|
|
81
|
+
this._user = n.user, await o.auth.onAuthenticated?.(n.user);
|
|
81
82
|
}
|
|
82
|
-
let
|
|
83
|
-
this._layout = await this.createLayout(
|
|
84
|
-
element:
|
|
85
|
-
breakpoints:
|
|
83
|
+
let s = o.root || document.body;
|
|
84
|
+
this._layout = await this.createLayout(s, o.layout), this._screen = new e({
|
|
85
|
+
element: s,
|
|
86
|
+
breakpoints: o.layout.breakpoints || [768, 1024]
|
|
86
87
|
});
|
|
87
|
-
let
|
|
88
|
-
this._layout.appendChild(
|
|
88
|
+
let c = document.createElement("u-outlet");
|
|
89
|
+
this._layout.appendChild(c), this._router = new r({
|
|
89
90
|
root: this._layout,
|
|
90
|
-
basepath:
|
|
91
|
-
mode:
|
|
92
|
-
routes:
|
|
93
|
-
fallback:
|
|
94
|
-
enter:
|
|
95
|
-
initialLoad:
|
|
96
|
-
useIntercept:
|
|
91
|
+
basepath: o.basepath,
|
|
92
|
+
mode: o.routerMode,
|
|
93
|
+
routes: o.routes,
|
|
94
|
+
fallback: o.fallback ?? t,
|
|
95
|
+
enter: o.enter,
|
|
96
|
+
initialLoad: o.initialLoad,
|
|
97
|
+
useIntercept: o.useIntercept
|
|
97
98
|
});
|
|
98
99
|
}
|
|
99
100
|
syncLanguage() {
|
|
100
101
|
let e = (e) => {
|
|
101
|
-
e && e !== "cimode" && (
|
|
102
|
+
e && e !== "cimode" && (s.set(e), typeof document < "u" && (document.documentElement.lang = e));
|
|
102
103
|
};
|
|
103
|
-
e(
|
|
104
|
-
let
|
|
105
|
-
|
|
104
|
+
e(n.resolvedLanguage ?? n.language);
|
|
105
|
+
let t = (t) => e(n.resolvedLanguage ?? t);
|
|
106
|
+
n.on("languageChanged", t), this._languageSync = () => n.off("languageChanged", t);
|
|
106
107
|
}
|
|
107
108
|
unload() {
|
|
108
109
|
this._languageSync?.(), this._languageSync = void 0, this._gateTeardown &&= (this._gateTeardown(), void 0), this._user = void 0, this._screen &&= (this._screen.destroy(), void 0), this._layout &&= (this._layout.remove(), void 0), this._router &&= (this._router.destroy(), void 0), this._config &&= void 0;
|
|
@@ -117,22 +118,22 @@ var u = class s {
|
|
|
117
118
|
t === void 0 ? delete r[e] : r[e] = t, n.counts = r;
|
|
118
119
|
}
|
|
119
120
|
async notice(e, t) {
|
|
120
|
-
await
|
|
121
|
+
await o.notice(e, { ...t });
|
|
121
122
|
}
|
|
122
123
|
async info(e, t) {
|
|
123
|
-
await
|
|
124
|
+
await o.info(e, { ...t });
|
|
124
125
|
}
|
|
125
126
|
async warning(e, t) {
|
|
126
|
-
await
|
|
127
|
+
await o.warning(e, { ...t });
|
|
127
128
|
}
|
|
128
129
|
async success(e, t) {
|
|
129
|
-
await
|
|
130
|
+
await o.success(e, { ...t });
|
|
130
131
|
}
|
|
131
132
|
async error(e, t) {
|
|
132
|
-
await
|
|
133
|
+
await o.error(e, { ...t });
|
|
133
134
|
}
|
|
134
135
|
async createLayout(e, t) {
|
|
135
|
-
e === document.body &&
|
|
136
|
+
e === document.body && u();
|
|
136
137
|
let n;
|
|
137
138
|
if (t.type === "sidebar") {
|
|
138
139
|
let { SidebarLayout: e } = await import("./layouts/SidebarLayout.js"), r = new e();
|
|
@@ -142,4 +143,4 @@ var u = class s {
|
|
|
142
143
|
}
|
|
143
144
|
}.instance;
|
|
144
145
|
//#endregion
|
|
145
|
-
export {
|
|
146
|
+
export { d as app };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { StyledElement } from '../internals/StyledElement.js';
|
|
2
2
|
type ElementParts = 'host' | 'icon' | 'message' | 'title' | 'description' | 'actions';
|
|
3
3
|
/** 빈 상태가 전하는 사실 — 다음 행동이 갈린다(`EmptyState.variant` 참조). */
|
|
4
|
-
export type EmptyStateVariant = 'no-data' | 'no-results' | 'error' | 'no-access';
|
|
4
|
+
export type EmptyStateVariant = 'no-data' | 'no-results' | 'error' | 'no-access' | 'not-found';
|
|
5
5
|
/**
|
|
6
6
|
* 빈 상태 — 목록·검색 결과가 비었을 때.
|
|
7
7
|
*
|
|
@@ -24,8 +24,14 @@ export type EmptyStateVariant = 'no-data' | 'no-results' | 'error' | 'no-access'
|
|
|
24
24
|
*
|
|
25
25
|
* <!-- 라우트 가드가 막았다(403) — 장애가 아니라 권한의 사실이다. -->
|
|
26
26
|
* <u-empty-state variant="no-access"></u-empty-state>
|
|
27
|
+
*
|
|
28
|
+
* <!-- 주소가 아무것도 가리키지 않는다(404) — 다음 행동은 주소 확인 · 돌아가기. -->
|
|
29
|
+
* <u-empty-state variant="not-found"></u-empty-state>
|
|
27
30
|
* ```
|
|
28
31
|
*
|
|
32
|
+
* `app.load()` 에 `fallback` 을 주지 않으면 라우팅 실패가 이 요소로 그려진다 — 403 `no-access` · 404 `not-found` ·
|
|
33
|
+
* 그 밖 `error`(오류 메시지가 설명).
|
|
34
|
+
*
|
|
29
35
|
* ⚠**기본 문구는 영어다** — 이 패키지는 범용 층이라 특정 언어를 기본값으로 가질 수 없다.
|
|
30
36
|
* 한국어 표는 내장돼 있고(언어는 `Locale.set()`), 다른 언어는 `modernAppLocale.register(lang, …)` 로 소비자가 등록한다. 화면별로 덮으려면
|
|
31
37
|
* `title`·`description` 을 준다.
|
|
@@ -38,9 +44,10 @@ export declare class EmptyState extends StyledElement<ElementParts> {
|
|
|
38
44
|
static styles: import('lit').CSSResultGroup[];
|
|
39
45
|
/**
|
|
40
46
|
* `no-data` = 아직 없음 / `no-results` = 조건에 맞는 것이 없음 / `error` = 불러오지 못함 /
|
|
41
|
-
* `no-access` = 있지만 이 사용자에게 보여 줄 수 없음(라우트 가드의 거부 · 403)
|
|
42
|
-
*
|
|
43
|
-
*
|
|
47
|
+
* `no-access` = 있지만 이 사용자에게 보여 줄 수 없음(라우트 가드의 거부 · 403) /
|
|
48
|
+
* `not-found` = 주소가 가리키는 화면이 없음(404).
|
|
49
|
+
* 다섯은 다음 행동이 다르다(만들기 · 조건 바꾸기 · 다시 시도 · 권한 요청 · 주소 확인) — `error` 의 제목은 보조기기에 알린다(`role="alert"`).
|
|
50
|
+
* `no-access`·`not-found` 는 알리지 않는다 — 장애가 아니라 화면의 내용이고, 라우트 완료 때 본문으로 가는 포커스가 그것을 읽게 한다.
|
|
44
51
|
*/
|
|
45
52
|
variant: EmptyStateVariant;
|
|
46
53
|
/** 제목. 비우면 variant 기본 문구. */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
2
|
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { slotHasContent as r } from "../internals/slotted.js";
|
|
5
|
+
import { getLocaleStrings as i } from "../internals/locale.js";
|
|
6
6
|
import { styles as a } from "./EmptyState.styles.js";
|
|
7
7
|
import { html as o, nothing as s } from "lit";
|
|
8
8
|
import { customElement as c, property as l, state as u } from "lit/decorators.js";
|
|
@@ -15,7 +15,7 @@ var d = class extends n {
|
|
|
15
15
|
this.styles = [super.styles, a];
|
|
16
16
|
}
|
|
17
17
|
get defaults() {
|
|
18
|
-
let e =
|
|
18
|
+
let e = i(this.locale || void 0);
|
|
19
19
|
switch (this.variant) {
|
|
20
20
|
case "error": return {
|
|
21
21
|
icon: "⚠️",
|
|
@@ -27,6 +27,11 @@ var d = class extends n {
|
|
|
27
27
|
title: e.noAccessTitle,
|
|
28
28
|
description: e.noAccessDescription
|
|
29
29
|
};
|
|
30
|
+
case "not-found": return {
|
|
31
|
+
icon: "🧭",
|
|
32
|
+
title: e.notFoundTitle,
|
|
33
|
+
description: e.notFoundDescription
|
|
34
|
+
};
|
|
30
35
|
case "no-results": return {
|
|
31
36
|
icon: "🔍",
|
|
32
37
|
title: e.noResultsTitle,
|
|
@@ -53,7 +58,7 @@ var d = class extends n {
|
|
|
53
58
|
</div>
|
|
54
59
|
<div class="actions ${this.hasActions ? "" : "empty"}" part="actions">
|
|
55
60
|
<slot name="actions"
|
|
56
|
-
@slotchange=${(e) => this.hasActions =
|
|
61
|
+
@slotchange=${(e) => this.hasActions = r(e.target)}
|
|
57
62
|
></slot>
|
|
58
63
|
</div>
|
|
59
64
|
`;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
2
|
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { slotHasContent as r } from "../internals/slotted.js";
|
|
5
|
+
import { getLocaleStrings as i } from "../internals/locale.js";
|
|
6
6
|
import { styles as a } from "./MasterDetailLayout.styles.js";
|
|
7
7
|
import { html as o } from "lit";
|
|
8
8
|
import { customElement as s, property as c, state as l } from "lit/decorators.js";
|
|
@@ -14,7 +14,7 @@ var u = class extends n {
|
|
|
14
14
|
super(...e), this.masterSize = "22rem", this.overlayBreakpoint = 760, this.locale = "", this.hasDetail = !1, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.handleDetailClose = () => {
|
|
15
15
|
this.fire("detail-close", { cancelable: !1 });
|
|
16
16
|
}, this.handleDetailSlotChange = (e) => {
|
|
17
|
-
this.hasDetail =
|
|
17
|
+
this.hasDetail = r(e.target);
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
static {
|
|
@@ -40,7 +40,7 @@ var u = class extends n {
|
|
|
40
40
|
<div class="divider ${this.hasDetail ? "" : "empty"}" part="divider"></div>
|
|
41
41
|
<div class="detail ${this.hasDetail ? "" : "empty"}" part="detail">
|
|
42
42
|
<u-button class="detail-close" part="detail-close" appearance="plain"
|
|
43
|
-
aria-label=${
|
|
43
|
+
aria-label=${i(this.locale || void 0).detailClose}
|
|
44
44
|
@click=${this.handleDetailClose}
|
|
45
45
|
>
|
|
46
46
|
<u-icon lib="internal" name="x"></u-icon>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
2
|
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { slotHasContent as r } from "../internals/slotted.js";
|
|
5
|
+
import { getLocaleStrings as i } from "../internals/locale.js";
|
|
6
6
|
import { styles as a } from "./PageHeader.styles.js";
|
|
7
7
|
import { html as o, nothing as s } from "lit";
|
|
8
8
|
import { customElement as c, property as l, state as u } from "lit/decorators.js";
|
|
@@ -16,18 +16,18 @@ var d = class extends n {
|
|
|
16
16
|
}
|
|
17
17
|
render() {
|
|
18
18
|
return o`
|
|
19
|
-
${this.back ? o`<a class="back" part="back" href=${this.back}>${this.backLabel ||
|
|
19
|
+
${this.back ? o`<a class="back" part="back" href=${this.back}>${this.backLabel || i(this.locale || void 0).back}</a>` : s}
|
|
20
20
|
<div class="heading" part="heading">
|
|
21
21
|
<div class="eyebrow ${this.eyebrow || this.hasEyebrow ? "" : "empty"}" part="eyebrow">
|
|
22
22
|
<slot name="eyebrow"
|
|
23
|
-
@slotchange=${(e) => this.hasEyebrow =
|
|
23
|
+
@slotchange=${(e) => this.hasEyebrow = r(e.target)}
|
|
24
24
|
>${this.eyebrow ?? s}</slot>
|
|
25
25
|
</div>
|
|
26
26
|
<div class="title-row">
|
|
27
27
|
<h1 class="title" part="title">${this.title}</h1>
|
|
28
28
|
<span class="status ${this.hasStatus ? "" : "empty"}" part="status">
|
|
29
29
|
<slot name="status"
|
|
30
|
-
@slotchange=${(e) => this.hasStatus =
|
|
30
|
+
@slotchange=${(e) => this.hasStatus = r(e.target)}
|
|
31
31
|
></slot>
|
|
32
32
|
</span>
|
|
33
33
|
</div>
|
|
@@ -35,7 +35,7 @@ var d = class extends n {
|
|
|
35
35
|
</div>
|
|
36
36
|
<div class="actions ${this.hasActions ? "" : "empty"}" part="actions">
|
|
37
37
|
<slot name="actions"
|
|
38
|
-
@slotchange=${(e) => this.hasActions =
|
|
38
|
+
@slotchange=${(e) => this.hasActions = r(e.target)}
|
|
39
39
|
></slot>
|
|
40
40
|
</div>
|
|
41
41
|
`;
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { ScreenObserver as e } from "./internals/ScreenObserver.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
2
|
+
import { getDefaultLocale as t, getLocaleStrings as n, modernAppLocale as r, registerLocale as i, setDefaultLocale as a } from "./internals/locale.js";
|
|
3
|
+
import { EmptyState as o } from "./components/EmptyState.js";
|
|
4
|
+
import { app as s } from "./App.js";
|
|
5
|
+
import { filterSidebarItems as c } from "./layouts/filterSidebarItems.js";
|
|
6
|
+
import { PageHeader as l } from "./components/PageHeader.js";
|
|
7
|
+
import { GroupBox as u } from "./components/GroupBox.js";
|
|
8
|
+
import { InfoSection as d } from "./components/InfoSection.js";
|
|
9
|
+
import { InfoField as f, isBlank as p } from "./components/InfoField.js";
|
|
10
10
|
import { TimelineItem as m } from "./components/TimelineItem.js";
|
|
11
11
|
import { Timeline as h } from "./components/Timeline.js";
|
|
12
12
|
import { ActionBar as g } from "./components/ActionBar.js";
|
|
@@ -15,6 +15,6 @@ import { Wizard as v } from "./components/Wizard.js";
|
|
|
15
15
|
import { translate as y } from "./translate.js";
|
|
16
16
|
import { AccessDeniedError as b, ContentLoadError as x, ContentRenderError as S, NotFoundError as C, OutletMissingError as w, RouteError as T } from "@iyulab/router";
|
|
17
17
|
//#region src/index.ts
|
|
18
|
-
var E =
|
|
18
|
+
var E = s;
|
|
19
19
|
//#endregion
|
|
20
|
-
export { b as AccessDeniedError, g as ActionBar, x as ContentLoadError, S as ContentRenderError,
|
|
20
|
+
export { b as AccessDeniedError, g as ActionBar, x as ContentLoadError, S as ContentRenderError, o as EmptyState, u as GroupBox, f as InfoField, d as InfoSection, _ as MasterDetailLayout, C as NotFoundError, w as OutletMissingError, l as PageHeader, T as RouteError, e as ScreenObserver, h as Timeline, m as TimelineItem, v as Wizard, s as app, E as default, c as filterSidebarItems, t as getDefaultLocale, n as getLocaleStrings, p as isBlank, r as modernAppLocale, i as registerLocale, a as setDefaultLocale, y as translate };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FallbackRouteConfig, RouteError } from '@iyulab/router';
|
|
2
|
+
import { EmptyStateVariant } from '../components/EmptyState.js';
|
|
3
|
+
/**
|
|
4
|
+
* `app.load()` 에 `fallback` 을 주지 않은 앱의 라우팅 실패 화면 — 앱 프레임워크의 빈 상태 어휘로 그린다.
|
|
5
|
+
*
|
|
6
|
+
* 종전에는 router 의 `UErrorPage`(큰 상태 코드 화면)가 그려졌다. 셸·빈 상태 어휘를 가진 프레임워크에서 오류만
|
|
7
|
+
* 남의 외형이었고, 권한 거부(403)도 «장애» 처럼 보였다.
|
|
8
|
+
*
|
|
9
|
+
* - 403 → `no-access`(라우트 가드의 거부) · 404 → `not-found` · 그 밖 → `error`(오류 메시지가 설명).
|
|
10
|
+
* - 판정은 클래스가 아니라 **코드**로 한다 — `AccessDeniedError`·`NotFoundError` 가 403·404 를 싣고, 라우트가 던진
|
|
11
|
+
* `{ status: 404 }` 류도 router 가 같은 코드의 `RouteError` 로 감싼다. 진단 코드(`OUTLET_MISSING` 등)는 화면에
|
|
12
|
+
* 코드로 내지 않고 메시지를 설명으로 쓴다.
|
|
13
|
+
* - 소비자가 `fallback` 을 주면 그것이 그대로 이긴다(이 모듈은 쓰이지 않는다).
|
|
14
|
+
*/
|
|
15
|
+
export declare function variantOf(error: Pick<RouteError, 'code'>): EmptyStateVariant;
|
|
16
|
+
export declare const defaultFallback: FallbackRouteConfig;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getLocaleStrings as e } from "./locale.js";
|
|
2
|
+
import { EmptyState as t } from "../components/EmptyState.js";
|
|
3
|
+
//#region src/internals/default-fallback.ts
|
|
4
|
+
function n(e) {
|
|
5
|
+
switch (String(e.code)) {
|
|
6
|
+
case "403": return "no-access";
|
|
7
|
+
case "404": return "not-found";
|
|
8
|
+
default: return "error";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
var r = { render: (r) => {
|
|
12
|
+
let i = new t();
|
|
13
|
+
return i.variant = n(r.error), i.variant === "error" && (i.title = e().routeErrorTitle, i.description = r.error.message), i;
|
|
14
|
+
} };
|
|
15
|
+
//#endregion
|
|
16
|
+
export { r as defaultFallback, n as variantOf };
|
|
@@ -13,6 +13,12 @@ export interface ModernAppLocaleStrings {
|
|
|
13
13
|
/** Empty state — the screen exists but this user may not see it (`variant="no-access"` — a route guard said no). */
|
|
14
14
|
noAccessTitle: string;
|
|
15
15
|
noAccessDescription: string;
|
|
16
|
+
/** Empty state — the address points at nothing (`variant="not-found"` — the router found no route, a 404). */
|
|
17
|
+
notFoundTitle: string;
|
|
18
|
+
notFoundDescription: string;
|
|
19
|
+
/** Default route fallback — the title when a route failed for another reason (load or render failure). The
|
|
20
|
+
* `error` variant's own default title speaks of a list. */
|
|
21
|
+
routeErrorTitle: string;
|
|
16
22
|
/** Accessible label for an overlay-mode close button (master-detail layout, sidebar layout overlay). */
|
|
17
23
|
detailClose: string;
|
|
18
24
|
/** Sidebar layout — accessible label for the mobile-header menu toggle button. */
|
package/dist/internals/locale.js
CHANGED
|
@@ -10,6 +10,9 @@ var t = {
|
|
|
10
10
|
errorDescription: "Something went wrong while loading. Try again.",
|
|
11
11
|
noAccessTitle: "You don’t have access",
|
|
12
12
|
noAccessDescription: "Ask an administrator for access if you need it.",
|
|
13
|
+
notFoundTitle: "Page not found",
|
|
14
|
+
notFoundDescription: "Check the address, or go back to the previous screen.",
|
|
15
|
+
routeErrorTitle: "Couldn’t open this page",
|
|
13
16
|
detailClose: "Close",
|
|
14
17
|
toggleMobileMenu: "Toggle menu",
|
|
15
18
|
toggleSidebar: "Toggle sidebar",
|
|
@@ -30,6 +33,9 @@ n.register("en", t), n.register("ko", {
|
|
|
30
33
|
errorDescription: "불러오는 중 문제가 생겼습니다. 다시 시도해 주세요.",
|
|
31
34
|
noAccessTitle: "접근 권한이 없습니다",
|
|
32
35
|
noAccessDescription: "필요하면 관리자에게 권한을 요청하세요.",
|
|
36
|
+
notFoundTitle: "페이지를 찾을 수 없습니다",
|
|
37
|
+
notFoundDescription: "주소를 확인하거나 이전 화면으로 돌아가 주세요.",
|
|
38
|
+
routeErrorTitle: "화면을 열지 못했습니다",
|
|
33
39
|
detailClose: "닫기",
|
|
34
40
|
toggleMobileMenu: "메뉴 열고 닫기",
|
|
35
41
|
toggleSidebar: "사이드바 열고 닫기",
|
|
@@ -68,6 +74,9 @@ function l(e) {
|
|
|
68
74
|
errorDescription: t("errorDescription"),
|
|
69
75
|
noAccessTitle: t("noAccessTitle"),
|
|
70
76
|
noAccessDescription: t("noAccessDescription"),
|
|
77
|
+
notFoundTitle: t("notFoundTitle"),
|
|
78
|
+
notFoundDescription: t("notFoundDescription"),
|
|
79
|
+
routeErrorTitle: t("routeErrorTitle"),
|
|
71
80
|
detailClose: t("detailClose"),
|
|
72
81
|
toggleMobileMenu: t("toggleMobileMenu"),
|
|
73
82
|
toggleSidebar: t("toggleSidebar"),
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import n from "../
|
|
4
|
-
import r from "../
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
|
+
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
|
+
import { slotHasContent as r } from "../internals/slotted.js";
|
|
5
|
+
import { getLocaleStrings as i } from "../internals/locale.js";
|
|
6
|
+
import { app as a } from "../App.js";
|
|
7
|
+
import { filterSidebarItems as o } from "./filterSidebarItems.js";
|
|
8
8
|
import "../components/SidebarSection.js";
|
|
9
9
|
import { DEFAULT_NAV_ICON as s } from "../internals/nav-icon.js";
|
|
10
10
|
import "../components/SidebarLink.js";
|
|
@@ -74,14 +74,14 @@ function j(e, t) {
|
|
|
74
74
|
}
|
|
75
75
|
return !1;
|
|
76
76
|
}
|
|
77
|
-
var M = class extends
|
|
77
|
+
var M = class extends n {
|
|
78
78
|
static {
|
|
79
79
|
S = this;
|
|
80
80
|
}
|
|
81
|
-
constructor(...
|
|
82
|
-
super(...
|
|
83
|
-
let
|
|
84
|
-
|
|
81
|
+
constructor(...e) {
|
|
82
|
+
super(...e), this.state = "default", this.counts = {}, this.locale = "", this.hasOverlay = !1, this.openMenu = null, this.menuIds = /* @__PURE__ */ new WeakMap(), this.hasNotice = !1, this.routeLoading = !1, this.mainTabStop = !1, this.overlayTrigger = null, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.currentPath = null, this.unsizedWarnKey = `unsized:${++w}`, this.isMatchedLink = (e) => this.currentPath === null || !e ? !1 : (e = typeof e == "string" ? new URLPattern(e, window.location.origin) : e, e.test(this.currentPath, window.location.origin)), this.handleToggleButtonClick = () => {
|
|
83
|
+
let e = this.screenSize ?? a.screen ?? "large";
|
|
84
|
+
e === "large" ? this.state = this.state === "default" ? "slim" : "default" : e === "medium" ? this.state = this.state === "slim" ? "modal" : "slim" : e === "small" ? this.state = this.state === "mobile" ? "mobile-open" : "mobile" : console.warn("Unknown screen size:", e);
|
|
85
85
|
}, this.handleBackdropClick = () => {
|
|
86
86
|
this.state = "slim";
|
|
87
87
|
}, this.handleRouteBegin = (e) => {
|
|
@@ -110,14 +110,14 @@ var M = class extends i {
|
|
|
110
110
|
this.routeLoading = !1, this.progressBarEl.status = "default";
|
|
111
111
|
}, 300);
|
|
112
112
|
}, this.handleOverlaySlotChange = (e) => {
|
|
113
|
-
let t =
|
|
113
|
+
let t = r(e.target);
|
|
114
114
|
if (t && !this.hasOverlay) {
|
|
115
115
|
let e = A();
|
|
116
116
|
this.overlayTrigger = e && e !== document.body ? e : null;
|
|
117
117
|
}
|
|
118
118
|
this.hasOverlay = t;
|
|
119
119
|
}, this.handleNoticeSlotChange = (e) => {
|
|
120
|
-
this.hasNotice =
|
|
120
|
+
this.hasNotice = r(e.target), this.handleMainSlotChange();
|
|
121
121
|
}, this.handleOverlayEscape = (e) => {
|
|
122
122
|
let t = this.shadowRoot?.querySelector(".overlay");
|
|
123
123
|
return !this.hasOverlay || !t || !e.composedPath().includes(t) ? !1 : (this.handleOverlayClose(), !0);
|
|
@@ -193,7 +193,7 @@ var M = class extends i {
|
|
|
193
193
|
return this.config ? u`
|
|
194
194
|
<!-- 반복 영역 건너뛰기(WCAG 2.4.1) — 셸의 첫 Tab. 포커스될 때만 보인다. -->
|
|
195
195
|
<a class="skip-link" part="skip-link" href="#" @click=${this.handleSkipLinkClick}>
|
|
196
|
-
${
|
|
196
|
+
${i(this.locale || void 0).skipToContent}
|
|
197
197
|
</a>
|
|
198
198
|
|
|
199
199
|
<!-- Mobile Header -->
|
|
@@ -203,7 +203,7 @@ var M = class extends i {
|
|
|
203
203
|
${this.config.title}
|
|
204
204
|
</span>
|
|
205
205
|
<u-button class="toggler" appearance="plain"
|
|
206
|
-
aria-label=${
|
|
206
|
+
aria-label=${i(this.locale || void 0).toggleMobileMenu}
|
|
207
207
|
@click=${this.handleToggleButtonClick}>
|
|
208
208
|
<u-icon
|
|
209
209
|
lib=${this.icon("lib")}
|
|
@@ -221,7 +221,7 @@ var M = class extends i {
|
|
|
221
221
|
${this.config.title}
|
|
222
222
|
</span>
|
|
223
223
|
<u-button class="toggler" appearance="plain"
|
|
224
|
-
aria-label=${
|
|
224
|
+
aria-label=${i(this.locale || void 0).toggleSidebar}
|
|
225
225
|
@click=${this.handleToggleButtonClick}>
|
|
226
226
|
<u-icon
|
|
227
227
|
lib=${this.icon("lib")}
|
|
@@ -233,12 +233,12 @@ var M = class extends i {
|
|
|
233
233
|
<!-- Sidebar Navigation Menu -->
|
|
234
234
|
<nav class="sidebar-main" part="sidebar-main" scrollable
|
|
235
235
|
aria-label=${this.config.mainAriaLabel ?? d}>
|
|
236
|
-
${_(
|
|
236
|
+
${_(o(this.config.main ?? [], this.config.hasPermission), (e, t) => t, (e) => this.renderItem(e))}
|
|
237
237
|
</nav>
|
|
238
238
|
|
|
239
239
|
<!-- Sidebar Footer -->
|
|
240
240
|
<div class="sidebar-footer" part="sidebar-footer">
|
|
241
|
-
${_(
|
|
241
|
+
${_(o(this.config.footer ?? [], this.config.hasPermission), (e, t) => t, (e) => this.renderItem(e))}
|
|
242
242
|
</div>
|
|
243
243
|
</aside>
|
|
244
244
|
|
|
@@ -247,7 +247,7 @@ var M = class extends i {
|
|
|
247
247
|
<main class="main" part="main" scrollable tabindex=${this.mainTabStop ? "0" : "-1"} @keydown=${this._handleMainKeydown}>
|
|
248
248
|
<u-progress-bar part="progress" class=${this.routeLoading ? "loading" : ""}
|
|
249
249
|
aria-hidden=${this.routeLoading ? d : "true"}
|
|
250
|
-
aria-label=${
|
|
250
|
+
aria-label=${i(this.locale || void 0).pageLoading}></u-progress-bar>
|
|
251
251
|
|
|
252
252
|
<div class="main-content" part="main-content" ?inert=${this.hasOverlay}>
|
|
253
253
|
<div class="notices ${this.hasNotice ? "" : "empty"}" part="notices"
|
|
@@ -261,7 +261,7 @@ var M = class extends i {
|
|
|
261
261
|
|
|
262
262
|
<div class="overlay ${this.hasOverlay ? "" : "empty"}" part="overlay">
|
|
263
263
|
<u-button class="overlay-close" part="overlay-close" appearance="plain"
|
|
264
|
-
aria-label=${
|
|
264
|
+
aria-label=${i(this.locale || void 0).detailClose}
|
|
265
265
|
@click=${this.handleOverlayClose}>
|
|
266
266
|
<u-icon lib=${this.icon("lib")} name=${this.icon("overlayClose")}></u-icon>
|
|
267
267
|
</u-button>
|
|
@@ -470,9 +470,9 @@ var M = class extends i {
|
|
|
470
470
|
t && t !== document.body || (e?.isConnected ? e.focus() : this.mainElement?.focus({ preventScroll: !0 }));
|
|
471
471
|
}
|
|
472
472
|
};
|
|
473
|
-
|
|
473
|
+
t([p({
|
|
474
474
|
type: String,
|
|
475
475
|
reflect: !0
|
|
476
|
-
}),
|
|
476
|
+
}), e("design:type", Object)], M.prototype, "state", void 0), t([p({ type: Object }), e("design:type", Object)], M.prototype, "config", void 0), t([p({ type: Object }), e("design:type", typeof Record > "u" ? Object : Record)], M.prototype, "counts", void 0), t([p({ type: String }), e("design:type", Object)], M.prototype, "locale", void 0), t([m("u-progress-bar"), e("design:type", Object)], M.prototype, "progressBarEl", void 0), t([h(), e("design:type", Object)], M.prototype, "hasOverlay", void 0), t([h(), e("design:type", Object)], M.prototype, "openMenu", void 0), t([h(), e("design:type", Object)], M.prototype, "hasNotice", void 0), t([h(), e("design:type", Object)], M.prototype, "routeLoading", void 0), t([h(), e("design:type", Object)], M.prototype, "mainTabStop", void 0), t([h(), e("design:type", Object)], M.prototype, "currentPath", void 0), M = S = t([f("u-sidebar-layout")], M);
|
|
477
477
|
//#endregion
|
|
478
478
|
export { M as SidebarLayout };
|
|
@@ -51,6 +51,8 @@ export interface AppConfig {
|
|
|
51
51
|
routes?: RouteConfig[];
|
|
52
52
|
/**
|
|
53
53
|
* 라우팅 실패 시 대체 컨텐츠 설정
|
|
54
|
+
* - 주지 않으면 `<u-empty-state>` 로 그린다 — 403(가드 거부) `no-access` · 404 `not-found` ·
|
|
55
|
+
* 그 밖 `error`(오류 메시지가 설명). 주면 이것이 그대로 쓰인다.
|
|
54
56
|
*/
|
|
55
57
|
fallback?: FallbackRouteConfig;
|
|
56
58
|
/**
|
package/package.json
CHANGED
|
@@ -196,6 +196,9 @@ code 403 · `NotFoundError` · `ContentLoadError` · `ContentRenderError` · `Ou
|
|
|
196
196
|
exported from `@iyulab/modern-app` — tell a blocked screen from a missing one with
|
|
197
197
|
`ctx.error instanceof AccessDeniedError` and render it as `<u-empty-state variant="no-access">`.
|
|
198
198
|
|
|
199
|
+
Without a `fallback`, `app.load()` draws `<u-empty-state>` itself: 403 → `no-access` · 404 → `not-found` ·
|
|
200
|
+
anything else → `error` (title "Couldn’t open this page", the error message as description).
|
|
201
|
+
|
|
199
202
|
---
|
|
200
203
|
|
|
201
204
|
## `I18nInitOptions`
|
|
@@ -25,6 +25,9 @@ action is **asking for access**.
|
|
|
25
25
|
|
|
26
26
|
<!-- A route guard said no (403) — not an outage, so it is not announced as an alert. -->
|
|
27
27
|
<u-empty-state variant="no-access"></u-empty-state>
|
|
28
|
+
|
|
29
|
+
<!-- The address points at nothing (404) — the next step is checking the address or going back. -->
|
|
30
|
+
<u-empty-state variant="not-found"></u-empty-state>
|
|
28
31
|
```
|
|
29
32
|
|
|
30
33
|
## Slots
|
|
@@ -38,7 +41,7 @@ action is **asking for access**.
|
|
|
38
41
|
|
|
39
42
|
| Property | Type | Default | Reflect | Description |
|
|
40
43
|
|----------|------|---------|---------|-------------|
|
|
41
|
-
| `variant` | `'no-data'\|'no-results'\|'error'\|'no-access'` | `'no-data'` | ✓ | Which fact is being shown; changes the default wording. `error` (could not load) announces its title; `no-access` (this user may not see it)
|
|
44
|
+
| `variant` | `'no-data'\|'no-results'\|'error'\|'no-access'\|'not-found'` | `'no-data'` | ✓ | Which fact is being shown; changes the default wording. `error` (could not load) announces its title; `no-access` (this user may not see it) and `not-found` (nothing at this address) do not |
|
|
42
45
|
| `title` | `string` | `''` | | Override the default title |
|
|
43
46
|
| `description` | `string` | `''` | | Override the default description |
|
|
44
47
|
| `locale` | `string` | `''` | | Locale tag override for built-in strings |
|
|
@@ -69,18 +72,40 @@ action is **asking for access**.
|
|
|
69
72
|
`@iyulab/components`, register others with `modernAppLocale.register(lang, …)`, or pass
|
|
70
73
|
`title`/`description` per screen.
|
|
71
74
|
|
|
72
|
-
## Blocked routes
|
|
75
|
+
## Blocked and missing routes
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
**An app that gives `app.load()` no `fallback` gets this element by default**: a route `enter` guard
|
|
78
|
+
that returns `false` (`AccessDeniedError`, code 403) shows `no-access`, an address with no route
|
|
79
|
+
(`NotFoundError`, 404) shows `not-found`, and any other failure (the route could not be loaded or
|
|
80
|
+
rendered) shows `error` with the page-level title "Couldn’t open this page" and the error message as
|
|
81
|
+
its description. The screen is drawn inside the shell, so the sidebar stays.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { app } from '@iyulab/modern-app';
|
|
85
|
+
import { html } from 'lit';
|
|
86
|
+
|
|
87
|
+
const canAudit = () => false; // your permission check
|
|
88
|
+
|
|
89
|
+
await app.load({
|
|
90
|
+
layout: { type: 'sidebar' },
|
|
91
|
+
routes: [{ path: '/audit', enter: canAudit, render: () => html`<audit-screen></audit-screen>` }],
|
|
92
|
+
// no fallback — /audit shows <u-empty-state variant="no-access">
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Give a `fallback` to draw something else — it replaces the default entirely. The error classes are
|
|
97
|
+
exported from `@iyulab/modern-app`, so it can tell a blocked screen from a missing one without
|
|
98
|
+
depending on the router directly:
|
|
77
99
|
|
|
78
100
|
```typescript
|
|
79
101
|
import { app, AccessDeniedError } from '@iyulab/modern-app';
|
|
80
102
|
import { html } from 'lit';
|
|
81
103
|
|
|
104
|
+
const canAudit = () => false; // your permission check
|
|
105
|
+
|
|
82
106
|
await app.load({
|
|
83
|
-
|
|
107
|
+
layout: { type: 'sidebar' },
|
|
108
|
+
routes: [{ path: '/audit', enter: canAudit, render: () => html`<audit-screen></audit-screen>` }],
|
|
84
109
|
fallback: {
|
|
85
110
|
render: (ctx) => ctx.error instanceof AccessDeniedError
|
|
86
111
|
? html`<u-empty-state variant="no-access"></u-empty-state>`
|