@iyulab/modern-app 0.20.1 → 0.21.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,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.21.0] - 2026-09-17
4
+
5
+ ### Added
6
+
7
+ - **`translate()` — a reactive translation directive**, exported from `@iyulab/modern-app`. It is
8
+ bound to the i18next instance `app.load({ i18n })` initializes and re-renders its part when the
9
+ language changes, when resources arrive later, and once i18next finishes initializing (until then
10
+ it renders an empty string rather than the key). A disconnected template stops listening and
11
+ catches up when reconnected. Sidebar item labels already accept a directive result, so
12
+ `label: translate('nav::home')` now works without a third-party package; the documentation
13
+ previously pointed to `lit-i18n` for this. Migrating is an import swap plus dropping
14
+ `initLitI18n` from `i18n.plugins`.
15
+
16
+ ### Fixed
17
+
18
+ - **A collapsed sidebar no longer leaves translated items without an accessible name.** In the
19
+ collapsed state the item label was removed with `hidden`, which also removes it from the
20
+ accessibility tree; an `aria-label` was substituted only when the label was a plain string, so an
21
+ item labelled with a directive was announced with no name. The label is now hidden visually and
22
+ stays in the accessibility tree, so it names the link, button or group whatever its type. Items
23
+ no longer carry an `aria-label` in the collapsed state.
24
+
3
25
  ## [0.20.1] - 2026-09-17
4
26
 
5
27
  ### Fixed
package/README.md CHANGED
@@ -113,8 +113,8 @@ await app.load({
113
113
  app.i18n.t('common::greeting');
114
114
  app.i18n.changeLanguage('ko');
115
115
 
116
- // Reactive translations in Lit templates
117
- import { translate } from 'lit-i18n';
116
+ // Reactive translations in Lit templates — and in shell labels
117
+ import { translate } from '@iyulab/modern-app';
118
118
  html`<p>${translate('common::greeting')}</p>`;
119
119
  ```
120
120
 
@@ -263,7 +263,7 @@ contrast likewise comes from that package's tokens.
263
263
  | [layout.md](./docs/layout.md) | Sidebar layout, all menu item types, responsive behaviour |
264
264
  | [theme.md](./docs/theme.md) | Theme init, runtime switching, token layers, shell surface tokens |
265
265
  | [notifications.md](./docs/notifications.md) | Toast methods and options |
266
- | [i18n.md](./docs/i18n.md) | i18next setup, plugins, lit-i18n usage |
266
+ | [i18n.md](./docs/i18n.md) | i18next setup, plugins, the `translate()` directive |
267
267
  | [configuration.md](./docs/configuration.md) | Full TypeScript interface reference |
268
268
 
269
269
  ## License
@@ -3,11 +3,11 @@ import t from "../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
4
  import { DEFAULT_NAV_ICON as r } from "../internals/nav-icon.js";
5
5
  import { styles as i } from "./SidebarButton.styles.js";
6
- import { html as a, nothing as o } from "lit";
7
- import { customElement as s, property as c } from "lit/decorators.js";
6
+ import { html as a } from "lit";
7
+ import { customElement as o, property as s } from "lit/decorators.js";
8
8
  import "@iyulab/components/dist/components/icon/UIcon.js";
9
9
  //#region src/components/SidebarButton.ts
10
- var l = class extends n {
10
+ var c = class extends n {
11
11
  constructor(...e) {
12
12
  super(...e), this.compact = !1;
13
13
  }
@@ -15,24 +15,23 @@ var l = class extends n {
15
15
  this.styles = [super.styles, i];
16
16
  }
17
17
  render() {
18
- let e = this.compact && typeof this.label == "string" ? this.label : o;
19
18
  return a`
20
- <button part="base" ?compact=${this.compact} aria-label=${e}>
19
+ <button part="base" ?compact=${this.compact}>
21
20
  <u-icon part="icon"
22
21
  .lib=${this.lib}
23
22
  .name=${this.icon}
24
23
  .fallback=${r}
25
24
  ></u-icon>
26
- <span part="label" ?hidden=${this.compact}>
25
+ <span part="label" ?compact=${this.compact}>
27
26
  ${this.label}
28
27
  </span>
29
28
  </button>
30
29
  `;
31
30
  }
32
31
  };
33
- t([c({
32
+ t([s({
34
33
  type: Boolean,
35
34
  reflect: !0
36
- }), e("design:type", Object)], l.prototype, "compact", void 0), t([c({ type: String }), e("design:type", String)], l.prototype, "icon", void 0), t([c({ type: String }), e("design:type", String)], l.prototype, "lib", void 0), t([c({ type: String }), e("design:type", Object)], l.prototype, "label", void 0), l = t([s("u-sidebar-button")], l);
35
+ }), e("design:type", Object)], c.prototype, "compact", void 0), t([s({ type: String }), e("design:type", String)], c.prototype, "icon", void 0), t([s({ type: String }), e("design:type", String)], c.prototype, "lib", void 0), t([s({ type: String }), e("design:type", Object)], c.prototype, "label", void 0), c = t([o("u-sidebar-button")], c);
37
36
  //#endregion
38
- export { l as SidebarButton };
37
+ export { c as SidebarButton };
@@ -55,6 +55,22 @@ var t = e`
55
55
  overflow: hidden;
56
56
  text-overflow: ellipsis;
57
57
  }
58
+
59
+ /*
60
+ * 접힌 상태의 라벨 - 시각적으로만 숨긴다. 접근성 트리에는 남아 항목의 이름이 된다
61
+ * (hidden 은 트리에서도 빼서 이름 없는 항목을 만들었다). 절대배치라 flex 배치와 gap 에 끼지 않는다.
62
+ */
63
+ [part~='label'][compact] {
64
+ position: absolute;
65
+ width: 1px;
66
+ height: 1px;
67
+ padding: 0;
68
+ margin: -1px;
69
+ overflow: hidden;
70
+ clip-path: inset(50%);
71
+ white-space: nowrap;
72
+ border: 0;
73
+ }
58
74
  `;
59
75
  //#endregion
60
76
  export { t as styles };
@@ -4,11 +4,11 @@ import { StyledElement as n } from "../internals/StyledElement.js";
4
4
  import { DEFAULT_NAV_ICON as r } from "../internals/nav-icon.js";
5
5
  import { SidebarLink as i } from "./SidebarLink.js";
6
6
  import { styles as a } from "./SidebarGroup.styles.js";
7
- import { html as o, nothing as s } from "lit";
8
- import { customElement as c, property as l } from "lit/decorators.js";
7
+ import { html as o } from "lit";
8
+ import { customElement as s, property as c } from "lit/decorators.js";
9
9
  import "@iyulab/components/dist/components/icon/UIcon.js";
10
10
  //#region src/components/SidebarGroup.ts
11
- var u = class extends n {
11
+ var l = class extends n {
12
12
  constructor(...e) {
13
13
  super(...e), this.compact = !1, this.selected = !1, this.collapsed = !0, this.handleButtonClick = (e) => {
14
14
  this.compact ? ((this.shadowRoot?.querySelector("slot")?.assignedElements({ flatten: !0 }).find((e) => e instanceof i))?.renderRoot.querySelector("u-link"))?.click() : this.collapsed = !this.collapsed;
@@ -18,12 +18,10 @@ var u = class extends n {
18
18
  this.styles = [super.styles, a];
19
19
  }
20
20
  render() {
21
- let e = this.compact && typeof this.label == "string" ? this.label : s;
22
21
  return o`
23
22
  <button part="header"
24
23
  ?compact=${this.compact}
25
24
  ?selected=${this.selected}
26
- aria-label=${e}
27
25
  @click=${this.handleButtonClick}>
28
26
  <!--
29
27
  ⚠ ?hidden 을 걸지 않는다. 접힌 상태에서는 라벨과 캐럿이 모두 숨으므로,
@@ -36,7 +34,7 @@ var u = class extends n {
36
34
  .name=${this.icon}
37
35
  .fallback=${r}
38
36
  ></u-icon>
39
- <span class="label" part="label" ?hidden=${this.compact}>
37
+ <span class="label" part="label" ?compact=${this.compact}>
40
38
  ${this.label}
41
39
  </span>
42
40
  <u-icon class="caret" part="caret" ?hidden=${this.compact}
@@ -53,15 +51,15 @@ var u = class extends n {
53
51
  `;
54
52
  }
55
53
  };
56
- t([l({
54
+ t([c({
57
55
  type: Boolean,
58
56
  reflect: !0
59
- }), e("design:type", Boolean)], u.prototype, "compact", void 0), t([l({
57
+ }), e("design:type", Boolean)], l.prototype, "compact", void 0), t([c({
60
58
  type: Boolean,
61
59
  reflect: !0
62
- }), e("design:type", Boolean)], u.prototype, "selected", void 0), t([l({
60
+ }), e("design:type", Boolean)], l.prototype, "selected", void 0), t([c({
63
61
  type: Boolean,
64
62
  reflect: !0
65
- }), e("design:type", Boolean)], u.prototype, "collapsed", void 0), t([l({ type: String }), e("design:type", String)], u.prototype, "icon", void 0), t([l({ type: String }), e("design:type", String)], u.prototype, "lib", void 0), t([l({ type: String }), e("design:type", Object)], u.prototype, "label", void 0), u = t([c("u-sidebar-group")], u);
63
+ }), e("design:type", Boolean)], l.prototype, "collapsed", void 0), t([c({ type: String }), e("design:type", String)], l.prototype, "icon", void 0), t([c({ type: String }), e("design:type", String)], l.prototype, "lib", void 0), t([c({ type: String }), e("design:type", Object)], l.prototype, "label", void 0), l = t([s("u-sidebar-group")], l);
66
64
  //#endregion
67
- export { u as SidebarGroup };
65
+ export { l as SidebarGroup };
@@ -93,6 +93,22 @@ var t = e`
93
93
  height: 0;
94
94
  opacity: 0;
95
95
  }
96
+
97
+ /*
98
+ * 접힌 상태의 라벨 - 시각적으로만 숨긴다. 접근성 트리에는 남아 항목의 이름이 된다
99
+ * (hidden 은 트리에서도 빼서 이름 없는 항목을 만들었다). 절대배치라 flex 배치와 gap 에 끼지 않는다.
100
+ */
101
+ [part~='label'][compact] {
102
+ position: absolute;
103
+ width: 1px;
104
+ height: 1px;
105
+ padding: 0;
106
+ margin: -1px;
107
+ overflow: hidden;
108
+ clip-path: inset(50%);
109
+ white-space: nowrap;
110
+ border: 0;
111
+ }
96
112
  `;
97
113
  //#endregion
98
114
  export { t as styles };
@@ -15,14 +15,12 @@ var l = class extends n {
15
15
  this.styles = [super.styles, i];
16
16
  }
17
17
  render() {
18
- let e = this.compact && typeof this.label == "string" ? this.label : o;
19
18
  return a`
20
19
  <u-link
21
20
  .href=${this.href || "#"}
22
21
  .navigate=${this.navigate}
23
22
  .target=${this.target}
24
23
  aria-current=${this.selected ? "page" : o}
25
- aria-label=${e}
26
24
  >
27
25
  <div class="container" part="base" ?compact=${this.compact}>
28
26
  <u-icon part="icon"
@@ -30,7 +28,7 @@ var l = class extends n {
30
28
  .name=${this.icon}
31
29
  .fallback=${r}
32
30
  ></u-icon>
33
- <span part="label" ?hidden=${this.compact}>
31
+ <span part="label" ?compact=${this.compact}>
34
32
  ${this.label}
35
33
  </span>
36
34
  </div>
@@ -69,6 +69,22 @@ var t = e`
69
69
  overflow: hidden;
70
70
  text-overflow: ellipsis;
71
71
  }
72
+
73
+ /*
74
+ * 접힌 상태의 라벨 - 시각적으로만 숨긴다. 접근성 트리에는 남아 항목의 이름이 된다
75
+ * (hidden 은 트리에서도 빼서 이름 없는 항목을 만들었다). 절대배치라 flex 배치와 gap 에 끼지 않는다.
76
+ */
77
+ [part~='label'][compact] {
78
+ position: absolute;
79
+ width: 1px;
80
+ height: 1px;
81
+ padding: 0;
82
+ margin: -1px;
83
+ overflow: hidden;
84
+ clip-path: inset(50%);
85
+ white-space: nowrap;
86
+ border: 0;
87
+ }
72
88
  `;
73
89
  //#endregion
74
90
  export { t as styles };
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export { ActionBar } from './components/ActionBar.js';
13
13
  export { MasterDetailLayout } from './components/MasterDetailLayout.js';
14
14
  export { Wizard } from './components/Wizard.js';
15
15
  export type { WizardStep, WizardStepState, WizardStepChangeDetail } from './components/Wizard.js';
16
+ export { translate } from './translate.js';
16
17
  export { registerLocale, setDefaultLocale, getLocaleStrings, getDefaultLocale, } from './internals/locale.js';
17
18
  export type { ModernAppLocaleStrings } from './internals/locale.js';
18
19
  export { ScreenObserver } from './internals/ScreenObserver.js';
package/dist/index.js CHANGED
@@ -10,7 +10,8 @@ import { EmptyState as f } from "./components/EmptyState.js";
10
10
  import { ActionBar as p } from "./components/ActionBar.js";
11
11
  import { MasterDetailLayout as m } from "./components/MasterDetailLayout.js";
12
12
  import { Wizard as h } from "./components/Wizard.js";
13
+ import { translate as g } from "./translate.js";
13
14
  //#region src/index.ts
14
- var g = t;
15
+ var _ = t;
15
16
  //#endregion
16
- export { p as ActionBar, f as EmptyState, c as GroupBox, u as InfoField, l as InfoSection, m as MasterDetailLayout, s as PageHeader, e as ScreenObserver, h as Wizard, t as app, g as default, n as filterSidebarItems, r as getDefaultLocale, i as getLocaleStrings, d as isBlank, a as registerLocale, o as setDefaultLocale };
17
+ export { p as ActionBar, f as EmptyState, c as GroupBox, u as InfoField, l as InfoSection, m as MasterDetailLayout, s as PageHeader, e as ScreenObserver, h as Wizard, t as app, _ as default, n as filterSidebarItems, r as getDefaultLocale, i as getLocaleStrings, d as isBlank, a as registerLocale, o as setDefaultLocale, g as translate };
@@ -0,0 +1,39 @@
1
+ import { AsyncDirective } from 'lit/async-directive.js';
2
+ import { TOptions } from 'i18next';
3
+ /**
4
+ * `app.load({ i18n })` 가 초기화하는 i18next 에 붙는 반응형 번역 디렉티브.
5
+ *
6
+ * ★**왜 이 패키지가 갖는가**: i18next 를 초기화하고(`app.load`) 노출하는(`app.i18n`) 것이 이미 이
7
+ * 패키지이고, 셸 설정의 `label` 은 `string | DirectiveResult` 로 **디렉티브를 받도록** 설계돼 있다.
8
+ * 그런데 그 디렉티브가 없어서 소비자는 서드파티를 따로 들여와야 했다 — 이미 소유한 개념의
9
+ * 프리미티브를 내놓지 않은 자리다.
10
+ *
11
+ * ★**구독은 Lit 의 연결 수명 주기로 관리한다** — `disconnected()` 에서 풀고 `reconnected()` 에서 다시
12
+ * 붙으며 그 사이의 변경을 따라잡는다. 주기적으로 끊긴 파트를 훑는 레지스트리가 필요 없다.
13
+ *
14
+ * 갱신 계기는 셋이다: 언어 변경 · 나중에 도착한 리소스(백엔드가 네임스페이스를 늦게 싣는 경우) ·
15
+ * 초기화 완료(셸 설정은 초기화 **전에** 만들어진다 — 그때는 빈 문자열을 그린다. 키를 그리면
16
+ * 번역 전 키가 화면에 잠깐 비친다).
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { translate } from '@iyulab/modern-app';
21
+ * html`<h1>${translate('common::title')}</h1>`;
22
+ * app.load({ layout: { type: 'sidebar', main: [{ type: 'link', label: translate('nav::home'), href: '/' }] } });
23
+ * ```
24
+ */
25
+ declare class TranslateDirective extends AsyncDirective {
26
+ private keys;
27
+ private options?;
28
+ private subscribed;
29
+ render(keys: string | string[], options?: TOptions): string;
30
+ protected disconnected(): void;
31
+ protected reconnected(): void;
32
+ private translate;
33
+ private readonly refresh;
34
+ private subscribe;
35
+ private unsubscribe;
36
+ }
37
+ /** 현재 언어의 번역을 그리고, 언어·리소스가 바뀌면 그 자리를 다시 그린다. */
38
+ export declare const translate: (keys: string | string[], options?: TOptions | undefined) => import('lit-html/directive.js').DirectiveResult<typeof TranslateDirective>;
39
+ export {};
@@ -0,0 +1,31 @@
1
+ import e from "i18next";
2
+ import { AsyncDirective as t, directive as n } from "lit/async-directive.js";
3
+ var r = n(class extends t {
4
+ constructor(...e) {
5
+ super(...e), this.keys = "", this.subscribed = !1, this.refresh = () => {
6
+ this.setValue(this.translate());
7
+ };
8
+ }
9
+ render(e, t) {
10
+ return this.keys = e, this.options = t, this.isConnected && this.subscribe(), this.translate();
11
+ }
12
+ disconnected() {
13
+ this.unsubscribe();
14
+ }
15
+ reconnected() {
16
+ this.subscribe(), this.refresh();
17
+ }
18
+ translate() {
19
+ if (!e.isInitialized) return "";
20
+ let t = e.t(this.keys, this.options);
21
+ return typeof t == "string" ? t : "";
22
+ }
23
+ subscribe() {
24
+ this.subscribed || (this.subscribed = !0, e.on("languageChanged", this.refresh), e.on("initialized", this.refresh), e.store?.on("added", this.refresh));
25
+ }
26
+ unsubscribe() {
27
+ this.subscribed && (this.subscribed = !1, e.off("languageChanged", this.refresh), e.off("initialized", this.refresh), e.store?.off("added", this.refresh));
28
+ }
29
+ });
30
+ //#endregion
31
+ export { r as translate };
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.20.1",
4
+ "version": "0.21.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -131,11 +131,14 @@ await app.load({
131
131
  app.i18n.t('namespace::key');
132
132
  ```
133
133
 
134
- Use in Lit templates with `lit-i18n`:
134
+ Use in Lit templates — and as a sidebar item `label` — with the `translate()` directive. It
135
+ re-renders when the language changes or resources arrive, and renders an empty string until i18next
136
+ is initialized:
135
137
 
136
138
  ```typescript
137
- import { translate } from 'lit-i18n';
139
+ import { translate } from '@iyulab/modern-app';
138
140
  html`<p>${translate('namespace::greeting')}</p>`;
141
+ // layout: { main: [{ type: 'link', label: translate('nav::home'), href: '/' }] }
139
142
  ```
140
143
 
141
144
  ---