@iyulab/modern-app 0.33.3 → 0.34.1

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/App.d.ts +11 -0
  3. package/dist/App.js +7 -0
  4. package/dist/components/ActionBar.d.ts +2 -2
  5. package/dist/components/GroupBox.d.ts +9 -2
  6. package/dist/components/GroupBox.js +18 -15
  7. package/dist/components/GroupBox.styles.js +28 -5
  8. package/dist/components/MasterDetailLayout.js +1 -1
  9. package/dist/components/PageHeader.d.ts +14 -4
  10. package/dist/components/PageHeader.js +8 -3
  11. package/dist/components/PageHeader.styles.js +22 -4
  12. package/dist/components/SidebarButton.styles.js +2 -0
  13. package/dist/components/SidebarGroup.styles.js +2 -0
  14. package/dist/components/SidebarLink.d.ts +11 -1
  15. package/dist/components/SidebarLink.js +2 -1
  16. package/dist/components/SidebarLink.styles.js +43 -7
  17. package/dist/components/SidebarSection.styles.js +6 -9
  18. package/dist/components/Wizard.js +1 -1
  19. package/dist/components/Wizard.styles.js +2 -0
  20. package/dist/layouts/SidebarLayout.d.ts +5 -0
  21. package/dist/layouts/SidebarLayout.js +6 -5
  22. package/dist/layouts/SidebarLayout.styles.js +23 -5
  23. package/dist/react/ActionBar.d.ts +4 -2
  24. package/dist/react/EmptyState.d.ts +4 -2
  25. package/dist/react/GroupBox.d.ts +4 -2
  26. package/dist/react/InfoField.d.ts +4 -2
  27. package/dist/react/InfoSection.d.ts +4 -2
  28. package/dist/react/MasterDetailLayout.d.ts +4 -2
  29. package/dist/react/PageHeader.d.ts +4 -2
  30. package/dist/react/SidebarButton.d.ts +4 -2
  31. package/dist/types/AppConfigs.d.ts +7 -0
  32. package/package.json +5 -4
  33. package/skills/modern-app/SKILL.md +0 -1
  34. package/skills/modern-app/references/api.md +3 -0
  35. package/skills/modern-app/references/components/action-bar.md +2 -2
  36. package/skills/modern-app/references/components/group-box.md +3 -0
  37. package/skills/modern-app/references/components/page-header.md +12 -8
  38. package/skills/modern-app/references/layout.md +24 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,70 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.34.1] - 2026-10-05
4
+
5
+ ### Fixed
6
+
7
+ - **The sidebar footer was cut off when the menu was longer than the screen** and the shell's parent
8
+ had no height of its own (a custom `root`, or `SidebarLayout` mounted from React). The shell is
9
+ capped at the viewport, but the sidebar took `height: 100%`, which cannot resolve against a cap, so
10
+ it grew to the menu's length and the shell clipped the footer out of reach. The sidebar now takes
11
+ the shell's height, the menu scrolls inside `part="sidebar-main"`, and the footer stays at the
12
+ bottom — in the default, slim, drawer and mobile states.
13
+
14
+ ### Documentation
15
+
16
+ - The layout guide now says that a custom `root` also keeps the browser's default body margin, and
17
+ shows `:where(body) { margin: 0; }` next to the height rule.
18
+
19
+ ## [0.34.0] - 2026-10-05
20
+
21
+ ### Added
22
+
23
+ - **Press targets follow `--u-target-size`** (`@iyulab/components` 2.0). When it is set, sidebar links,
24
+ buttons and group headers, the page header's back link, the shell logo and wizard steps are at least
25
+ that size (the logo image and the back link keep their visible size and layout). Unset, nothing changes.
26
+ - **`routerMode` in `app.load()`** — `'hash'` puts the route after `#` (`/app/#/orders/7`), so refreshes and
27
+ deep links work on a static host with no server fallback. Forwarded to `@iyulab/router`'s `mode`
28
+ (router 0.18.0); the default stays `'history'`.
29
+ - **`eyebrow` on `u-page-header`** — a small line above the title for the menu group or the parent
30
+ path, on the overline step. A `slot="eyebrow"` replaces the text when it needs links (a breadcrumb).
31
+ The row takes no space when neither is given (`part="eyebrow"`).
32
+ - **`description` on `u-group-box`** — one line under the title saying what the box holds or what
33
+ its numbers are based on, at caption size in the weak text color (`part="description"`). A
34
+ description alone opens the header, like `title`.
35
+ - **`--app-sidebar-active-icon-color` and `--app-sidebar-active-indicator-color`** (with
36
+ `--app-sidebar-active-indicator-width`, default `3px`) — a shell with a light active background
37
+ can color only the active icon and draw a bar on the start edge of the active menu, instead of
38
+ filling the whole item. Defaults keep today's look: the icon follows `--app-sidebar-active-fg` and
39
+ the bar matches the active background.
40
+ - **`count` on sidebar links, and `app.setNavCount(href, value)`** — a link can show the work waiting
41
+ behind it at the end of the item (`part="count"`, tabular figures). Set it in the item config or
42
+ change it while the app runs; `undefined` clears it. In the compact sidebar it stays in the
43
+ accessible name and is hidden visually.
44
+ - **Shell size tokens** — `--app-main-padding` (32px), `--app-sidebar-item-radius` (8px),
45
+ `--app-sidebar-item-gap` (12px), `--app-sidebar-item-padding` (8px 12px),
46
+ `--app-sidebar-icon-size` (20px), `--app-sidebar-active-shadow` and
47
+ `--app-sidebar-active-shadow-hover`. A theme can now set a denser navigation or a flat current item
48
+ without reaching into the shadow DOM. Defaults keep today's look.
49
+
50
+ ### Changed
51
+
52
+ - 🔴**Requires `@iyulab/components` 2.0.** Internal buttons (the shell's sidebar toggler and
53
+ overlay close, the master-detail close, the wizard's back button) use the 2.0 `appearance` vocabulary
54
+ (`appearance="plain"`, was `variant="ghost"`); with a 1.x components they render as solid buttons.
55
+ - **The routed page sits on `--u-canvas-bg-color`** (new in `@iyulab/components` 2.0 — the
56
+ application background), not on `--u-bg-color`, which is the surface components paint their own
57
+ face with. With the built-in defaults the page is a near-white `#FAFAFA`; cards and tables on it
58
+ keep their white surface.
59
+ - **`u-group-box` is a card like `u-card`**: `--u-panel-bg-color` face, `--u-border-color` line,
60
+ `--u-radius-xl` corners (was the chrome tint `--u-bg-color-raised`, a weak line and `2xl`). The page
61
+ now sits on the canvas, so a card no longer needs a tint to stand apart from it.
62
+ - **Sidebar section title and subtitle read role tokens** (`--u-txt-color-weak` · `--u-txt-color-weaker`)
63
+ instead of the neutral palette, so a theme reaches them. Same values with the built-in defaults.
64
+ - **`u-page-header`'s `subtitle` reads at body size** (was caption). It is the screen's one-line
65
+ description; at caption size it read as a footnote under a 26px title. Color stays
66
+ `--u-txt-color-weak`.
67
+
3
68
  ## [0.33.3] - 2026-10-05
4
69
 
5
70
  ### Fixed
package/dist/App.d.ts CHANGED
@@ -35,6 +35,17 @@ declare class App {
35
35
  unload(): void;
36
36
  /** 페이지 이동 */
37
37
  navigate(path: string): void;
38
+ /**
39
+ * Sets the count shown on a sidebar link — the work waiting behind it — without rebuilding the
40
+ * layout. `href` is the link's `href` as configured; `undefined` clears it (and falls back to the
41
+ * item's static `count`, if any).
42
+ *
43
+ * ```ts
44
+ * app.setNavCount('/reviews', 12);
45
+ * app.setNavCount('/reviews', undefined);
46
+ * ```
47
+ */
48
+ setNavCount(href: string, value: number | string | undefined): void;
38
49
  /** 공지 메시지 */
39
50
  notice(message: string, options?: NotificationOptions): Promise<void>;
40
51
  /** 정보 메시지 */
package/dist/App.js CHANGED
@@ -68,6 +68,7 @@ var l = class o {
68
68
  this._layout.appendChild(s), this._router = new n({
69
69
  root: this._layout,
70
70
  basepath: a.basepath,
71
+ mode: a.routerMode,
71
72
  routes: a.routes,
72
73
  fallback: a.fallback,
73
74
  enter: a.enter,
@@ -81,6 +82,12 @@ var l = class o {
81
82
  navigate(e) {
82
83
  this._router?.go(e);
83
84
  }
85
+ setNavCount(e, t) {
86
+ let n = this._layout;
87
+ if (!n || !("counts" in n)) return;
88
+ let r = { ...n.counts };
89
+ t === void 0 ? delete r[e] : r[e] = t, n.counts = r;
90
+ }
84
91
  async notice(e, t) {
85
92
  await a.notice(e, { ...t });
86
93
  }
@@ -11,8 +11,8 @@ type ElementParts = 'host' | 'danger' | 'main';
11
11
  *
12
12
  * ```html
13
13
  * <u-action-bar sticky>
14
- * <u-button slot="danger" color="danger" variant="ghost">삭제</u-button>
15
- * <u-button variant="ghost">취소</u-button>
14
+ * <u-button slot="danger" color="danger" appearance="plain">삭제</u-button>
15
+ * <u-button appearance="plain">취소</u-button>
16
16
  * <u-button color="primary">저장</u-button>
17
17
  * </u-action-bar>
18
18
  * ```
@@ -1,6 +1,6 @@
1
1
  import { StyledElement } from '../internals/StyledElement.js';
2
2
  import { default as React } from 'react';
3
- type ElementParts = 'host' | 'header' | 'title' | 'meta' | 'actions' | 'body';
3
+ type ElementParts = 'host' | 'header' | 'title' | 'meta' | 'description' | 'actions' | 'body';
4
4
  /** 제목 단계 — 페이지 제목(h1)은 `u-page-header` 몫이라 2 부터다. */
5
5
  export type GroupBoxLevel = 2 | 3 | 4 | 5 | 6;
6
6
  /**
@@ -18,7 +18,7 @@ export type GroupBoxLevel = 2 | 3 | 4 | 5 | 6;
18
18
  * </u-group-box>
19
19
  * ```
20
20
  *
21
- * 오버라이드: `part`(host·header·title·meta·actions·body) + slot 치환.
21
+ * 오버라이드: `part`(host·header·title·meta·description·actions·body) + slot 치환.
22
22
  * `divider` 속성으로 제목과 본문 사이 구분선을 켠다(기본 꺼짐 — 선이 많으면 화면이 시끄럽다).
23
23
  */
24
24
  export declare class GroupBox extends StyledElement<ElementParts> {
@@ -33,6 +33,12 @@ export declare class GroupBox extends StyledElement<ElementParts> {
33
33
  * without a `title`: it describes the title.
34
34
  */
35
35
  meta?: string;
36
+ /**
37
+ * One line under the title saying what the box holds or what its numbers are based on
38
+ * (`Prices are per unit, VAT excluded`). Caption size, weak color — it explains the title
39
+ * without competing with it. Opens the header on its own, like `title`.
40
+ */
41
+ description?: string;
36
42
  /** 제목과 본문 사이에 구분선을 넣는다. */
37
43
  divider: boolean;
38
44
  /** 본문 여백을 없앤다 — 표를 카드 가장자리까지 붙일 때. */
@@ -69,6 +75,7 @@ declare module 'react' {
69
75
  flush?: boolean;
70
76
  level?: GroupBoxLevel;
71
77
  meta?: string;
78
+ description?: string;
72
79
  };
73
80
  }
74
81
  }
@@ -3,17 +3,17 @@ import t from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
4
  import { slotHasContent as r } from "../internals/slotted.js";
5
5
  import { styles as i } from "./GroupBox.styles.js";
6
- import { html as a } from "lit";
7
- import { customElement as o, property as s, state as c } from "lit/decorators.js";
8
- import { html as l, literal as u } from "lit/static-html.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 { html as u, literal as d } from "lit/static-html.js";
9
9
  //#region src/components/GroupBox.ts
10
- var d = {
11
- 2: u`h2`,
12
- 3: u`h3`,
13
- 4: u`h4`,
14
- 5: u`h5`,
15
- 6: u`h6`
16
- }, f = class extends n {
10
+ var f = {
11
+ 2: d`h2`,
12
+ 3: d`h3`,
13
+ 4: d`h4`,
14
+ 5: d`h5`,
15
+ 6: d`h6`
16
+ }, p = class extends n {
17
17
  constructor(...e) {
18
18
  super(...e), this.title = "", this.divider = !1, this.flush = !1, this.level = 3, this.hasActions = !1;
19
19
  }
@@ -21,10 +21,13 @@ var d = {
21
21
  this.styles = [super.styles, i];
22
22
  }
23
23
  render() {
24
- let e = !!this.title || this.hasActions;
24
+ let e = !!this.title || !!this.description || this.hasActions;
25
25
  return a`
26
26
  <div class="header ${this.divider ? "divider" : ""} ${e ? "" : "empty"}" part="header">
27
- ${l`<${d[this.level] ?? d[3]} class="title" part="title">${this.title}${this.title && this.meta ? a` <span class="meta" part="meta">${this.meta}</span>` : ""}</${d[this.level] ?? d[3]}>`}
27
+ <div class="titles">
28
+ ${u`<${f[this.level] ?? f[3]} class="title" part="title">${this.title}${this.title && this.meta ? a` <span class="meta" part="meta">${this.meta}</span>` : ""}</${f[this.level] ?? f[3]}>`}
29
+ ${this.description ? a`<p class="description" part="description">${this.description}</p>` : o}
30
+ </div>
28
31
  <div class="actions ${this.hasActions ? "" : "empty"}" part="actions">
29
32
  <slot name="actions"
30
33
  @slotchange=${(e) => this.hasActions = r(e.target)}
@@ -35,9 +38,9 @@ var d = {
35
38
  `;
36
39
  }
37
40
  };
38
- t([s({ type: String }), e("design:type", Object)], f.prototype, "title", void 0), t([s({ type: String }), e("design:type", String)], f.prototype, "meta", void 0), t([s({ type: Boolean }), e("design:type", Object)], f.prototype, "divider", void 0), t([s({ type: Boolean }), e("design:type", Object)], f.prototype, "flush", void 0), t([s({
41
+ t([c({ type: String }), e("design:type", Object)], p.prototype, "title", void 0), t([c({ type: String }), e("design:type", String)], p.prototype, "meta", void 0), t([c({ type: String }), e("design:type", String)], p.prototype, "description", void 0), t([c({ type: Boolean }), e("design:type", Object)], p.prototype, "divider", void 0), t([c({ type: Boolean }), e("design:type", Object)], p.prototype, "flush", void 0), t([c({
39
42
  type: Number,
40
43
  reflect: !0
41
- }), e("design:type", Object)], f.prototype, "level", void 0), t([c(), e("design:type", Object)], f.prototype, "hasActions", void 0), f = t([o("u-group-box")], f);
44
+ }), e("design:type", Object)], p.prototype, "level", void 0), t([l(), e("design:type", Object)], p.prototype, "hasActions", void 0), p = t([s("u-group-box")], p);
42
45
  //#endregion
43
- export { f as GroupBox };
46
+ export { p as GroupBox };
@@ -7,10 +7,12 @@ var t = e`
7
7
  사이드바가 열린 1280px 화면의 좁은 본문에서도, 넓은 본문을 가진 태블릿에서도
8
8
  같은 판단을 하려면 기준이 자기 폭이어야 한다. */
9
9
  container-type: inline-size;
10
- background-color: var(--u-bg-color-raised, #FAFAFA);
11
- border: 1px solid var(--u-border-color-weak, #EEEEEE);
12
- /* 면(surface) 단 — 컨트롤과 같은 반경을 쓰면 큰 사각형이 각져 보인다. */
13
- border-radius: var(--u-radius-2xl, 12px);
10
+ /* 카드 = 떠 있는 컨테이너(--u-panel-bg-color) — u-card 와 같은 면·반경·선이다.
11
+ 페이지는 --u-canvas-bg-color 위에 있으므로 카드는 바탕보다 한 단 밝은 면으로 선다
12
+ (종전에는 페이지가 흰색이라 카드를 크롬 틴트 --u-bg-color-raised 로 칠해 구분했다). */
13
+ background-color: var(--u-panel-bg-color, #FFFFFF);
14
+ border: 1px solid var(--u-border-color, #E0E0E0);
15
+ border-radius: var(--u-radius-xl, 8px);
14
16
  box-shadow: var(--u-shadow-sm, 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 1px rgba(0, 0, 0, 0.04));
15
17
  }
16
18
 
@@ -32,9 +34,17 @@ var t = e`
32
34
  border-bottom: 1px solid var(--u-border-color-weak, #EEEEEE);
33
35
  }
34
36
 
37
+ /* 제목과 설명 한 줄을 세로로 묶는다 — 액션은 그 오른쪽에 선다. */
38
+ .titles {
39
+ flex: 1 1 auto;
40
+ min-width: 0;
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: var(--u-space-3xs, 2px);
44
+ }
45
+
35
46
  .title {
36
47
  margin: 0;
37
- flex: 1 1 auto;
38
48
  min-width: 0;
39
49
  font-size: var(--u-text-subtitle-size, 16px);
40
50
  font-weight: var(--u-text-subtitle-weight, 600);
@@ -51,6 +61,19 @@ var t = e`
51
61
  font-variant-numeric: tabular-nums;
52
62
  }
53
63
 
64
+ .description {
65
+ margin: 0;
66
+ font-size: var(--u-text-caption-size, 12px);
67
+ font-weight: var(--u-text-caption-weight, 400);
68
+ line-height: var(--u-text-caption-leading, 1.5);
69
+ color: var(--u-txt-color-weak, #616161);
70
+ }
71
+
72
+ /* 제목 없이 설명만 있으면 빈 헤딩이 자리를 잡지 않는다. */
73
+ .title:empty {
74
+ display: none;
75
+ }
76
+
54
77
  .actions {
55
78
  flex: 0 0 auto;
56
79
  display: flex;
@@ -39,7 +39,7 @@ var u = class extends n {
39
39
  <div class="master ${this.hasDetail ? "covered" : ""}" part="master"><slot></slot></div>
40
40
  <div class="divider ${this.hasDetail ? "" : "empty"}" part="divider"></div>
41
41
  <div class="detail ${this.hasDetail ? "" : "empty"}" part="detail">
42
- <u-button class="detail-close" part="detail-close" variant="ghost"
42
+ <u-button class="detail-close" part="detail-close" appearance="plain"
43
43
  aria-label=${r(this.locale || void 0).detailClose}
44
44
  @click=${this.handleDetailClose}
45
45
  >
@@ -1,6 +1,6 @@
1
1
  import { StyledElement } from '../internals/StyledElement.js';
2
2
  import { default as React } from 'react';
3
- type ElementParts = 'host' | 'back' | 'heading' | 'title' | 'subtitle' | 'status' | 'actions';
3
+ type ElementParts = 'host' | 'back' | 'heading' | 'eyebrow' | 'title' | 'subtitle' | 'status' | 'actions';
4
4
  /**
5
5
  * 페이지 헤더 — 모든 LOB 화면 최상단의 같은 골격.
6
6
  *
@@ -10,21 +10,29 @@ type ElementParts = 'host' | 'back' | 'heading' | 'title' | 'subtitle' | 'status
10
10
  * 하나로 만들어지지 않았다"* 로 읽힌다.
11
11
  *
12
12
  * ```html
13
- * <u-page-header title="주문 G-2026-I-0629" subtitle="2026-02-24 접수" back="/orders">
13
+ * <u-page-header eyebrow="주문 관리" title="주문 G-2026-I-0629" subtitle="2026-02-24 접수" back="/orders">
14
14
  * <span slot="status"><!-- 배지 --></span>
15
15
  * <span slot="actions"><!-- 버튼 --></span>
16
16
  * </u-page-header>
17
17
  * ```
18
18
  *
19
- * 오버라이드: `part`(host·back·heading·title·subtitle·status·actions) + slot 치환.
19
+ * 오버라이드: `part`(host·back·heading·eyebrow·title·subtitle·status·actions) + slot 치환.
20
20
  * 값은 전부 토큰을 경유한다 — 리터럴을 두면 소비자가 밀도를 바꿀 수 없다.
21
21
  */
22
22
  export declare class PageHeader extends StyledElement<ElementParts> {
23
23
  static styles: import('lit').CSSResultGroup[];
24
24
  /** 페이지 제목. 타입 스케일의 `display` 단을 쓴다. */
25
25
  title: string;
26
- /** 제목 아래 보조 설명. 없으면 렌더하지 않는다. */
26
+ /**
27
+ * 제목 아래 한 줄 설명 — 이 화면이 무엇을 하는지, 숫자가 무엇을 기준으로 하는지. 없으면 렌더하지 않는다.
28
+ * 본문 단(`body`)을 한 단 옅은 색으로 쓴다 — 캡션 단이면 설명이 아니라 각주로 읽힌다.
29
+ */
27
30
  subtitle?: string;
31
+ /**
32
+ * 제목 위 위치 표시 — 메뉴 그룹 이름이나 상위 경로. `overline` 단.
33
+ * 링크가 필요하면(브레드크럼) 같은 이름의 슬롯(`slot="eyebrow"`)을 쓴다 — 슬롯이 이 문자열을 대신한다.
34
+ */
35
+ eyebrow?: string;
28
36
  /**
29
37
  * 뒤로가기 링크 주소. 주면 제목 왼쪽에 `← 목록` 형태로 나온다.
30
38
  * ⚠텍스트는 `backLabel` 또는 locale 레지스트리로 바꾼다.
@@ -42,6 +50,7 @@ export declare class PageHeader extends StyledElement<ElementParts> {
42
50
  * 슬롯 배정 상태. ★CSS 로는 알 수 없다 — `<slot>` 자신이 자식이라 `:has(*)` 가
43
51
  * 항상 참이다(실브라우저로 확인). 빈 배지 자리가 남으면 제목 위치가 화면마다 달라진다.
44
52
  */
53
+ private hasEyebrow;
45
54
  private hasStatus;
46
55
  private hasActions;
47
56
  render(): import('lit-html').TemplateResult<1>;
@@ -57,6 +66,7 @@ declare module 'react' {
57
66
  'u-page-header': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
58
67
  title?: string;
59
68
  subtitle?: string;
69
+ eyebrow?: string;
60
70
  back?: string;
61
71
  'back-label'?: string;
62
72
  locale?: string;
@@ -9,7 +9,7 @@ import { customElement as c, property as l, state as u } from "lit/decorators.js
9
9
  //#region src/components/PageHeader.ts
10
10
  var d = class extends n {
11
11
  constructor(...e) {
12
- super(...e), this.title = "", this.backLabel = "", this.locale = "", this.hasStatus = !1, this.hasActions = !1;
12
+ super(...e), this.title = "", this.backLabel = "", this.locale = "", this.hasEyebrow = !1, this.hasStatus = !1, this.hasActions = !1;
13
13
  }
14
14
  static {
15
15
  this.styles = [super.styles, a];
@@ -18,6 +18,11 @@ var d = class extends n {
18
18
  return o`
19
19
  ${this.back ? o`<a class="back" part="back" href=${this.back}>${this.backLabel || r(this.locale || void 0).back}</a>` : s}
20
20
  <div class="heading" part="heading">
21
+ <div class="eyebrow ${this.eyebrow || this.hasEyebrow ? "" : "empty"}" part="eyebrow">
22
+ <slot name="eyebrow"
23
+ @slotchange=${(e) => this.hasEyebrow = i(e.target)}
24
+ >${this.eyebrow ?? s}</slot>
25
+ </div>
21
26
  <div class="title-row">
22
27
  <h1 class="title" part="title">${this.title}</h1>
23
28
  <span class="status ${this.hasStatus ? "" : "empty"}" part="status">
@@ -36,9 +41,9 @@ var d = class extends n {
36
41
  `;
37
42
  }
38
43
  };
39
- t([l({ type: String }), e("design:type", Object)], d.prototype, "title", void 0), t([l({ type: String }), e("design:type", String)], d.prototype, "subtitle", void 0), t([l({ type: String }), e("design:type", String)], d.prototype, "back", void 0), t([l({
44
+ t([l({ type: String }), e("design:type", Object)], d.prototype, "title", void 0), t([l({ type: String }), e("design:type", String)], d.prototype, "subtitle", void 0), t([l({ type: String }), e("design:type", String)], d.prototype, "eyebrow", void 0), t([l({ type: String }), e("design:type", String)], d.prototype, "back", void 0), t([l({
40
45
  type: String,
41
46
  attribute: "back-label"
42
- }), e("design:type", Object)], d.prototype, "backLabel", void 0), t([l({ type: String }), e("design:type", Object)], d.prototype, "locale", void 0), t([u(), e("design:type", Object)], d.prototype, "hasStatus", void 0), t([u(), e("design:type", Object)], d.prototype, "hasActions", void 0), d = t([c("u-page-header")], d);
47
+ }), e("design:type", Object)], d.prototype, "backLabel", void 0), t([l({ type: String }), e("design:type", Object)], d.prototype, "locale", void 0), t([u(), e("design:type", Object)], d.prototype, "hasEyebrow", void 0), t([u(), e("design:type", Object)], d.prototype, "hasStatus", void 0), t([u(), e("design:type", Object)], d.prototype, "hasActions", void 0), d = t([c("u-page-header")], d);
43
48
  //#endregion
44
49
  export { d as PageHeader };
@@ -34,12 +34,28 @@ var t = e`
34
34
 
35
35
  .subtitle {
36
36
  margin: var(--u-space-2xs, 4px) 0 0;
37
- font-size: var(--u-text-caption-size, 12px);
38
- font-weight: var(--u-text-caption-weight, 400);
39
- line-height: var(--u-text-caption-leading, 1.5);
37
+ font-size: var(--u-text-body-size, 14px);
38
+ font-weight: var(--u-text-body-weight, 400);
39
+ line-height: var(--u-text-body-leading, 1.6);
40
40
  color: var(--u-txt-color-weak, #616161);
41
41
  }
42
42
 
43
+ /* 제목 위 위치 표시 — 제목보다 네 단 작고 옅다. 링크(브레드크럼)는 상속받은 색으로 선다. */
44
+ .eyebrow {
45
+ margin: 0 0 var(--u-space-2xs, 4px);
46
+ font-size: var(--u-text-overline-size, 11px);
47
+ font-weight: var(--u-text-overline-weight, 700);
48
+ line-height: var(--u-text-overline-leading, 1.45);
49
+ letter-spacing: var(--u-text-overline-tracking, 0.06em);
50
+ color: var(--u-txt-color-weak, #616161);
51
+ }
52
+ .eyebrow ::slotted(*) {
53
+ color: inherit;
54
+ }
55
+ .eyebrow.empty {
56
+ display: none;
57
+ }
58
+
43
59
  .back {
44
60
  flex: 0 0 auto;
45
61
  align-self: center;
@@ -52,9 +68,11 @@ var t = e`
52
68
  ⚠고정 패딩(4px)이면 안 된다: line-height: normal 의 높이는 글꼴 메트릭이 정해서 글꼴에 따라 17px(→25)도
53
69
  15px(→23)도 된다. 줄 상자가 이미 24px 이상이면 패딩은 0 이다. */
54
70
  display: inline-block;
55
- --_back-pad: max(0px, calc((24px - 1lh) / 2));
71
+ /* 호스트 하한(--u-target-size)이 있으면 24px 대신 그 값까지 — 폭도 그 값 이상. */
72
+ --_back-pad: max(0px, calc((max(24px, var(--u-target-size, 0px)) - 1lh) / 2));
56
73
  padding-block: var(--_back-pad);
57
74
  margin-block: calc(-1 * var(--_back-pad));
75
+ min-width: var(--u-target-size, 0px);
58
76
  }
59
77
  .back::before {
60
78
  content: '←';
@@ -28,6 +28,8 @@ var t = e`
28
28
  justify-content: flex-start;
29
29
  gap: 12px;
30
30
  padding: 8px 12px;
31
+ /* 호스트 하한(--u-target-size, 미설정 = 0) — 상자는 border-box(기반 스타일)라 여백을 포함한다. */
32
+ min-height: var(--u-target-size, 0px);
31
33
  }
32
34
  button[compact] {
33
35
  justify-content: center;
@@ -19,6 +19,8 @@ var t = e`
19
19
  justify-content: space-between;
20
20
  gap: 12px;
21
21
  padding: 8px 12px;
22
+ /* 호스트 하한(--u-target-size, 미설정 = 0) — 상자는 border-box(기반 스타일)라 여백을 포함한다. */
23
+ min-height: var(--u-target-size, 0px);
22
24
  background-color: transparent;
23
25
  border: none;
24
26
  border-radius: 8px;
@@ -1,7 +1,7 @@
1
1
  import { DirectiveResult } from 'lit/directive.js';
2
2
  import { StyledElement, StyleMap } from '../internals/StyledElement.js';
3
3
  import { SidebarPermissionGuard } from '../layouts/SidebarPermission.js';
4
- type ElementParts = 'host' | 'base' | 'icon' | 'label';
4
+ type ElementParts = 'host' | 'base' | 'icon' | 'label' | 'count';
5
5
  /** 링크 항목 디폴트 타입 */
6
6
  export interface SidebarLinkConfig extends SidebarPermissionGuard {
7
7
  type: 'link';
@@ -30,6 +30,14 @@ export interface SidebarLinkConfig extends SidebarPermissionGuard {
30
30
  * 다른 문서로」를 표현할 수 없다. 그것이 종전에 유일한 탈출구였고, 그래서 부족했다.
31
31
  */
32
32
  target?: '_self' | '_blank';
33
+ /**
34
+ * A count shown at the end of the item — work waiting behind it (items to review, unpaid
35
+ * invoices). Omit it, or pass `undefined`, for items with nothing waiting; show numbers only
36
+ * where they ask for action, or the sidebar turns into a dashboard.
37
+ *
38
+ * The value can also be set later without rebuilding the layout: `app.setNavCount(href, n)`.
39
+ */
40
+ count?: number | string;
33
41
  }
34
42
  /**
35
43
  * SidebarLink 컴포넌트는 계층형 네비게이션 아이템을 표시합니다.
@@ -55,6 +63,8 @@ export declare class SidebarLink extends StyledElement<ElementParts> {
55
63
  navigate?: 'router' | 'document';
56
64
  /** 앵커 target — 새 탭으로 열 때 */
57
65
  target?: '_self' | '_blank';
66
+ /** 항목 끝의 건수 — 처리할 일이 있을 때만. `undefined`·빈 문자열이면 그리지 않는다. */
67
+ count?: number | string;
58
68
  render(): import('lit-html').TemplateResult<1>;
59
69
  }
60
70
  export {};
@@ -33,6 +33,7 @@ var u = class extends n {
33
33
  <span part="label" ?compact=${this.compact}>
34
34
  ${this.label}
35
35
  </span>
36
+ ${this.count === void 0 || this.count === null || this.count === "" ? s : o`<span class="count" part="count" ?compact=${this.compact}>${this.count}</span>`}
36
37
  ${this.target === "_blank" ? o`<span class="new-tab-hint">${r().opensInNewTab}</span>` : s}
37
38
  </div>
38
39
  </u-link>
@@ -42,6 +43,6 @@ var u = class extends n {
42
43
  t([l({
43
44
  type: Boolean,
44
45
  reflect: !0
45
- }), e("design:type", Object)], u.prototype, "selected", void 0), t([l({ type: Boolean }), e("design:type", Object)], u.prototype, "compact", 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), t([l({ type: String }), e("design:type", String)], u.prototype, "href", void 0), t([l({ type: String }), e("design:type", Object)], u.prototype, "pattern", void 0), t([l({ type: String }), e("design:type", String)], u.prototype, "navigate", void 0), t([l({ type: String }), e("design:type", String)], u.prototype, "target", void 0), u = t([c("u-sidebar-link")], u);
46
+ }), e("design:type", Object)], u.prototype, "selected", void 0), t([l({ type: Boolean }), e("design:type", Object)], u.prototype, "compact", 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), t([l({ type: String }), e("design:type", String)], u.prototype, "href", void 0), t([l({ type: String }), e("design:type", Object)], u.prototype, "pattern", void 0), t([l({ type: String }), e("design:type", String)], u.prototype, "navigate", void 0), t([l({ type: String }), e("design:type", String)], u.prototype, "target", void 0), t([l(), e("design:type", Object)], u.prototype, "count", void 0), u = t([c("u-sidebar-link")], u);
46
47
  //#endregion
47
48
  export { u as SidebarLink };
@@ -11,7 +11,7 @@ var t = e`
11
11
  display: block;
12
12
  color: var(--app-sidebar-fg, var(--u-txt-color, #212121));
13
13
  background-color: transparent;
14
- border-radius: 8px;
14
+ border-radius: var(--app-sidebar-item-radius, 8px);
15
15
  transition: all var(--u-duration-normal, 220ms) ease;
16
16
  cursor: pointer;
17
17
  }
@@ -22,21 +22,25 @@ var t = e`
22
22
  :host([selected]) {
23
23
  color: var(--app-sidebar-active-fg, var(--u-txt-color-inverse, #FFFFFF));
24
24
  background-color: var(--link-active-bg);
25
- box-shadow: 0 1px 3px var(--u-shadow-color-weak, rgba(0, 0, 0, 0.08));
25
+ box-shadow: var(--app-sidebar-active-shadow, 0 1px 3px var(--u-shadow-color-weak, rgba(0, 0, 0, 0.08)));
26
26
  }
27
27
  :host([selected]:hover) {
28
28
  color: var(--app-sidebar-active-fg, var(--u-txt-color-inverse, #FFFFFF));
29
29
  background-color: color-mix(in srgb, var(--link-active-bg) 85%, black);
30
- box-shadow: 0 2px 6px var(--u-shadow-color-normal, rgba(0, 0, 0, 0.12));
30
+ box-shadow: var(--app-sidebar-active-shadow-hover, 0 2px 6px var(--u-shadow-color-normal, rgba(0, 0, 0, 0.12)));
31
31
  }
32
32
 
33
33
  .container {
34
+ position: relative;
34
35
  display: flex;
35
36
  flex-direction: row;
36
37
  align-items: center;
37
38
  justify-content: flex-start;
38
- gap: 12px;
39
- padding: 8px 12px;
39
+ gap: var(--app-sidebar-item-gap, 12px);
40
+ padding: var(--app-sidebar-item-padding, 8px 12px);
41
+ /* 호스트 하한(--u-target-size, 미설정 = 0). */
42
+ box-sizing: border-box;
43
+ min-height: var(--u-target-size, 0px);
40
44
  }
41
45
  .container[compact] {
42
46
  justify-content: center;
@@ -47,12 +51,24 @@ var t = e`
47
51
  u-icon {
48
52
  flex-shrink: 0;
49
53
  color: var(--link-icon-color, inherit);
50
- font-size: 20px;
54
+ font-size: var(--app-sidebar-icon-size, 20px);
51
55
  }
52
56
  /* Selected always wins over any consumer-supplied --link-icon-color — a
53
57
  * high-contrast icon on the active background matters more than a brand tint. */
54
58
  :host([selected]) u-icon {
55
- color: var(--app-sidebar-active-fg, var(--u-txt-color-inverse, #FFFFFF));
59
+ color: var(--app-sidebar-active-icon-color, var(--app-sidebar-active-fg, var(--u-txt-color-inverse, #FFFFFF)));
60
+ }
61
+
62
+ /* 활성 표시 막대 — 시작 변(좌→우 언어에서 왼쪽). 기본 색은 활성 면과 같아 보이지 않는다(기본 외형 불변).
63
+ 밝은 활성 면처럼 면 색만으로 현재 위치를 말하지 않는 셸이 위치를 한 번 더 표시하는 자리다. */
64
+ :host([selected]) .container::before {
65
+ content: '';
66
+ position: absolute;
67
+ inset-block: 6px;
68
+ inset-inline-start: 0;
69
+ width: var(--app-sidebar-active-indicator-width, 3px);
70
+ border-radius: var(--u-radius-pill, 9999px);
71
+ background: var(--app-sidebar-active-indicator-color, var(--app-sidebar-active-bg, var(--u-primary-color, #1976D2)));
56
72
  }
57
73
 
58
74
  /*
@@ -87,6 +103,26 @@ var t = e`
87
103
  white-space: nowrap;
88
104
  border: 0;
89
105
  }
106
+
107
+ /* 건수 — 라벨 끝, 숫자는 표 숫자처럼 자릿수가 흔들리지 않게. 접힌 사이드바에서는 라벨처럼
108
+ 시각적으로만 숨긴다(접근 가능한 이름에는 남는다). */
109
+ .count {
110
+ flex: none;
111
+ margin-inline-start: auto;
112
+ font-size: var(--u-text-overline-size, 11px);
113
+ font-weight: var(--u-text-caption-weight, 400);
114
+ font-variant-numeric: tabular-nums;
115
+ opacity: 0.8;
116
+ }
117
+ .count[compact] {
118
+ position: absolute;
119
+ width: 1px;
120
+ height: 1px;
121
+ margin: -1px;
122
+ overflow: hidden;
123
+ clip-path: inset(50%);
124
+ white-space: nowrap;
125
+ }
90
126
  `;
91
127
  //#endregion
92
128
  export { t as styles };
@@ -14,19 +14,16 @@ var t = e`
14
14
  padding: 8px 12px 4px;
15
15
  }
16
16
 
17
- /* ⚠아래 두 색만 팔레트를 직접 읽는다 — 시맨틱 토큰으로 옮기면 대비가 무너지기 때문이다.
18
- 섹션 제목·부제는 본문(neutral-800)보다 약한 3단 위계를 이루는데, 역할 층의 보조
19
- 텍스트 단은 --u-txt-color-weak 하나뿐이고 그 값은 흰 배경에서 2.68:1 이라
20
- WCAG AA(4.5:1)에 미달한다. 현행 두 값은 6.19:1 · 4.61:1 로 통과한다.
21
- ⇒ 역할 층에 AA 를 넘는 보조 텍스트 단이 생기면 그때 옮긴다. */
17
+ /* 섹션 제목·부제는 본문보다 약한 두 단이다 — 제목 = 보조 글자(--u-txt-color-weak),
18
+ 부제 = 3단 글자(--u-txt-color-weaker, components 2.0). 종전에는 역할 층에 AA 를 넘는
19
+ 3단이 없어 팔레트(neutral-700/600)를 직접 읽었고, 그래서 테마가 이 두 줄에 닿지 않았다. */
22
20
  /*
23
- * ★크기·굵기·자간은 스케일의 단으로 옮겼다(용도 배정). 위 주석은 **색**에
24
- * 대한 것이고 그대로 유효하다: 두 색은 여전히 팔레트를 직접 읽는다.
21
+ * ★크기·굵기·자간은 스케일의 단으로 옮겼다(용도 배정).
25
22
  * ⚠ overline 에 대문자 변환을 붙이지 않는다 — 이 줄에 ellipsis 가 있어 잘림이 늘고,
26
23
  * 섹션 이름은 소비자가 쓴 글자다(CJK 에는 효과도 없다).
27
24
  */
28
25
  .title {
29
- color: var(--u-neutral-700, #616161);
26
+ color: var(--u-txt-color-weak, #616161);
30
27
  font-size: var(--u-text-overline-size, 11px);
31
28
  font-weight: var(--u-text-overline-weight, 700);
32
29
  line-height: var(--u-text-overline-leading, 1.45);
@@ -37,7 +34,7 @@ var t = e`
37
34
  }
38
35
 
39
36
  .subtitle {
40
- color: var(--u-neutral-600, #757575);
37
+ color: var(--u-txt-color-weaker, #757575);
41
38
  font-size: var(--u-text-caption-size, 12px);
42
39
  font-weight: var(--u-text-caption-weight, 400);
43
40
  line-height: var(--u-text-caption-leading, 1.5);
@@ -92,7 +92,7 @@ var u = class extends n {
92
92
  </div>
93
93
  <div class="actions" part="actions">
94
94
  <slot name="actions">
95
- <u-button variant="ghost" ?disabled=${this.active === 0} @click=${() => this.back()}>
95
+ <u-button appearance="plain" ?disabled=${this.active === 0} @click=${() => this.back()}>
96
96
  ${e.wizardBack}
97
97
  </u-button>
98
98
  ${t ? o : a`
@@ -39,6 +39,8 @@ var t = e`
39
39
  background: none;
40
40
  border: none;
41
41
  padding: var(--u-space-2xs, 4px) 0;
42
+ /* 호스트 하한(--u-target-size, 미설정 = 0). */
43
+ min-height: var(--u-target-size, 0px);
42
44
  font: inherit;
43
45
  color: var(--u-txt-color-weak, #616161);
44
46
  cursor: pointer;
@@ -9,6 +9,11 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
9
9
  state: SidebarState;
10
10
  /** 사이드바 레이아웃 설정 */
11
11
  config?: SidebarLayoutConfig;
12
+ /**
13
+ * Live counts for link items, keyed by the item's `href` — overrides the static `count` of the
14
+ * item config. Set through `app.setNavCount(href, value)`; assigning a new object re-renders.
15
+ */
16
+ counts: Record<string, number | string | undefined>;
12
17
  /** 크롬 문자열(토글 버튼 접근성 라벨 등) 로케일 — 이 요소만 다른 언어로 쓸 때. 비우면 `Locale` 의 활성 로케일 */
13
18
  locale: string;
14
19
  progressBarEl: UProgressBar;
@@ -59,7 +59,7 @@ var j = class extends i {
59
59
  C = this;
60
60
  }
61
61
  constructor(...t) {
62
- super(...t), this.state = "default", this.locale = "", this.hasOverlay = !1, this.hasNotice = !1, this.overlayTrigger = null, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.context = null, this.unsizedWarnKey = `unsized:${++T}`, this.isMatchedLink = (e) => !this.context || !e ? !1 : (e = typeof e == "string" ? new URLPattern(e, window.location.origin) : e, e.test(this.context.path, window.location.origin)), this.handleToggleButtonClick = () => {
62
+ super(...t), this.state = "default", this.counts = {}, this.locale = "", this.hasOverlay = !1, this.hasNotice = !1, this.overlayTrigger = null, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.context = null, this.unsizedWarnKey = `unsized:${++T}`, this.isMatchedLink = (e) => !this.context || !e ? !1 : (e = typeof e == "string" ? new URLPattern(e, window.location.origin) : e, e.test(this.context.path, window.location.origin)), this.handleToggleButtonClick = () => {
63
63
  let t = this.screenSize ?? e.screen ?? "large";
64
64
  t === "large" ? this.state = this.state === "default" ? "slim" : "default" : t === "medium" ? this.state = this.state === "slim" ? "modal" : "slim" : t === "small" ? this.state = this.state === "mobile" ? "mobile-open" : "mobile" : console.warn("Unknown screen size:", t);
65
65
  }, this.handleBackdropClick = () => {
@@ -171,7 +171,7 @@ var j = class extends i {
171
171
  <span class="title">
172
172
  ${this.config.title}
173
173
  </span>
174
- <u-button class="toggler" variant="ghost"
174
+ <u-button class="toggler" appearance="plain"
175
175
  aria-label=${a(this.locale || void 0).toggleMobileMenu}
176
176
  @click=${this.handleToggleButtonClick}>
177
177
  <u-icon
@@ -189,7 +189,7 @@ var j = class extends i {
189
189
  <span class="title" ?hidden=${this.state === "slim"}>
190
190
  ${this.config.title}
191
191
  </span>
192
- <u-button class="toggler" variant="ghost"
192
+ <u-button class="toggler" appearance="plain"
193
193
  aria-label=${a(this.locale || void 0).toggleSidebar}
194
194
  @click=${this.handleToggleButtonClick}>
195
195
  <u-icon
@@ -227,7 +227,7 @@ var j = class extends i {
227
227
  </main>
228
228
 
229
229
  <div class="overlay ${this.hasOverlay ? "" : "empty"}" part="overlay">
230
- <u-button class="overlay-close" part="overlay-close" variant="ghost"
230
+ <u-button class="overlay-close" part="overlay-close" appearance="plain"
231
231
  aria-label=${a(this.locale || void 0).detailClose}
232
232
  @click=${this.handleOverlayClose}>
233
233
  <u-icon lib=${this.icon("lib")} name=${this.icon("overlayClose")}></u-icon>
@@ -272,6 +272,7 @@ var j = class extends i {
272
272
  .pattern="${e.pattern}"
273
273
  .navigate="${e.navigate}"
274
274
  .target="${e.target}"
275
+ .count=${e.href in this.counts ? this.counts[e.href] : e.count}
275
276
  .styles="${e.styles}"
276
277
  ></u-sidebar-link>
277
278
  `;
@@ -377,6 +378,6 @@ var j = class extends i {
377
378
  r([f({
378
379
  type: String,
379
380
  reflect: !0
380
- }), n("design:type", Object)], j.prototype, "state", void 0), r([f({ type: Object }), n("design:type", Object)], j.prototype, "config", void 0), r([f({ type: String }), n("design:type", Object)], j.prototype, "locale", void 0), r([p("u-progress-bar"), n("design:type", S === void 0 ? Object : S)], j.prototype, "progressBarEl", void 0), r([m(), n("design:type", Object)], j.prototype, "hasOverlay", void 0), r([m(), n("design:type", Object)], j.prototype, "hasNotice", void 0), r([m(), n("design:type", Object)], j.prototype, "context", void 0), j = C = r([d("u-sidebar-layout")], j);
381
+ }), n("design:type", Object)], j.prototype, "state", void 0), r([f({ type: Object }), n("design:type", Object)], j.prototype, "config", void 0), r([f({ type: Object }), n("design:type", typeof Record > "u" ? Object : Record)], j.prototype, "counts", void 0), r([f({ type: String }), n("design:type", Object)], j.prototype, "locale", void 0), r([p("u-progress-bar"), n("design:type", S === void 0 ? Object : S)], j.prototype, "progressBarEl", void 0), r([m(), n("design:type", Object)], j.prototype, "hasOverlay", void 0), r([m(), n("design:type", Object)], j.prototype, "hasNotice", void 0), r([m(), n("design:type", Object)], j.prototype, "context", void 0), j = C = r([d("u-sidebar-layout")], j);
381
382
  //#endregion
382
383
  export { j as SidebarLayout };
@@ -32,6 +32,12 @@ var t = e`
32
32
  height: 24px;
33
33
  width: auto;
34
34
  object-fit: contain;
35
+ /* 호스트 하한(--u-target-size) — 그림은 24px 그대로, 누르는 상자만 여백으로 넓히고 같은 만큼 되돌려 배치는 그대로.
36
+ 미설정이면 여백 0(종전). */
37
+ --_logo-pad: max(0px, calc((var(--u-target-size, 0px) - 24px) / 2));
38
+ box-sizing: content-box;
39
+ padding: var(--_logo-pad);
40
+ margin: calc(-1 * var(--_logo-pad));
35
41
  }
36
42
  /* 로고를 감싼 링크 — 인라인 기준선 여백이 생기지 않게 로고 상자에 딱 맞춘다. */
37
43
  .logo-link {
@@ -58,14 +64,14 @@ var t = e`
58
64
  }
59
65
 
60
66
  /*
61
- * 토글러의 "테두리·배경 없음"은 마크업의 variant="ghost" 가 낸다.
67
+ * 토글러의 "테두리·배경 없음"은 마크업의 appearance="plain" 이 낸다.
62
68
  *
63
69
  * ★여기에 border/background 를 적는 것으로는 부족했다 — 이 규칙은 <u-button> **호스트**에만
64
70
  * 닿고, 테두리를 실제로 그리는 것은 그 섀도 루트 안의 <button> 이다(solid 기본값이
65
71
  * --btn-border-color: var(--btn-color) 로 그린다). 호스트만 재면 0px 이라 보이지도 않았다.
66
72
  * variant 를 명시해 컴포넌트가 스스로 투명하게 그리도록 한다.
67
73
  *
68
- * color: inherit 는 유지한다 — 헤더 색을 따라가게 하려는 의도이고 ghost 의 기본 전경
74
+ * color: inherit 는 유지한다 — 헤더 색을 따라가게 하려는 의도이고 plain 의 기본 전경
69
75
  * (--u-txt-color)보다 이 레이아웃의 뜻이 앞선다.
70
76
  */
71
77
  .toggler {
@@ -114,7 +120,12 @@ var t = e`
114
120
  flex-direction: column;
115
121
  justify-content: space-between;
116
122
  gap: 8px;
117
- height: 100%;
123
+ /* 높이는 백분율이 아니라 flex 의 stretch 로 받는다 — 셸의 높이가 «부모의 확정 높이» 가 아니라
124
+ max-height 상한에서 나오면 height: 100% 는 풀리지 않아(auto) 사이드바가 메뉴 길이로 커지고,
125
+ 셸의 overflow: hidden 이 아래쪽 계정 영역을 잘라 닿을 수 없게 된다. 늘어난 항목의 높이는
126
+ 그 경우에도 셸의 (상한으로 잘린) 높이다. .main-region 과 같은 해법이다. 떠 있는 상태
127
+ (modal · mobile*)는 절대배치라 top/bottom 으로 채운다. */
128
+ min-height: 0;
118
129
  /* --u-panel-bg-color 가 아니라 --u-bg-color-raised — 사이드바는 카드·메뉴류의
119
130
  "떠 있는 패널"이 아니라 상시 크롬이고, 그 구분이 정확히 두 토큰이 갈리는 지점이다
120
131
  (light.css 293~298행 참조). 미설정 시 페이지(--u-bg-color)와 같은 흰색으로 앉아
@@ -140,11 +151,13 @@ var t = e`
140
151
  width: var(--app-sidebar-width, 260px);
141
152
  position: absolute;
142
153
  top: 0;
154
+ bottom: 0;
143
155
  left: 0;
144
156
  }
145
157
  .sidebar[state="mobile"] {
146
158
  position: absolute;
147
159
  top: 0;
160
+ bottom: 0;
148
161
  left: 0;
149
162
  right: 0;
150
163
  transform: translateY(-100%);
@@ -152,6 +165,7 @@ var t = e`
152
165
  .sidebar[state="mobile-open"] {
153
166
  position: absolute;
154
167
  top: 0;
168
+ bottom: 0;
155
169
  left: 0;
156
170
  right: 0;
157
171
  transform: translateY(0);
@@ -188,6 +202,8 @@ var t = e`
188
202
  /* Sidebar Main Menu */
189
203
  .sidebar-main {
190
204
  flex: 1;
205
+ /* 열 방향 flex 항목의 자동 최소 높이는 내용 길이다 — 0 으로 풀어야 남는 높이 안에서 스크롤한다. */
206
+ min-height: 0;
191
207
  display: flex;
192
208
  flex-direction: column;
193
209
  gap: 4px;
@@ -237,12 +253,14 @@ var t = e`
237
253
  flex-direction: column;
238
254
  }
239
255
 
256
+ /* The page sits on the canvas (the application background), not on a surface: cards and tables
257
+ placed on it keep \`--u-bg-color\` as their own face. */
240
258
  .main {
241
259
  position: relative;
242
260
  flex: 1 1 auto;
243
261
  min-height: 0;
244
- padding: var(--u-space-3xl, 32px);
245
- background: var(--u-bg-color, #FFFFFF);
262
+ padding: var(--app-main-padding, var(--u-space-3xl, 32px));
263
+ background: var(--u-canvas-bg-color, #F5F5F5);
246
264
  overflow: auto;
247
265
  outline: none;
248
266
  }
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
2
  import { ActionBar as ActionBarElement } from '@iyulab/modern-app/dist/components/ActionBar.js';
3
3
 
4
+ type ActionBarOwnProps = Omit<ActionBarElement, keyof HTMLElement>;
5
+
4
6
  export declare const ActionBar: React.ForwardRefExoticComponent<
5
- Omit<Partial<ActionBarElement>, keyof React.HTMLAttributes<ActionBarElement>>
6
- & Omit<React.HTMLAttributes<ActionBarElement>, never>
7
+ Partial<Omit<ActionBarElement, Exclude<keyof React.HTMLAttributes<ActionBarElement>, keyof ActionBarOwnProps>>>
8
+ & Omit<React.HTMLAttributes<ActionBarElement>, keyof ActionBarOwnProps | never>
7
9
  & React.RefAttributes<ActionBarElement>
8
10
  & {
9
11
  }
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
2
  import { EmptyState as EmptyStateElement } from '@iyulab/modern-app/dist/components/EmptyState.js';
3
3
 
4
+ type EmptyStateOwnProps = Omit<EmptyStateElement, keyof HTMLElement>;
5
+
4
6
  export declare const EmptyState: React.ForwardRefExoticComponent<
5
- Omit<Partial<EmptyStateElement>, keyof React.HTMLAttributes<EmptyStateElement>>
6
- & Omit<React.HTMLAttributes<EmptyStateElement>, never>
7
+ Partial<Omit<EmptyStateElement, Exclude<keyof React.HTMLAttributes<EmptyStateElement>, keyof EmptyStateOwnProps>>>
8
+ & Omit<React.HTMLAttributes<EmptyStateElement>, keyof EmptyStateOwnProps | never>
7
9
  & React.RefAttributes<EmptyStateElement>
8
10
  & {
9
11
  }
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
2
  import { GroupBox as GroupBoxElement } from '@iyulab/modern-app/dist/components/GroupBox.js';
3
3
 
4
+ type GroupBoxOwnProps = Omit<GroupBoxElement, keyof HTMLElement>;
5
+
4
6
  export declare const GroupBox: React.ForwardRefExoticComponent<
5
- Omit<Partial<GroupBoxElement>, keyof React.HTMLAttributes<GroupBoxElement>>
6
- & Omit<React.HTMLAttributes<GroupBoxElement>, never>
7
+ Partial<Omit<GroupBoxElement, Exclude<keyof React.HTMLAttributes<GroupBoxElement>, keyof GroupBoxOwnProps>>>
8
+ & Omit<React.HTMLAttributes<GroupBoxElement>, keyof GroupBoxOwnProps | never>
7
9
  & React.RefAttributes<GroupBoxElement>
8
10
  & {
9
11
  }
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
2
  import { InfoField as InfoFieldElement } from '@iyulab/modern-app/dist/components/InfoField.js';
3
3
 
4
+ type InfoFieldOwnProps = Omit<InfoFieldElement, keyof HTMLElement>;
5
+
4
6
  export declare const InfoField: React.ForwardRefExoticComponent<
5
- Omit<Partial<InfoFieldElement>, keyof React.HTMLAttributes<InfoFieldElement>>
6
- & Omit<React.HTMLAttributes<InfoFieldElement>, never>
7
+ Partial<Omit<InfoFieldElement, Exclude<keyof React.HTMLAttributes<InfoFieldElement>, keyof InfoFieldOwnProps>>>
8
+ & Omit<React.HTMLAttributes<InfoFieldElement>, keyof InfoFieldOwnProps | never>
7
9
  & React.RefAttributes<InfoFieldElement>
8
10
  & {
9
11
  }
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
2
  import { InfoSection as InfoSectionElement } from '@iyulab/modern-app/dist/components/InfoSection.js';
3
3
 
4
+ type InfoSectionOwnProps = Omit<InfoSectionElement, keyof HTMLElement>;
5
+
4
6
  export declare const InfoSection: React.ForwardRefExoticComponent<
5
- Omit<Partial<InfoSectionElement>, keyof React.HTMLAttributes<InfoSectionElement>>
6
- & Omit<React.HTMLAttributes<InfoSectionElement>, never>
7
+ Partial<Omit<InfoSectionElement, Exclude<keyof React.HTMLAttributes<InfoSectionElement>, keyof InfoSectionOwnProps>>>
8
+ & Omit<React.HTMLAttributes<InfoSectionElement>, keyof InfoSectionOwnProps | never>
7
9
  & React.RefAttributes<InfoSectionElement>
8
10
  & {
9
11
  }
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
2
  import { MasterDetailLayout as MasterDetailLayoutElement } from '@iyulab/modern-app/dist/components/MasterDetailLayout.js';
3
3
 
4
+ type MasterDetailLayoutOwnProps = Omit<MasterDetailLayoutElement, keyof HTMLElement>;
5
+
4
6
  export declare const MasterDetailLayout: React.ForwardRefExoticComponent<
5
- Omit<Partial<MasterDetailLayoutElement>, keyof React.HTMLAttributes<MasterDetailLayoutElement>>
6
- & Omit<React.HTMLAttributes<MasterDetailLayoutElement>, 'onDetailClose'>
7
+ Partial<Omit<MasterDetailLayoutElement, Exclude<keyof React.HTMLAttributes<MasterDetailLayoutElement>, keyof MasterDetailLayoutOwnProps>>>
8
+ & Omit<React.HTMLAttributes<MasterDetailLayoutElement>, keyof MasterDetailLayoutOwnProps | 'onDetailClose'>
7
9
  & React.RefAttributes<MasterDetailLayoutElement>
8
10
  & {
9
11
  onDetailClose?: (event: CustomEvent) => void;
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
2
  import { PageHeader as PageHeaderElement } from '@iyulab/modern-app/dist/components/PageHeader.js';
3
3
 
4
+ type PageHeaderOwnProps = Omit<PageHeaderElement, keyof HTMLElement>;
5
+
4
6
  export declare const PageHeader: React.ForwardRefExoticComponent<
5
- Omit<Partial<PageHeaderElement>, keyof React.HTMLAttributes<PageHeaderElement>>
6
- & Omit<React.HTMLAttributes<PageHeaderElement>, never>
7
+ Partial<Omit<PageHeaderElement, Exclude<keyof React.HTMLAttributes<PageHeaderElement>, keyof PageHeaderOwnProps>>>
8
+ & Omit<React.HTMLAttributes<PageHeaderElement>, keyof PageHeaderOwnProps | never>
7
9
  & React.RefAttributes<PageHeaderElement>
8
10
  & {
9
11
  }
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
2
  import { SidebarButton as SidebarButtonElement } from '@iyulab/modern-app/dist/components/SidebarButton.js';
3
3
 
4
+ type SidebarButtonOwnProps = Omit<SidebarButtonElement, keyof HTMLElement>;
5
+
4
6
  export declare const SidebarButton: React.ForwardRefExoticComponent<
5
- Omit<Partial<SidebarButtonElement>, keyof React.HTMLAttributes<SidebarButtonElement>>
6
- & Omit<React.HTMLAttributes<SidebarButtonElement>, never>
7
+ Partial<Omit<SidebarButtonElement, Exclude<keyof React.HTMLAttributes<SidebarButtonElement>, keyof SidebarButtonOwnProps>>>
8
+ & Omit<React.HTMLAttributes<SidebarButtonElement>, keyof SidebarButtonOwnProps | never>
7
9
  & React.RefAttributes<SidebarButtonElement>
8
10
  & {
9
11
  }
@@ -33,6 +33,13 @@ export interface AppConfig {
33
33
  * @default '/'
34
34
  */
35
35
  basepath?: string;
36
+ /**
37
+ * 라우트를 주소의 어디에서 읽는가 — `@iyulab/router` 의 `mode` 로 그대로 전달된다.
38
+ * - `'history'`: 경로(`/app/orders/7`). 서버가 앱의 모든 경로에 같은 문서를 돌려줘야 새로고침·딥링크가 동작한다.
39
+ * - `'hash'`: `#` 뒤(`/app/#/orders/7`). 정적 호스팅에서 서버 설정 없이 딥링크가 동작한다.
40
+ * @default 'history'
41
+ */
42
+ routerMode?: 'history' | 'hash';
36
43
  /**
37
44
  * 현재 애플리케이션의 기본 아이콘 경로 설정
38
45
  * @default '/assets/icons/'
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.33.3",
4
+ "version": "0.34.1",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -50,6 +50,7 @@
50
50
  },
51
51
  "scripts": {
52
52
  "preversion": "node -e \"if(require('fs').existsSync('../../scripts/preversion-check.mjs'))require('child_process').execFileSync('node',['../../scripts/preversion-check.mjs'],{stdio:'inherit'})\"",
53
+ "pretest": "node -e \"if(require('fs').existsSync('../../scripts/sibling-dist-check.js'))require('child_process').execFileSync('node',['../../scripts/sibling-dist-check.js'],{stdio:'inherit'})\"",
53
54
  "test": "vitest run",
54
55
  "test:watch": "vitest",
55
56
  "start": "vite",
@@ -59,12 +60,12 @@
59
60
  "test:react-types": "tsc -p tsconfig.react-smoke.json"
60
61
  },
61
62
  "dependencies": {
62
- "@iyulab/router": "^0.17.0",
63
+ "@iyulab/router": "^0.19.0",
63
64
  "i18next": "^26.3.6",
64
65
  "lit": "^3.3.3"
65
66
  },
66
67
  "peerDependencies": {
67
- "@iyulab/components": ">=1.49.0",
68
+ "@iyulab/components": ">=2.0.0",
68
69
  "@lit/react": "^1.0.8",
69
70
  "react": "^18.0.0 || ^19.0.0"
70
71
  },
@@ -77,7 +78,7 @@
77
78
  }
78
79
  },
79
80
  "devDependencies": {
80
- "@iyulab/components": ">=1.49.0",
81
+ "@iyulab/components": ">=2.0.0",
81
82
  "@lit/react": "^1.0.8",
82
83
  "@types/node": "^26.1.1",
83
84
  "@types/react": "^19.2.14",
@@ -4,7 +4,6 @@ description: Client-side SPA framework built on Lit Element. Bootstraps an appli
4
4
  license: MIT
5
5
  metadata:
6
6
  author: iyulab
7
- version: "0.3.4"
8
7
  compatibility: Designed for Lit Element / TypeScript projects. Requires @iyulab/modern-app.
9
8
  ---
10
9
 
@@ -10,6 +10,9 @@ interface AppConfig {
10
10
  /** Base path for all routes. Default: '/' */
11
11
  basepath?: string;
12
12
 
13
+ /** Where the route lives: 'history' = the path, 'hash' = after `#` (static hosting, no server fallback). Default: 'history' */
14
+ routerMode?: 'history' | 'hash';
15
+
13
16
  /** Base URL for icon assets. Default: '/assets/icons/' */
14
17
  iconBasepath?: string;
15
18
 
@@ -7,8 +7,8 @@ destructive ones separated on the trailing edge.
7
7
 
8
8
  ```html
9
9
  <u-action-bar sticky>
10
- <u-button variant="solid">Save</u-button>
11
- <u-button variant="outline">Cancel</u-button>
10
+ <u-button appearance="solid">Save</u-button>
11
+ <u-button appearance="outlined">Cancel</u-button>
12
12
  <u-button slot="danger" color="danger">Delete</u-button>
13
13
  </u-action-bar>
14
14
  ```
@@ -31,6 +31,7 @@ a left edge without any consumer CSS.
31
31
  |----------|------|---------|---------|-------------|
32
32
  | `title` | `string` | `''` | | Header title |
33
33
  | `meta` | `string` | — | | Secondary text after the title — a count, progress or short status (`3 items`, `2/5 done`), one step below the title: body size and weight, weak color, tabular figures. It sits inside the heading, so the heading's accessible name includes it. Not drawn without `title` |
34
+ | `description` | `string` | — | | One line under the title — what the box holds or what its numbers are based on. Caption size, weak color. Opens the header on its own |
34
35
  | `divider` | `boolean` | `false` | | Rule between header and body. Off by default — many rules make a screen noisy |
35
36
  | `flush` | `boolean` | `false` | | Remove body padding, for tables and lists that draw their own edges |
36
37
  | `level` | `2|3|4|5|6` | `3` | ✓ | Heading level of the title in the document outline. Set `2` for a box directly under `u-page-header` (the page's `h1`), so the outline does not skip a level. Semantics only — the title looks the same at every level |
@@ -41,6 +42,7 @@ a left edge without any consumer CSS.
41
42
  |------|-------------|
42
43
  | `header` · `title` · `actions` | Header row |
43
44
  | `meta` | Secondary text inside the title |
45
+ | `description` | One-line description under the title |
44
46
  | `body` | Body wrapper |
45
47
 
46
48
  ## CSS Custom Properties
@@ -52,6 +54,7 @@ a left edge without any consumer CSS.
52
54
  | `--u-text-subtitle-leading` | Title line height |
53
55
  | `--u-text-subtitle-tracking` | Title letter spacing |
54
56
  | `--u-text-body-size` / `--u-text-body-weight` / `--u-txt-color-weak` | `meta` size, weight and color |
57
+ | `--u-text-caption-size` / `-weight` / `-leading` | `description` typography |
55
58
  | `--u-bg-color-raised` | Surface background |
56
59
  | `--u-border-color-weak` | Border and header divider color |
57
60
  | `--u-radius-2xl` | Corner radius |
@@ -2,15 +2,15 @@
2
2
 
3
3
  **Tag:** `u-page-header`
4
4
 
5
- The same skeleton at the top of every LOB screen: optional back link, title, subtitle,
6
- a status slot, and an actions slot.
5
+ The same skeleton at the top of every LOB screen: optional back link, an eyebrow (menu group or
6
+ breadcrumb), title, a one-line description, a status slot, and an actions slot.
7
7
 
8
8
  Hand-rolling this per screen is where a product stops looking like one product — title sizes
9
9
  drift, action alignment goes slightly off, and the narrow-viewport rule gets forgotten on the
10
10
  screens nobody opens.
11
11
 
12
12
  ```html
13
- <u-page-header title="Order G-2026-I-0629" subtitle="Received 2026-02-24" back="/orders">
13
+ <u-page-header eyebrow="Sales" title="Order G-2026-I-0629" subtitle="Received 2026-02-24" back="/orders">
14
14
  <span slot="status"><u-tag color="success">Confirmed</u-tag></span>
15
15
  <span slot="actions"><u-button>Edit</u-button></span>
16
16
  </u-page-header>
@@ -20,6 +20,7 @@ screens nobody opens.
20
20
 
21
21
  | Name | Description |
22
22
  |------|-------------|
23
+ | `eyebrow` | Replaces the `eyebrow` text — e.g. breadcrumb links |
23
24
  | `status` | Badge or tag beside the title |
24
25
  | `actions` | Buttons on the trailing edge |
25
26
 
@@ -28,7 +29,8 @@ screens nobody opens.
28
29
  | Property | Type | Default | Reflect | Description |
29
30
  |----------|------|---------|---------|-------------|
30
31
  | `title` | `string` | `''` | | Page title |
31
- | `subtitle` | `string` | — | | Secondary line under the title |
32
+ | `subtitle` | `string` | — | | One-line description under the title — what the screen is for. Body size, weak color |
33
+ | `eyebrow` | `string` | — | | Small line above the title — the menu group or parent path. Overline step; the `eyebrow` slot replaces it |
32
34
  | `back` | `string` | — | | Back-link href; renders a back affordance before the title |
33
35
  | `backLabel` | `string` | `''` | | Back-link text (`back-label`). Empty falls back to the locale registry, then English |
34
36
  | `locale` | `string` | `''` | | Locale tag override for built-in strings |
@@ -38,7 +40,8 @@ screens nobody opens.
38
40
  | Part | Description |
39
41
  |------|-------------|
40
42
  | `back` | Back link |
41
- | `heading` | Title + subtitle wrapper |
43
+ | `heading` | Eyebrow + title + subtitle wrapper |
44
+ | `eyebrow` | Eyebrow row (hidden when empty) |
42
45
  | `title` · `subtitle` | Text elements |
43
46
  | `status` · `actions` | Slot wrappers |
44
47
 
@@ -48,14 +51,15 @@ screens nobody opens.
48
51
  |----------|-------------|
49
52
  | `--u-text-display-size` / `-weight` / `-leading` / `-tracking` | Title typography |
50
53
  | `--u-txt-color` | Title color |
51
- | `--u-text-caption-size` / `-weight` / `-leading` | Subtitle typography |
52
- | `--u-txt-color-weak` | Subtitle color |
54
+ | `--u-text-body-size` / `-weight` / `-leading` | Subtitle typography |
55
+ | `--u-text-overline-size` / `-weight` / `-leading` / `-tracking` | Eyebrow typography |
56
+ | `--u-txt-color-weak` | Subtitle and eyebrow color |
53
57
  | `--u-link-txt-color` | Back-link color |
54
58
  | `--u-text-label-size` / `-weight` | Back-link typography |
55
59
  | `--u-space-2xl` | Host bottom margin |
56
60
  | `--u-space-md` | Row gap (heading vs. actions) |
57
61
  | `--u-space-sm` | Title-row and actions internal gap |
58
- | `--u-space-2xs` | Title-to-subtitle spacing |
62
+ | `--u-space-2xs` | Eyebrow-to-title and title-to-subtitle spacing |
59
63
  | `--u-space-3xs` | Back-arrow-to-label spacing |
60
64
 
61
65
  ⚠ Built-in strings default to **English**, with Korean built in — choose the language with
@@ -61,7 +61,9 @@ interface SidebarLinkConfig {
61
61
  navigate?: 'router' | 'document';
62
62
  /** Anchor target. Use for opening in a new tab. */
63
63
  target?: '_self' | '_blank';
64
- styles?: StyleMap<'host' | 'base' | 'icon' | 'label'>;
64
+ /** Work waiting behind the item, shown at its end. Change it at runtime with app.setNavCount(href, n). */
65
+ count?: number | string;
66
+ styles?: StyleMap<'host' | 'base' | 'icon' | 'label' | 'count'>;
65
67
  }
66
68
  ```
67
69
 
@@ -87,6 +89,21 @@ highlighting still works, and it can live inside a section or group. `target: '_
87
89
  is available too, but it is not a substitute — it forces a new tab, so same-tab
88
90
  navigation to another document needs `navigate`.
89
91
 
92
+ #### Counts — work waiting behind an item
93
+
94
+ A link can end with a count: items to review, unpaid invoices. Show numbers only where
95
+ they ask for action, or the sidebar turns into a dashboard.
96
+
97
+ ```typescript
98
+ { type: 'link', icon: 'inbox', label: 'Reviews', href: '/reviews', count: 12 }
99
+
100
+ // later, without rebuilding the layout — `undefined` clears it
101
+ app.setNavCount('/reviews', 3);
102
+ ```
103
+
104
+ The count is `part="count"`; in the compact sidebar it stays in the link's accessible
105
+ name and is hidden visually.
106
+
90
107
  ---
91
108
 
92
109
  ### `SidebarGroupConfig` — `type: 'group'`
@@ -285,11 +302,14 @@ rule of your own wins.
285
302
 
286
303
  ⚠ **A custom `root` receives no styling.** Hand it a container with no height of its own and
287
304
  `height: 100%` has nothing to resolve against: the shell renders at whatever its own chrome
288
- resolves to (measured: about 133px) instead of filling the screen — with no error and nothing in
289
- the console. (With a long screen the shell grows to the viewport cap instead, so the
290
- symptom only shows on short screens.) Give that container a height — for screen only, so printing is not cut at one page:
305
+ resolves to (measured: about 133px) instead of filling the screen — no error, only a
306
+ development-mode console warning when it is shorter than 200px. (With a long screen the shell grows
307
+ to the viewport cap instead, so the symptom only shows on short screens.) The body margin is not
308
+ removed either, so the browser's default 8px stays around the shell. Give that container a height —
309
+ for screen only, so printing is not cut at one page — and clear the margin:
291
310
 
292
311
  ```css
312
+ :where(body) { margin: 0; }
293
313
  @media screen {
294
314
  #app { height: 100vh; } /* or 100%, inside an already-constrained ancestor */
295
315
  }