@iyulab/modern-app 0.37.1 → 0.38.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,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.38.0] - 2026-10-06
4
+
5
+ ### Added
6
+
7
+ - **`u-info-field` `tone="warning"`** — "needs attention, not wrong" (a deadline coming up, a rate under target
8
+ but within limits). Paints the value and the trend with `--u-warning-color-strong`, like `positive`/`negative`
9
+ use the success/danger tokens. It is never inferred from `trend`, so existing fields are unchanged.
10
+ - **`auth.renderUnavailable({ root, error, retry })`** on the boot auth gate — drawn when `me()` throws, that is,
11
+ when the session could not be checked (server down, offline). The gate no longer has only "signed in" or
12
+ "show the login screen": `me()` should throw rather than return `null` when it got no answer, and `retry()`
13
+ checks again. Without `renderUnavailable`, `app.load()` rejects with the error as before.
14
+
15
+ ### Documentation
16
+
17
+ - The auth gate examples follow the three-state session lookup of `@iyulab/enterprise` 0.25
18
+ (`fetchMe()` returns `{ status }`): the README example used to pass the lookup straight through, which with
19
+ 0.25 would treat every answer as signed in.
20
+
21
+ ## [0.37.2] - 2026-10-06
22
+
23
+ ### Fixed
24
+
25
+ - **`TimelineItem` has a React wrapper** — `@iyulab/modern-app/react/TimelineItem.js` (and the barrel).
26
+ The entry lived in the same module as `u-timeline`, and the wrappers are generated one element per
27
+ module, so only `Timeline` was wrapped. `u-timeline-item` now has its own module; importing
28
+ `Timeline` still registers both.
29
+ - `u-timeline` exposes a `list` part (the column the entries stack in), and the reference page's
30
+ parts table lists what the elements actually expose.
31
+
3
32
  ## [0.37.1] - 2026-10-06
4
33
 
5
34
  ### Fixed
package/README.md CHANGED
@@ -182,8 +182,13 @@ const auth = createAuthClient<User, Cred>({ meUrl: '/api/auth/me', loginUrl: '/a
182
182
  await app.load({
183
183
  layout: { type: 'sidebar', /* ... */ },
184
184
  auth: {
185
- me: () => auth.fetchMe(), // null → 미인증 → renderLogin
185
+ me: async () => {
186
+ const s = await auth.fetchMe();
187
+ if (s.status === 'unknown') throw s.error; // 모름(서버 다운·오프라인) → renderUnavailable
188
+ return s.status === 'authenticated' ? s.user : null; // null → 미인증 → renderLogin
189
+ },
186
190
  renderLogin: ({ root, onSuccess }) => renderLoginPage(root, auth, onSuccess),
191
+ renderUnavailable: ({ root, retry }) => renderOfflinePage(root, retry),
187
192
  onAuthenticated: (user) => setPermissions((user as User).Permissions),
188
193
  },
189
194
  routes: [ /* ... */ ],
@@ -193,6 +198,7 @@ app.user; // 인증된 현재 사용자(미인증/미사용 시 undefined)
193
198
  ```
194
199
 
195
200
  - `me()` 가 값을 반환하면 셸 로드, `null`/`undefined` 면 `renderLogin({ root, onSuccess })`.
201
+ - `me()` 가 **던지면** 세션을 «모름» 으로 보고 로그인 UI 를 그리지 않는다 — `renderUnavailable({ root, error, retry })` 를 그리고, 없으면 `app.load()` 가 그 오류로 실패한다. 서버가 잠깐 503 을 낸 것을 «미인증» 으로 돌려주면 로그인된 사용자가 로그인 화면으로 간다.
196
202
  - 로그인 성공 시 `onSuccess()` 를 호출하면 앱이 재로드되어 셸이 나타나고 로그인 UI 는 정리된다.
197
203
  - `auth` 미지정 시 완전히 하위호환(게이트 없이 기존대로 로드).
198
204
 
package/dist/App.d.ts CHANGED
@@ -13,7 +13,7 @@ declare class App {
13
13
  private _router?;
14
14
  private _screen?;
15
15
  private _user?;
16
- private _loginTeardown?;
16
+ private _gateTeardown?;
17
17
  private constructor();
18
18
  /** 싱글톤 인스턴스 반환 */
19
19
  static get instance(): App;
package/dist/App.js CHANGED
@@ -46,18 +46,30 @@ var l = class o {
46
46
  await t.init(a.i18n);
47
47
  }
48
48
  if (a.auth) {
49
- let e = await a.auth.me();
50
- if (e == null) {
51
- let e = a.root || document.body, t = a.auth.renderLogin({
49
+ let e = a.root || document.body, t = () => {
50
+ this.load(a);
51
+ }, n;
52
+ try {
53
+ n = await a.auth.me();
54
+ } catch (n) {
55
+ if (!a.auth.renderUnavailable) throw n;
56
+ let r = a.auth.renderUnavailable({
52
57
  root: e,
53
- onSuccess: () => {
54
- this.load(a);
55
- }
58
+ error: n,
59
+ retry: t
56
60
  });
57
- t && (this._loginTeardown = t);
61
+ r && (this._gateTeardown = r);
58
62
  return;
59
63
  }
60
- this._user = e, await a.auth.onAuthenticated?.(e);
64
+ if (n == null) {
65
+ let n = a.auth.renderLogin({
66
+ root: e,
67
+ onSuccess: t
68
+ });
69
+ n && (this._gateTeardown = n);
70
+ return;
71
+ }
72
+ this._user = n, await a.auth.onAuthenticated?.(n);
61
73
  }
62
74
  let o = a.root || document.body;
63
75
  this._layout = await this.createLayout(o, a.layout), this._screen = new e({
@@ -77,7 +89,7 @@ var l = class o {
77
89
  });
78
90
  }
79
91
  unload() {
80
- this._loginTeardown &&= (this._loginTeardown(), 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;
92
+ 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;
81
93
  }
82
94
  navigate(e) {
83
95
  this._router?.go(e);
@@ -3,7 +3,7 @@ type ElementParts = 'host' | 'label' | 'value' | 'unit' | 'trend';
3
3
  export type InfoFieldFormat = 'number' | 'currency' | 'date';
4
4
  export type InfoFieldSize = 'default' | 'lg';
5
5
  export type InfoFieldTrend = 'up' | 'down' | 'flat';
6
- export type InfoFieldTone = 'positive' | 'negative' | 'neutral';
6
+ export type InfoFieldTone = 'positive' | 'negative' | 'warning' | 'neutral';
7
7
  /** 값이 "아직 없음"인가 — `0` 과 `false` 는 **값이다**. */
8
8
  export declare function isBlank(v: unknown): boolean;
9
9
  /**
@@ -89,8 +89,11 @@ export declare class InfoField extends StyledElement<ElementParts> {
89
89
  * direction-to-sentiment mapping (e.g. a falling "open tickets" count is `positive`).
90
90
  *
91
91
  * Colors the value text itself, independent of `trend` — a static "balance due" figure can be
92
- * toned `negative` without a trend arrow. `neutral` has no visual effect on the value text
93
- * (it already renders at full strength; only `positive`/`negative` stand out from it).
92
+ * toned `negative` without a trend arrow. `warning` is for "needs attention, not wrong" (a
93
+ * deadline coming up, a rate under target but within limits) and is never inferred from `trend`.
94
+ * `neutral` has no visual effect on the value text (it already renders at full strength; only
95
+ * `positive`/`negative`/`warning` stand out from it). Colors come from the theme's
96
+ * `--u-success|danger|warning-color-strong`.
94
97
  */
95
98
  tone?: InfoFieldTone;
96
99
  private get hasSlotted();
@@ -73,6 +73,10 @@ var t = e`
73
73
  color: var(--u-danger-color-strong, #C62828);
74
74
  }
75
75
 
76
+ .value.tone-warning {
77
+ color: var(--u-warning-color-strong, #8A4A00);
78
+ }
79
+
76
80
  .trend {
77
81
  font-size: var(--u-text-caption-size, 12px);
78
82
  margin-top: var(--u-space-3xs, 2px);
@@ -86,6 +90,10 @@ var t = e`
86
90
  color: var(--u-danger-color-strong, #C62828);
87
91
  }
88
92
 
93
+ .trend.tone-warning {
94
+ color: var(--u-warning-color-strong, #8A4A00);
95
+ }
96
+
89
97
  .trend.tone-neutral {
90
98
  color: var(--u-txt-color-weak, #616161);
91
99
  }
@@ -1,8 +1,5 @@
1
1
  import { StyledElement } from '../internals/StyledElement.js';
2
- type TimelineParts = 'host';
3
- type TimelineItemParts = 'host' | 'rail' | 'marker' | 'line' | 'content' | 'header' | 'heading' | 'time' | 'body';
4
- /** The role colours `u-tag`/`u-badge` use — the marker's colour. */
5
- export type TimelineItemColor = 'neutral' | 'primary' | 'info' | 'success' | 'warning' | 'danger';
2
+ type TimelineParts = 'host' | 'list';
6
3
  /**
7
4
  * A status or activity history — "what happened, and when" on a detail screen (order, ticket,
8
5
  * approval). An ordered list of {@link TimelineItem}s, each a marker on a rail, a heading, a time
@@ -25,39 +22,9 @@ export declare class Timeline extends StyledElement<TimelineParts> {
25
22
  connectedCallback(): void;
26
23
  render(): import('lit-html').TemplateResult<1>;
27
24
  }
28
- /**
29
- * One entry of a {@link Timeline}.
30
- *
31
- * - `heading` names the event ("Shipped"); the `heading` slot takes richer content.
32
- * - `datetime` is an ISO date or date-time. It is rendered as `<time datetime>` and formatted for the
33
- * active locale (`Locale`) — a date alone as a date, a date-time with its time. The `time` slot
34
- * replaces the text (a relative "2 hours ago", say); `datetime` still annotates it.
35
- * - `color` tints the marker. ⚠Colour is never the only carrier — say the status in `heading` too
36
- * (WCAG 1.4.1). `icon` (and `lib`) draws inside the marker.
37
- */
38
- export declare class TimelineItem extends StyledElement<TimelineItemParts> {
39
- static styles: import('lit').CSSResultGroup[];
40
- /** What happened. */
41
- heading: string;
42
- /** When — ISO `YYYY-MM-DD` or `YYYY-MM-DDTHH:mm[:ss]`. */
43
- datetime: string;
44
- /** Marker colour. Default `neutral`. */
45
- color: TimelineItemColor;
46
- /** Icon drawn inside the marker. */
47
- icon?: string;
48
- /** Icon library for `icon`. */
49
- lib?: string;
50
- /** Body slot assignment — CSS `:has()` cannot see slotted content (`internals/slotted.ts`). */
51
- private hasBody;
52
- connectedCallback(): void;
53
- /** Locale text for `datetime` — the raw value when it is not a date the formatter can read. */
54
- private get timeText();
55
- render(): import('lit-html').TemplateResult<1>;
56
- }
57
25
  declare global {
58
26
  interface HTMLElementTagNameMap {
59
27
  'u-timeline': Timeline;
60
- 'u-timeline-item': TimelineItem;
61
28
  }
62
29
  }
63
30
  export {};
@@ -1,68 +1,21 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.152.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 { itemStyles as i, styles as a } from "./Timeline.styles.js";
6
- import { html as o, nothing as s } from "lit";
7
- import { customElement as c, property as l, state as u } from "lit/decorators.js";
8
- import { formatDate as d } from "@iyulab/components/dist/utilities/format.js";
9
- import "@iyulab/components/dist/components/icon/UIcon.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
2
+ import { StyledElement as t } from "../internals/StyledElement.js";
3
+ import "./TimelineItem.js";
4
+ import { styles as n } from "./Timeline.styles.js";
5
+ import { html as r } from "lit";
6
+ import { customElement as i } from "lit/decorators.js";
10
7
  //#region src/components/Timeline.ts
11
- var f = class extends n {
8
+ var a = class extends t {
12
9
  static {
13
- this.styles = [super.styles, a];
10
+ this.styles = [super.styles, n];
14
11
  }
15
12
  connectedCallback() {
16
13
  super.connectedCallback(), this.hasAttribute("role") || this.setAttribute("role", "list");
17
14
  }
18
15
  render() {
19
- return o`<slot></slot>`;
16
+ return r`<div class="list" part="list"><slot></slot></div>`;
20
17
  }
21
18
  };
22
- f = t([c("u-timeline")], f);
23
- var p = class extends n {
24
- constructor(...e) {
25
- super(...e), this.heading = "", this.datetime = "", this.color = "neutral", this.hasBody = !1;
26
- }
27
- static {
28
- this.styles = [super.styles, i];
29
- }
30
- connectedCallback() {
31
- super.connectedCallback(), this.hasAttribute("role") || this.setAttribute("role", "listitem");
32
- }
33
- get timeText() {
34
- let e = this.datetime.trim();
35
- if (!e) return "";
36
- if (!e.includes("T")) return d(e, { dateStyle: "medium" });
37
- let t = new Date(e);
38
- return Number.isNaN(t.getTime()) ? e : d(t, {
39
- dateStyle: "medium",
40
- timeStyle: "short"
41
- });
42
- }
43
- render() {
44
- return o`
45
- <div class="rail" part="rail" aria-hidden="true">
46
- <span class="marker ${this.icon ? "with-icon" : ""}" part="marker">
47
- ${this.icon ? o`<u-icon .lib=${this.lib} .name=${this.icon}></u-icon>` : s}
48
- </span>
49
- <span class="line" part="line"></span>
50
- </div>
51
- <div class="content" part="content">
52
- <div class="header" part="header">
53
- <span class="heading" part="heading"><slot name="heading">${this.heading}</slot></span>
54
- ${this.datetime ? o`<time class="time" part="time" datetime=${this.datetime}><slot name="time">${this.timeText}</slot></time>` : o`<span class="time" part="time"><slot name="time"></slot></span>`}
55
- </div>
56
- <div class="body ${this.hasBody ? "" : "empty"}" part="body">
57
- <slot @slotchange=${(e) => this.hasBody = r(e.target)}></slot>
58
- </div>
59
- </div>
60
- `;
61
- }
62
- };
63
- t([l({ type: String }), e("design:type", Object)], p.prototype, "heading", void 0), t([l({ type: String }), e("design:type", Object)], p.prototype, "datetime", void 0), t([l({
64
- type: String,
65
- reflect: !0
66
- }), e("design:type", Object)], p.prototype, "color", void 0), t([l({ type: String }), e("design:type", String)], p.prototype, "icon", void 0), t([l({ type: String }), e("design:type", String)], p.prototype, "lib", void 0), t([u(), e("design:type", Object)], p.prototype, "hasBody", void 0), p = t([c("u-timeline-item")], p);
19
+ a = e([i("u-timeline")], a);
67
20
  //#endregion
68
- export { f as Timeline, p as TimelineItem };
21
+ export { a as Timeline };
@@ -1,2 +1 @@
1
1
  export declare const styles: import('lit').CSSResult;
2
- export declare const itemStyles: import('lit').CSSResult;
@@ -4,123 +4,11 @@ var t = e`
4
4
  :host {
5
5
  display: block;
6
6
  }
7
- `, n = e`
8
- :host {
9
- display: grid;
10
- grid-template-columns: var(--app-timeline-rail-width, 24px) minmax(0, 1fr);
11
- column-gap: var(--u-space-md, 12px);
12
- }
13
7
 
14
- /* The rail: a marker, then a line down to the next entry's marker. */
15
- .rail {
8
+ .list {
16
9
  display: flex;
17
10
  flex-direction: column;
18
- align-items: center;
19
- }
20
-
21
- .marker {
22
- flex: none;
23
- display: inline-flex;
24
- align-items: center;
25
- justify-content: center;
26
- box-sizing: border-box;
27
- width: var(--app-timeline-marker-size, 12px);
28
- height: var(--app-timeline-marker-size, 12px);
29
- /* Centred on the heading's first line (label size × leading). */
30
- margin-top: calc((var(--u-text-label-size, 13px) * var(--u-text-label-leading, 1.5) - var(--app-timeline-marker-size, 12px)) / 2);
31
- border-radius: 50%;
32
- /* Fill and the colour on it come as a pair (the components contract — warning carries dark text,
33
- not white). The edge is the strong step: a yellow fill alone is about 1.4:1 against the page. */
34
- background-color: var(--_marker, var(--u-border-color-strong, #BDBDBD));
35
- color: var(--_on-marker, var(--u-txt-color, #212121));
36
- border: 1px solid var(--_edge, var(--u-border-color-strong, #BDBDBD));
37
- }
38
- .marker.with-icon {
39
- width: var(--app-timeline-rail-width, 24px);
40
- height: var(--app-timeline-rail-width, 24px);
41
- margin-top: calc((var(--u-text-label-size, 13px) * var(--u-text-label-leading, 1.5) - var(--app-timeline-rail-width, 24px)) / 2);
42
- font-size: var(--u-text-caption-size, 12px);
43
- }
44
-
45
- :host([color="primary"]) {
46
- --_marker: var(--u-primary-color, #1976D2);
47
- --_on-marker: var(--u-primary-txt-color, #FFFFFF);
48
- --_edge: var(--u-primary-color-strong, #1565C0);
49
- }
50
- :host([color="info"]) {
51
- --_marker: var(--u-info-color, #1976D2);
52
- --_on-marker: var(--u-info-txt-color, #FFFFFF);
53
- --_edge: var(--u-info-color-strong, #1565C0);
54
- }
55
- :host([color="success"]) {
56
- --_marker: var(--u-success-color, #2E7D32);
57
- --_on-marker: var(--u-success-txt-color, #FFFFFF);
58
- --_edge: var(--u-success-color-strong, #1B5E20);
59
- }
60
- :host([color="warning"]) {
61
- --_marker: var(--u-warning-color, #FDD835);
62
- --_on-marker: var(--u-warning-txt-color, #000000);
63
- --_edge: var(--u-warning-color-strong, #8A4A00);
64
- }
65
- :host([color="danger"]) {
66
- --_marker: var(--u-danger-color, #D32F2F);
67
- --_on-marker: var(--u-danger-txt-color, #FFFFFF);
68
- --_edge: var(--u-danger-color-strong, #C62828);
69
- }
70
-
71
- .line {
72
- flex: 1 1 auto;
73
- min-height: var(--u-space-md, 12px);
74
- margin-top: var(--u-space-2xs, 4px);
75
- /* The rail is what reads as a timeline — the weak border step (about 1.25:1 on a card) vanished. */
76
- border-left: 1px solid var(--u-border-color-strong, #BDBDBD);
77
- }
78
- /* The last entry has no next marker to reach. */
79
- :host(:last-child) .line {
80
- display: none;
81
- }
82
-
83
- .content {
84
- min-width: 0;
85
- padding-bottom: var(--u-space-lg, 16px);
86
- }
87
- :host(:last-child) .content {
88
- padding-bottom: 0;
89
- }
90
-
91
- .header {
92
- display: flex;
93
- flex-wrap: wrap;
94
- align-items: baseline;
95
- column-gap: var(--u-space-sm, 8px);
96
- }
97
-
98
- .heading {
99
- font-size: var(--u-text-label-size, 13px);
100
- line-height: var(--u-text-label-leading, 1.5);
101
- font-weight: var(--u-text-label-weight, 600);
102
- color: var(--u-txt-color, #212121);
103
- overflow-wrap: anywhere;
104
- }
105
-
106
- .time {
107
- font-size: var(--u-text-caption-size, 12px);
108
- color: var(--u-txt-color-weak, #616161);
109
- font-variant-numeric: tabular-nums;
110
- white-space: nowrap;
111
- }
112
-
113
- .body {
114
- margin-top: var(--u-space-2xs, 4px);
115
- font-size: var(--u-text-body-size, 14px);
116
- line-height: var(--u-text-body-leading, 1.6);
117
- color: var(--u-txt-color-weak, #616161);
118
- overflow-wrap: anywhere;
119
- }
120
- /* No body content — no gap under the header. */
121
- .body.empty {
122
- display: none;
123
11
  }
124
12
  `;
125
13
  //#endregion
126
- export { n as itemStyles, t as styles };
14
+ export { t as styles };
@@ -0,0 +1,39 @@
1
+ import { StyledElement } from '../internals/StyledElement.js';
2
+ type TimelineItemParts = 'host' | 'rail' | 'marker' | 'line' | 'content' | 'header' | 'heading' | 'time' | 'body';
3
+ /** The role colours `u-tag`/`u-badge` use — the marker's colour. */
4
+ export type TimelineItemColor = 'neutral' | 'primary' | 'info' | 'success' | 'warning' | 'danger';
5
+ /**
6
+ * One entry of a `u-timeline`.
7
+ *
8
+ * - `heading` names the event ("Shipped"); the `heading` slot takes richer content.
9
+ * - `datetime` is an ISO date or date-time. It is rendered as `<time datetime>` and formatted for the
10
+ * active locale (`Locale`) — a date alone as a date, a date-time with its time. The `time` slot
11
+ * replaces the text (a relative "2 hours ago", say); `datetime` still annotates it.
12
+ * - `color` tints the marker. ⚠Colour is never the only carrier — say the status in `heading` too
13
+ * (WCAG 1.4.1). `icon` (and `lib`) draws inside the marker.
14
+ */
15
+ export declare class TimelineItem extends StyledElement<TimelineItemParts> {
16
+ static styles: import('lit').CSSResultGroup[];
17
+ /** What happened. */
18
+ heading: string;
19
+ /** When — ISO `YYYY-MM-DD` or `YYYY-MM-DDTHH:mm[:ss]`. */
20
+ datetime: string;
21
+ /** Marker colour. Default `neutral`. */
22
+ color: TimelineItemColor;
23
+ /** Icon drawn inside the marker. */
24
+ icon?: string;
25
+ /** Icon library for `icon`. */
26
+ lib?: string;
27
+ /** Body slot assignment — CSS `:has()` cannot see slotted content (`internals/slotted.ts`). */
28
+ private hasBody;
29
+ connectedCallback(): void;
30
+ /** Locale text for `datetime` — the raw value when it is not a date the formatter can read. */
31
+ private get timeText();
32
+ render(): import('lit-html').TemplateResult<1>;
33
+ }
34
+ declare global {
35
+ interface HTMLElementTagNameMap {
36
+ 'u-timeline-item': TimelineItem;
37
+ }
38
+ }
39
+ export {};
@@ -0,0 +1,56 @@
1
+ import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.152.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 { styles as i } from "./TimelineItem.styles.js";
6
+ import { html as a, nothing as o } from "lit";
7
+ import { customElement as s, property as c, state as l } from "lit/decorators.js";
8
+ import { formatDate as u } from "@iyulab/components/dist/utilities/format.js";
9
+ import "@iyulab/components/dist/components/icon/UIcon.js";
10
+ //#region src/components/TimelineItem.ts
11
+ var d = class extends n {
12
+ constructor(...e) {
13
+ super(...e), this.heading = "", this.datetime = "", this.color = "neutral", this.hasBody = !1;
14
+ }
15
+ static {
16
+ this.styles = [super.styles, i];
17
+ }
18
+ connectedCallback() {
19
+ super.connectedCallback(), this.hasAttribute("role") || this.setAttribute("role", "listitem");
20
+ }
21
+ get timeText() {
22
+ let e = this.datetime.trim();
23
+ if (!e) return "";
24
+ if (!e.includes("T")) return u(e, { dateStyle: "medium" });
25
+ let t = new Date(e);
26
+ return Number.isNaN(t.getTime()) ? e : u(t, {
27
+ dateStyle: "medium",
28
+ timeStyle: "short"
29
+ });
30
+ }
31
+ render() {
32
+ return a`
33
+ <div class="rail" part="rail" aria-hidden="true">
34
+ <span class="marker ${this.icon ? "with-icon" : ""}" part="marker">
35
+ ${this.icon ? a`<u-icon .lib=${this.lib} .name=${this.icon}></u-icon>` : o}
36
+ </span>
37
+ <span class="line" part="line"></span>
38
+ </div>
39
+ <div class="content" part="content">
40
+ <div class="header" part="header">
41
+ <span class="heading" part="heading"><slot name="heading">${this.heading}</slot></span>
42
+ ${this.datetime ? a`<time class="time" part="time" datetime=${this.datetime}><slot name="time">${this.timeText}</slot></time>` : a`<span class="time" part="time"><slot name="time"></slot></span>`}
43
+ </div>
44
+ <div class="body ${this.hasBody ? "" : "empty"}" part="body">
45
+ <slot @slotchange=${(e) => this.hasBody = r(e.target)}></slot>
46
+ </div>
47
+ </div>
48
+ `;
49
+ }
50
+ };
51
+ t([c({ type: String }), e("design:type", Object)], d.prototype, "heading", void 0), t([c({ type: String }), e("design:type", Object)], d.prototype, "datetime", void 0), t([c({
52
+ type: String,
53
+ reflect: !0
54
+ }), e("design:type", Object)], d.prototype, "color", void 0), t([c({ type: String }), e("design:type", String)], d.prototype, "icon", void 0), t([c({ type: String }), e("design:type", String)], d.prototype, "lib", void 0), t([l(), e("design:type", Object)], d.prototype, "hasBody", void 0), d = t([s("u-timeline-item")], d);
55
+ //#endregion
56
+ export { d as TimelineItem };
@@ -0,0 +1 @@
1
+ export declare const styles: import('lit').CSSResult;
@@ -0,0 +1,122 @@
1
+ import { css as e } from "lit";
2
+ //#region src/components/TimelineItem.styles.ts
3
+ var t = e`
4
+ :host {
5
+ display: grid;
6
+ grid-template-columns: var(--app-timeline-rail-width, 24px) minmax(0, 1fr);
7
+ column-gap: var(--u-space-md, 12px);
8
+ }
9
+
10
+ /* The rail: a marker, then a line down to the next entry's marker. */
11
+ .rail {
12
+ display: flex;
13
+ flex-direction: column;
14
+ align-items: center;
15
+ }
16
+
17
+ .marker {
18
+ flex: none;
19
+ display: inline-flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ box-sizing: border-box;
23
+ width: var(--app-timeline-marker-size, 12px);
24
+ height: var(--app-timeline-marker-size, 12px);
25
+ /* Centred on the heading's first line (label size × leading). */
26
+ margin-top: calc((var(--u-text-label-size, 13px) * var(--u-text-label-leading, 1.5) - var(--app-timeline-marker-size, 12px)) / 2);
27
+ border-radius: 50%;
28
+ /* Fill and the colour on it come as a pair (the components contract — warning carries dark text,
29
+ not white). The edge is the strong step: a yellow fill alone is about 1.4:1 against the page. */
30
+ background-color: var(--_marker, var(--u-border-color-strong, #BDBDBD));
31
+ color: var(--_on-marker, var(--u-txt-color, #212121));
32
+ border: 1px solid var(--_edge, var(--u-border-color-strong, #BDBDBD));
33
+ }
34
+ .marker.with-icon {
35
+ width: var(--app-timeline-rail-width, 24px);
36
+ height: var(--app-timeline-rail-width, 24px);
37
+ margin-top: calc((var(--u-text-label-size, 13px) * var(--u-text-label-leading, 1.5) - var(--app-timeline-rail-width, 24px)) / 2);
38
+ font-size: var(--u-text-caption-size, 12px);
39
+ }
40
+
41
+ :host([color="primary"]) {
42
+ --_marker: var(--u-primary-color, #1976D2);
43
+ --_on-marker: var(--u-primary-txt-color, #FFFFFF);
44
+ --_edge: var(--u-primary-color-strong, #1565C0);
45
+ }
46
+ :host([color="info"]) {
47
+ --_marker: var(--u-info-color, #1976D2);
48
+ --_on-marker: var(--u-info-txt-color, #FFFFFF);
49
+ --_edge: var(--u-info-color-strong, #1565C0);
50
+ }
51
+ :host([color="success"]) {
52
+ --_marker: var(--u-success-color, #2E7D32);
53
+ --_on-marker: var(--u-success-txt-color, #FFFFFF);
54
+ --_edge: var(--u-success-color-strong, #1B5E20);
55
+ }
56
+ :host([color="warning"]) {
57
+ --_marker: var(--u-warning-color, #FDD835);
58
+ --_on-marker: var(--u-warning-txt-color, #000000);
59
+ --_edge: var(--u-warning-color-strong, #8A4A00);
60
+ }
61
+ :host([color="danger"]) {
62
+ --_marker: var(--u-danger-color, #D32F2F);
63
+ --_on-marker: var(--u-danger-txt-color, #FFFFFF);
64
+ --_edge: var(--u-danger-color-strong, #C62828);
65
+ }
66
+
67
+ .line {
68
+ flex: 1 1 auto;
69
+ min-height: var(--u-space-md, 12px);
70
+ margin-top: var(--u-space-2xs, 4px);
71
+ /* The rail is what reads as a timeline — the weak border step (about 1.25:1 on a card) vanished. */
72
+ border-left: 1px solid var(--u-border-color-strong, #BDBDBD);
73
+ }
74
+ /* The last entry has no next marker to reach. */
75
+ :host(:last-child) .line {
76
+ display: none;
77
+ }
78
+
79
+ .content {
80
+ min-width: 0;
81
+ padding-bottom: var(--u-space-lg, 16px);
82
+ }
83
+ :host(:last-child) .content {
84
+ padding-bottom: 0;
85
+ }
86
+
87
+ .header {
88
+ display: flex;
89
+ flex-wrap: wrap;
90
+ align-items: baseline;
91
+ column-gap: var(--u-space-sm, 8px);
92
+ }
93
+
94
+ .heading {
95
+ font-size: var(--u-text-label-size, 13px);
96
+ line-height: var(--u-text-label-leading, 1.5);
97
+ font-weight: var(--u-text-label-weight, 600);
98
+ color: var(--u-txt-color, #212121);
99
+ overflow-wrap: anywhere;
100
+ }
101
+
102
+ .time {
103
+ font-size: var(--u-text-caption-size, 12px);
104
+ color: var(--u-txt-color-weak, #616161);
105
+ font-variant-numeric: tabular-nums;
106
+ white-space: nowrap;
107
+ }
108
+
109
+ .body {
110
+ margin-top: var(--u-space-2xs, 4px);
111
+ font-size: var(--u-text-body-size, 14px);
112
+ line-height: var(--u-text-body-leading, 1.6);
113
+ color: var(--u-txt-color-weak, #616161);
114
+ overflow-wrap: anywhere;
115
+ }
116
+ /* No body content — no gap under the header. */
117
+ .body.empty {
118
+ display: none;
119
+ }
120
+ `;
121
+ //#endregion
122
+ export { t as styles };
package/dist/index.d.ts CHANGED
@@ -11,8 +11,9 @@ export { GroupBox } from './components/GroupBox.js';
11
11
  export { InfoSection } from './components/InfoSection.js';
12
12
  export { InfoField, isBlank } from './components/InfoField.js';
13
13
  export { EmptyState } from './components/EmptyState.js';
14
- export { Timeline, TimelineItem } from './components/Timeline.js';
15
- export type { TimelineItemColor } from './components/Timeline.js';
14
+ export { Timeline } from './components/Timeline.js';
15
+ export { TimelineItem } from './components/TimelineItem.js';
16
+ export type { TimelineItemColor } from './components/TimelineItem.js';
16
17
  export { ActionBar } from './components/ActionBar.js';
17
18
  export { MasterDetailLayout } from './components/MasterDetailLayout.js';
18
19
  export type { MasterDetailLayoutEventMap } from './components/MasterDetailLayout.js';
package/dist/index.js CHANGED
@@ -7,7 +7,8 @@ import { GroupBox as l } from "./components/GroupBox.js";
7
7
  import { InfoSection as u } from "./components/InfoSection.js";
8
8
  import { InfoField as d, isBlank as f } from "./components/InfoField.js";
9
9
  import { EmptyState as p } from "./components/EmptyState.js";
10
- import { Timeline as m, TimelineItem as h } from "./components/Timeline.js";
10
+ import { TimelineItem as m } from "./components/TimelineItem.js";
11
+ import { Timeline as h } from "./components/Timeline.js";
11
12
  import { ActionBar as g } from "./components/ActionBar.js";
12
13
  import { MasterDetailLayout as _ } from "./components/MasterDetailLayout.js";
13
14
  import { Wizard as v } from "./components/Wizard.js";
@@ -15,4 +16,4 @@ import { translate as y } from "./translate.js";
15
16
  //#region src/index.ts
16
17
  var b = t;
17
18
  //#endregion
18
- export { g as ActionBar, p as EmptyState, l as GroupBox, d as InfoField, u as InfoSection, _ as MasterDetailLayout, c as PageHeader, e as ScreenObserver, m as Timeline, h as TimelineItem, v as Wizard, t as app, b as default, n as filterSidebarItems, r as getDefaultLocale, i as getLocaleStrings, f as isBlank, a as modernAppLocale, o as registerLocale, s as setDefaultLocale, y as translate };
19
+ export { g as ActionBar, p as EmptyState, l as GroupBox, d as InfoField, u as InfoSection, _ as MasterDetailLayout, c as PageHeader, e as ScreenObserver, h as Timeline, m as TimelineItem, v as Wizard, t as app, b as default, n as filterSidebarItems, r as getDefaultLocale, i as getLocaleStrings, f as isBlank, a as modernAppLocale, o as registerLocale, s as setDefaultLocale, y as translate };
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { TimelineItem as TimelineItemElement } from '@iyulab/modern-app/dist/components/TimelineItem.js';
3
+
4
+ type TimelineItemOwnProps = Omit<TimelineItemElement, keyof HTMLElement>;
5
+
6
+ export declare const TimelineItem: React.ForwardRefExoticComponent<
7
+ Partial<Omit<TimelineItemElement, Exclude<keyof React.HTMLAttributes<TimelineItemElement>, keyof TimelineItemOwnProps>>>
8
+ & Omit<React.HTMLAttributes<TimelineItemElement>, keyof TimelineItemOwnProps | never>
9
+ & React.RefAttributes<TimelineItemElement>
10
+ & {
11
+ }
12
+ >;
13
+
14
+ export type TimelineItemProps = React.ComponentProps<typeof TimelineItem>;
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { createComponent } from '@lit/react';
3
+ import { TimelineItem as TimelineItemElement } from '@iyulab/modern-app/dist/components/TimelineItem.js';
4
+
5
+ export const TimelineItem = createComponent({
6
+ react: React,
7
+ tagName: 'u-timeline-item',
8
+ elementClass: TimelineItemElement,
9
+ events: {},
10
+ });
@@ -1,4 +1,5 @@
1
1
  export * from '@iyulab/modern-app/dist/react.js';
2
+ export { TimelineItem, TimelineItemProps } from './TimelineItem';
2
3
  export { Timeline, TimelineProps } from './Timeline';
3
4
  export { SidebarButton, SidebarButtonProps } from './SidebarButton';
4
5
  export { PageHeader, PageHeaderProps } from './PageHeader';
@@ -1,4 +1,5 @@
1
1
  export * from '@iyulab/modern-app/dist/react.js';
2
+ export { TimelineItem } from './TimelineItem.js';
2
3
  export { Timeline } from './Timeline.js';
3
4
  export { SidebarButton } from './SidebarButton.js';
4
5
  export { PageHeader } from './PageHeader.js';
package/dist/react.d.ts CHANGED
@@ -3,7 +3,7 @@ import { EventName } from '@lit/react';
3
3
  import { SidebarLayout as SidebarLayoutElement, SidebarLayoutEventMap } from './layouts/SidebarLayout.js';
4
4
  import { Wizard as WizardElement, WizardEventMap } from './components/Wizard.js';
5
5
  import { GroupBoxLevel } from './components/GroupBox.js';
6
- import { TimelineItemColor } from './components/Timeline.js';
6
+ import { TimelineItemColor } from './components/TimelineItem.js';
7
7
  import { InfoFieldFormat, InfoFieldSize, InfoFieldTrend, InfoFieldTone } from './components/InfoField.js';
8
8
  export { ScreenObserver } from './internals/ScreenObserver.js';
9
9
  export type { ScreenSize, ScreenObserverConfig, ScreenResizeEvent } from './internals/ScreenObserver.js';
@@ -102,15 +102,21 @@ export interface AppConfig {
102
102
  i18n?: I18nInitOptions;
103
103
  /**
104
104
  * 부팅 인증 게이트(선택). 지정하면 앱 셸을 만들기 전에 `me()` 로 세션을 판정하여
105
- * 인증 시 셸 로드, 미인증 시 `renderLogin` 으로 로그인 UI 를 띄운다.
105
+ * 인증 시 셸 로드, 미인증 시 `renderLogin` 으로 로그인 UI 를 띄운다. `me()` 가 던지면(세션 모름)
106
+ * `renderUnavailable` 을 그린다.
106
107
  * @see AuthGateConfig
107
108
  * @example
108
109
  * ```typescript
109
110
  * app.load({
110
111
  * layout: { type: 'sidebar', ... },
111
112
  * auth: {
112
- * me: () => authClient.fetchMe(), // null → 미인증
113
+ * me: async () => { // @iyulab/enterprise createAuthClient
114
+ * const s = await authClient.fetchMe();
115
+ * if (s.status === 'unknown') throw s.error; // 모름 — 로그인이 아니라 renderUnavailable
116
+ * return s.status === 'authenticated' ? s.user : null; // null → 미인증
117
+ * },
113
118
  * renderLogin: ({ root, onSuccess }) => renderLoginPage(root, onSuccess),
119
+ * renderUnavailable: ({ root, retry }) => renderOfflinePage(root, retry),
114
120
  * onAuthenticated: (user) => setPermissions((user as User).Permissions),
115
121
  * },
116
122
  * routes: [ ... ],
@@ -4,7 +4,9 @@
4
4
  * `app.load({ auth })` 에 넘기면, 앱 셸(레이아웃·라우터)을 만들기 전에 세션을 판정한다:
5
5
  * `me()` 가 값을 반환하면 인증된 것으로 보고 셸을 로드하고, `null`/`undefined` 면
6
6
  * 미인증으로 보고 `renderLogin` 으로 로그인 UI 를 띄운다. 로그인 성공 시 `onSuccess()` 를
7
- * 호출하면 앱이 (재)로드되어 셸이 나타난다.
7
+ * 호출하면 앱이 (재)로드되어 셸이 나타난다. `me()` 가 **던지면** 세션을 «모름»(서버 다운·오프라인)
8
+ * 으로 보고 로그인 UI 를 그리지 않는다 — `renderUnavailable` 이 있으면 그것을, 없으면 `load` 가 그
9
+ * 오류로 실패한다.
8
10
  *
9
11
  * 프레임워크는 인증의 **오케스트레이션**(판정 → 분기 → 재로드)만 소유한다. 세션 조회/로그인
10
12
  * 자체(HTTP)와 사용자·권한 형태, 세션-중 401 처리는 앱/`@iyulab/enterprise` 가 소유한다.
@@ -15,9 +17,19 @@ export interface AuthGateContext {
15
17
  /** 로그인 성공 시 호출한다. 앱이 (재)로드되어 셸을 띄운다(이때 `me()` 는 사용자를 반환해야 한다). */
16
18
  onSuccess: () => void;
17
19
  }
20
+ /** `renderUnavailable` 이 받는 맥락 — 세션을 확인하지 못했을 때. */
21
+ export interface AuthGateUnavailableContext {
22
+ /** 안내 UI 를 그릴 루트 요소(= `app.load` 의 `root`, 기본 `document.body`). */
23
+ root: Element;
24
+ /** `me()` 가 던진 값. */
25
+ error: unknown;
26
+ /** 다시 확인한다 — 앱이 (재)로드되어 `me()` 를 다시 부른다(재시도 버튼·`online` 이벤트에서). */
27
+ retry: () => void;
28
+ }
18
29
  export interface AuthGateConfig {
19
30
  /**
20
- * 현재 세션 조회. 값을 반환하면 인증, `null`/`undefined` 면 미인증으로 본다.
31
+ * 현재 세션 조회. 값을 반환하면 인증, `null`/`undefined` 면 미인증으로 본다. **세션을 확인하지
32
+ * 못했으면 던진다**(서버 다운·오프라인) — 그것을 `null` 로 돌려주면 로그인된 사용자가 로그인 화면으로 간다.
21
33
  * 동기/비동기 모두 허용한다.
22
34
  */
23
35
  me: () => Promise<unknown | null | undefined> | unknown | null | undefined;
@@ -26,6 +38,11 @@ export interface AuthGateConfig {
26
38
  * 정리 함수를 반환하면 앱 로드/`unload` 시 호출되어 로그인 UI 를 제거한다.
27
39
  */
28
40
  renderLogin: (context: AuthGateContext) => (() => void) | void;
41
+ /**
42
+ * `me()` 가 던졌을 때(세션 모름) `context.root` 에 안내 UI 를 그린다(선택). `context.retry()` 로 다시
43
+ * 확인한다. 정리 함수를 반환하면 앱 로드/`unload` 시 호출된다. 없으면 `load` 가 `me()` 의 오류로 실패한다.
44
+ */
45
+ renderUnavailable?: (context: AuthGateUnavailableContext) => (() => void) | void;
29
46
  /**
30
47
  * 인증 성공 후, 앱 셸을 만들기 직전에 호출된다(선택).
31
48
  * 사용자별 라우트/메뉴 필터 등 셸 구성 전 처리를 여기서 한다.
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.37.1",
4
+ "version": "0.38.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -228,7 +228,11 @@ The framework owns only the orchestration (check → branch → reload) — sess
228
228
 
229
229
  ```typescript
230
230
  interface AuthGateConfig {
231
- /** Resolve the current session. Return a value for authenticated, `null`/`undefined` for not. */
231
+ /**
232
+ * Resolve the current session. Return a value for authenticated, `null`/`undefined` for not.
233
+ * **Throw when the session could not be checked** (server down, offline) — returning `null`
234
+ * there would send a signed-in user to the login screen.
235
+ */
232
236
  me: () => Promise<unknown | null | undefined> | unknown | null | undefined;
233
237
 
234
238
  /**
@@ -237,6 +241,14 @@ interface AuthGateConfig {
237
241
  */
238
242
  renderLogin: (context: AuthGateContext) => (() => void) | void;
239
243
 
244
+ /**
245
+ * Renders a "can't reach the server" view into `context.root` when `me()` throws. Call
246
+ * `context.retry()` to check again (a retry button, the `online` event). Return a cleanup
247
+ * function to have it called on app load/`unload`. Without it, `app.load()` rejects with
248
+ * `me()`'s error.
249
+ */
250
+ renderUnavailable?: (context: AuthGateUnavailableContext) => (() => void) | void;
251
+
240
252
  /** Called once authenticated, right before the app shell is built. */
241
253
  onAuthenticated?: (user: unknown) => void | Promise<void>;
242
254
  }
@@ -247,6 +259,15 @@ interface AuthGateContext {
247
259
  /** Call on successful login — the app (re)loads and the shell appears. */
248
260
  onSuccess: () => void;
249
261
  }
262
+
263
+ interface AuthGateUnavailableContext {
264
+ /** Root element to render into (same as `AppConfig.root`, default `document.body`). */
265
+ root: Element;
266
+ /** What `me()` threw. */
267
+ error: unknown;
268
+ /** Check again — the app (re)loads and calls `me()` again. */
269
+ retry: () => void;
270
+ }
250
271
  ```
251
272
 
252
273
  ---
@@ -40,7 +40,7 @@ and those are different states of the business. So the component owns the rule.
40
40
  | `size` | `'default'\|'lg'` | `'default'` | ✓ | `'lg'` renders the value at the `title` type-scale step — for dashboard KPI tiles composed inside `u-info-section` |
41
41
  | `trend` | `'up'\|'down'\|'flat'` | — | | Trend direction; renders a trend indicator when set, alongside `trendLabel`; pair with trendLabel for an accessible name — trend alone conveys direction by color only |
42
42
  | `trendLabel` | `string` | — | | Trend copy, e.g. `"+12% vs last month"` — wording is the consumer's responsibility |
43
- | `tone` | `'positive'\|'negative'\|'neutral'` | — | | Explicit tone override; resolves from `trend` when unset (`up→positive`, `down→negative`, `flat`/unset→`neutral`) but always wins over inference. Colors the value text itself, independent of `trend` — a static figure (e.g. a balance due) can be toned `negative` with no trend arrow |
43
+ | `tone` | `'positive'\|'negative'\|'warning'\|'neutral'` | — | | Explicit tone override; resolves from `trend` when unset (`up→positive`, `down→negative`, `flat`/unset→`neutral`) but always wins over inference. Colors the value text itself, independent of `trend` — a static figure (e.g. a balance due) can be toned `negative` with no trend arrow. `warning` means "needs attention, not wrong" (a deadline coming up, a rate under target but within limits) and is never inferred |
44
44
 
45
45
  ⚠ `numeric` no longer right-aligns (it did before 0.25.0). A field is a label/value pair that owns
46
46
  one grid cell, with no neighbouring figures to line up, so right-aligning only pushed the value
@@ -73,8 +73,8 @@ stay level:
73
73
  blank="No downtime recorded for the failures"></u-info-field>
74
74
  ```
75
75
 
76
- `tone` colors a blank value too, so a reason that needs attention takes `tone="negative"`. Set it
77
- conditionally (`tone=${mttr == null ? 'negative' : undefined}`) — otherwise the value is toned
76
+ `tone` colors a blank value too, so a reason that needs attention takes `tone="warning"`. Set it
77
+ conditionally (`tone=${mttr == null ? 'warning' : undefined}`) — otherwise the value is toned
78
78
  as well once it exists.
79
79
 
80
80
  ## CSS Parts
@@ -99,3 +99,4 @@ as well once it exists.
99
99
  | `--u-text-label-size` | Unit size |
100
100
  | `--u-success-color-strong` | `tone="positive"` color (value and trend) |
101
101
  | `--u-danger-color-strong` | `tone="negative"` color (value and trend) |
102
+ | `--u-warning-color-strong` | `tone="warning"` color (value and trend) |
@@ -42,10 +42,11 @@ a list and each entry a list item, so assistive technology announces the count.
42
42
 
43
43
  ## CSS Parts
44
44
 
45
- | Element | Parts |
46
- |---------|-------|
47
- | `u-timeline` | `host` |
48
- | `u-timeline-item` | `host` · `rail` · `marker` · `line` · `content` · `header` · `heading` · `time` · `body` |
45
+ | Part | Description |
46
+ |------|-------------|
47
+ | `list` | `u-timeline`: the column the entries stack in |
48
+ | `rail` · `marker` · `line` | Entry: the rail column, its marker, and the line down to the next entry |
49
+ | `content` · `header` · `heading` · `time` · `body` | Entry: the text column — header row (heading and time) and body |
49
50
 
50
51
  ## CSS Custom Properties
51
52