@iyulab/components 1.22.0 → 1.23.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/README.md +32 -0
  3. package/dist/components/UOverlayElement.d.ts +18 -0
  4. package/dist/components/UOverlayElement.js +24 -0
  5. package/dist/components/badge/UBadge.d.ts +5 -0
  6. package/dist/components/badge/UBadge.js +9 -0
  7. package/dist/components/badge/UBadge.styles.js +6 -0
  8. package/dist/components/dialog/UDialog.styles.js +1 -1
  9. package/dist/components/drawer/UDrawer.styles.js +1 -1
  10. package/dist/components/expander/UExpander.d.ts +38 -0
  11. package/dist/components/expander/UExpander.js +78 -0
  12. package/dist/components/expander/UExpander.styles.d.ts +1 -0
  13. package/dist/components/expander/UExpander.styles.js +95 -0
  14. package/dist/components/panel/UPanel.d.ts +0 -2
  15. package/dist/components/panel/UPanel.js +0 -5
  16. package/dist/components/skeleton/USkeleton.d.ts +7 -0
  17. package/dist/components/skeleton/USkeleton.js +7 -1
  18. package/dist/components/skeleton/USkeleton.styles.js +64 -0
  19. package/dist/components/tab-panel/UTabPanel.d.ts +4 -2
  20. package/dist/components/tab-panel/UTabPanel.js +0 -5
  21. package/dist/components/tag/UTag.d.ts +7 -0
  22. package/dist/components/tag/UTag.js +10 -1
  23. package/dist/components/tag/UTag.styles.js +4 -0
  24. package/dist/components/tree/UTree.d.ts +4 -2
  25. package/dist/components/tree/UTree.js +0 -5
  26. package/dist/components/tree-item/UTreeItem.d.ts +4 -1
  27. package/dist/components/tree-item/UTreeItem.js +0 -5
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.js +2 -1
  30. package/dist/react/UExpander.d.ts +16 -0
  31. package/dist/react/UExpander.js +13 -0
  32. package/dist/react/index.d.ts +1 -0
  33. package/dist/react/index.js +1 -0
  34. package/dist/utilities/Dialog.js +1 -1
  35. package/dist/utilities/Locale.d.ts +34 -0
  36. package/dist/utilities/Locale.js +43 -6
  37. package/dist/utilities/Theme.d.ts +24 -0
  38. package/dist/utilities/Theme.js +52 -0
  39. package/dist/utilities/accent.d.ts +52 -0
  40. package/dist/utilities/accent.js +127 -0
  41. package/dist/utilities/icons.js +1 -1
  42. package/dist/utilities/statusIcon.d.ts +31 -0
  43. package/dist/utilities/statusIcon.js +34 -0
  44. package/package.json +3 -2
  45. package/skills/iyulab-components/SKILL.md +2 -1
  46. package/skills/iyulab-components/references/components/badge.md +2 -1
  47. package/skills/iyulab-components/references/components/button.md +3 -1
  48. package/skills/iyulab-components/references/components/drawer.md +30 -0
  49. package/skills/iyulab-components/references/components/expander.md +70 -0
  50. package/skills/iyulab-components/references/components/input.md +6 -0
  51. package/skills/iyulab-components/references/components/panel.md +6 -4
  52. package/skills/iyulab-components/references/components/skeleton.md +12 -0
  53. package/skills/iyulab-components/references/components/tab-panel.md +4 -5
  54. package/skills/iyulab-components/references/components/tag.md +18 -1
  55. package/skills/iyulab-components/references/components/textarea.md +5 -0
  56. package/skills/iyulab-components/references/components/tree.md +2 -4
  57. package/skills/iyulab-components/references/usage.md +22 -2
  58. package/skills/iyulab-components/references/utilities/theme.md +23 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,185 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.23.0] - 2026-08-04
4
+
5
+ ### Added
6
+
7
+ - **`u-expander` — 제목을 눌러 본문을 접고 펴는 디스클로저.**
8
+
9
+ ```html
10
+ <u-expander label="배송 정보" open>
11
+ <p>본문</p>
12
+ </u-expander>
13
+ ```
14
+
15
+ 헤더는 **네이티브 `<button>`** 이라 Enter/Space·포커스 순서가 그대로 따라온다. 높이 전환은
16
+ grid 트랙(`0fr` ↔ `1fr`)이라 콘텐츠 높이를 JS 로 재지 않으며, `prefers-reduced-motion:
17
+ reduce` 에서 멈춘다(접힘/펼침의 결과는 정지 상태에서도 그대로 읽히므로 이 전환은 장식이다).
18
+
19
+ ⚠**접힘의 요점은 높이가 아니라 «도달 불가»다** — `overflow: hidden` 만으로 접으면 화면에서
20
+ 사라진 콘텐츠가 **접근성 트리와 탭 순서에 그대로 남는다.** `visibility` 를 함께 옮겨
21
+ 접힌 본문이 포커스를 받지 못하게 한다(회귀 테스트가 이 조건만 따로 잰다).
22
+
23
+ ### Changed
24
+
25
+ - **콘솔 진단 문구가 영어로 이주했다** — 토큰 시트 부재 경고 · `[Dialog]` 표시 실패 ·
26
+ `[IconRegistry]` 리졸브 실패. 화면 문자열이 아니라 **개발자 대면 진단**이고, 검색 가능성과
27
+ 이슈 재현 공유에서 영어가 유리하다는 판단이다(로케일 레지스트리에 넣지 않는다 — 사용자가
28
+ 읽는 문자열이 아니다). 문구를 판별식으로 쓰던 테스트가 있다면 *"무엇을 말하는가"* 로 좁힐 것.
29
+
30
+ ### Removed
31
+
32
+ - 🔴**한 번도 동작한 적 없는 선언 3종을 걷었다.** 전부 «선언 + 문서»만 있고 그 값을 읽는
33
+ 코드가 **어디에도 없었다**(전수 검색). 동작이 없었으므로 **깨질 동작도 없다.**
34
+
35
+ | 제거 | 문서가 약속하던 것 | 대체 |
36
+ |---|---|---|
37
+ | `u-panel[collapsible]` | *"Allow the panel to collapse"* | **`u-expander`**(위) |
38
+ | `u-tab-panel[editable]` | *"Allow adding/removing tabs"* | 없음 — 탭 추가/삭제는 소비자가 슬롯을 직접 다룬다 |
39
+ | `u-tree[droppable]` · `u-tree-item[droppable]` | *"Enable drop" / "Accepts drops"* | 없음 — 아래 참조 |
40
+
41
+ ⚠**`draggable` 은 남는다** — 그것은 네이티브 전역 속성이라 브라우저가 드래그 자체는
42
+ 켜 준다. 다만 문서가 *"Allow reordering tabs by drag"*·*"Enable drag"* 로 **재정렬/드롭
43
+ 기능을 약속**하고 있었고 그런 코드는 존재한 적이 없다 ⇒ **문서를 사실로 정정**했다
44
+ (*"native drag attribute — no built-in reordering"*). 드래그 재정렬·트리 드롭은 수요가
45
+ 모이면 기능으로 설계한다.
46
+
47
+ - **`Locale.namespace()` — 상위 패키지가 자기 화면 문자열을 담을 자리.**
48
+
49
+ ```ts
50
+ const t = Locale.namespace<'empty' | 'loading'>('u-data-view');
51
+ t.register('en', { empty: 'No data', loading: 'Loading…' });
52
+ t.register('ko', { empty: '데이터가 없습니다' });
53
+ t.text('empty'); // 활성 로케일 기준
54
+ ```
55
+
56
+ 종전의 `Locale.register`/`getValue` 는 **검증 메시지 9키의 닫힌 유니온**(`LocaleMessageKey`)만
57
+ 받는다. 상위 패키지의 화면 문자열은 그 유니온에 없고, 넣으면 기반 라이브러리에 소비자
58
+ 도메인 어휘가 쌓인다. ⇒ 결과적으로 각 패키지가 **자기 레지스트리를 손으로 만들거나**
59
+ 문자열을 하드코딩해 왔다.
60
+
61
+ ⚠**순수 가산이다** — `LocaleMessageKey`·`register`·`getValue` 세 시그니처를 하나도
62
+ 건드리지 않는다. 키 유니온은 **소비자가** 제네릭으로 정하므로 라이브러리 키셋은 커지지 않는다.
63
+
64
+ 조회 사슬(정확 일치 → base 언어 → `en`)과 `{name}` 치환은 검증 메시지와 **같은 구현을
65
+ 공유한다** — 두 벌로 두면 한쪽만 고쳐진다. 사슬에 없는 키는 **키 자체**를 돌려준다
66
+ (조용히 빈 문자열이 되는 것보다 화면에 드러나는 편이 낫다).
67
+
68
+ - **`Theme.accent(seed)` — 브랜드 색 하나로 `--u-primary-*` 램프를 만든다.**
69
+
70
+ ```ts
71
+ Theme.accent('#6A1B9A'); // 램프 5단 + 면 위 글자색이 계산된다
72
+ Theme.accent(null); // 해제 — 시트 기본값으로
73
+ ```
74
+
75
+ 종전에는 소비자가 램프를 **손으로 열 줄 적었고**, 그 값이 대비 계약을 만족하는지는
76
+ 아무것도 확인하지 않았다. 이제 **계산해서** 넣는다 — 면 위 글자 4.5 · 바탕 위 글자 4.5 ·
77
+ `-strong` ↔ `-color` 구분 1.20 · 그래픽 3.0.
78
+
79
+ ⚠**CSS `color-mix()` 만으로는 안 된다** — 고정 비율은 밝은 시드에서 깨진다(노랑의
80
+ `-strong` 이 바탕 대비 **2.21**, 기준 4.5). 목표 대비까지 섞으려면 대비를 **계산**해야
81
+ 하고 CSS 에는 그 수단이 없다. ⇒ 계산은 JS, 결과만 커스텀 프로퍼티로.
82
+
83
+ ⚠**테마가 바뀌면 다시 계산된다** — 대비 목표가 «바탕 기준»이라 라이트와 다크는 같은
84
+ 시드에서 **다른 램프**를 갖는다. 바탕은 계산 시점의 `--u-bg-color` 를 읽으므로 소비자가
85
+ 바탕을 덮었으면 그 값이 기준이 된다.
86
+
87
+ - **`u-skeleton` 에 `lines` — 여러 줄 자리표시자.**
88
+
89
+ ```html
90
+ <u-skeleton lines="3" height="1em"></u-skeleton> <!-- 막대 3개 · 마지막 줄은 짧게 -->
91
+ ```
92
+
93
+ 종전에는 목록 한 줄·문단 자리표시자를 만들 때 **폭만 다른 막대를 손으로 반복**해야 했다
94
+ (이 리포 안에서만 12사용처). 마지막 줄을 짧게 그리는 것은 장식이 아니라 **문단의 끝**을
95
+ 나타내는 신호다(`--skeleton-last-line-width`, 기본 60%). 줄 간격은
96
+ `--skeleton-line-gap`(기본 `0.5em`).
97
+
98
+ ⚠**단일 막대의 모양은 바뀌지 않는다** — `lines` 가 없으면 종전과 완전히 같고, 회귀
99
+ 테스트가 그 계약을 감시한다. `pulse`/`shimmer` 는 줄마다 적용되며 `prefers-reduced-motion`
100
+ 에서 함께 멈춘다.
101
+
102
+ - **오버레이가 열릴 때 `autofocus` 를 존중하고, 없으면 첫 입력으로 간다** (`u-dialog`·`u-drawer`).
103
+
104
+ ```html
105
+ <u-drawer closable>
106
+ <span slot="header">주문 편집</span>
107
+ <u-input label="수량" autofocus></u-input> <!-- 여기로 간다 -->
108
+ </u-drawer>
109
+ ```
110
+
111
+ 종전에는 `focus-trap` 의 기본값(**첫 tabbable 노드**)에 맡겨져 있었다. 그 결과 ⑴`autofocus`
112
+ 가 **무시되고**(focus-trap 은 그 속성을 보지 않는다) ⑵**버튼이 마크업 앞에 있으면 버튼이
113
+ 포커스를 가져갔다**(확인/취소가 먼저 오는 다이얼로그가 그 모양이다 — 실측: 입력이 아니라
114
+ 첫 버튼). ⇒ 순서를 계약으로 고정한다: `[autofocus]` → 첫 입력 컨트롤 → 첫 tabbable.
115
+
116
+ ⚠**시각적 변화가 있을 수 있다** — 버튼이 앞에 오는 다이얼로그는 이제 **입력**에서 시작한다.
117
+ 기존 동작을 원하면 그 버튼에 `autofocus` 를 준다.
118
+
119
+ - **`u-tag`·`u-badge` 에 `icon` — 상태가 «색 없이도» 구분된다.**
120
+
121
+ ```html
122
+ <u-tag color="danger" icon>실패</u-tag> <!-- ✕ 원형 아이콘 -->
123
+ <u-tag color="success" icon>완료</u-tag> <!-- ✓ 원형 아이콘 -->
124
+ ```
125
+
126
+ 대비 계약(1.20.0)은 각 상태색이 **자기 바탕에서 읽히는가**를 지키지만 *구별되는가* 는 지키지
127
+ 않는다 — 색각 이상(남성 약 8%)이나 흑백 인쇄에서는 «성공»과 «실패»가 같은 회색 알약이 된다.
128
+ 역할 색 충돌 검사가 **색 공간에서**(ΔE) 세운 구분을 이 축은 **모양 공간에서** 세운다.
129
+
130
+ ⚠**의미가 없는 색에는 그리지 않는다** — 장식 축(`blue`·`purple` …)은 *색 자체*를 뜻하고
131
+ `neutral`·`primary` 는 상태가 아니다. 없는 의미를 아이콘으로 지어내면 그 아이콘이 **잘못된
132
+ 정보를 나른다**. `u-badge variant="dot"` 도 콘텐츠를 렌더하지 않으므로 대상이 아니다.
133
+
134
+ ⚠**렌더는 가산이다** — `icon` 을 주지 않으면 종전 그대로다. 아이콘은 **내장 번들**에서 오므로
135
+ 네트워크를 타지 않는다(오프라인에서도 그려진다).
136
+
137
+ ⚠**다만 번들 크기는 가산이 아니다** — `u-tag`·`u-badge` 가 `u-icon` 을 import 하고, 내부
138
+ 아이콘 번들은 **빌드 시점 eager glob** 이다. 그래서 `dist/components/tag/UTag.js` 만 deep
139
+ import 하던 소비자도 이제 `UIcon`(2.3 KB)과 내장 SVG 세트(**~9.8 KB**, 19종)를 함께 받는다.
140
+ 전체 라이브러리를 쓰는 소비자에게는 변화가 없다(이미 포함돼 있다).
141
+
142
+ - **`u-skeleton` 의 `pulse`/`shimmer` 가 `prefers-reduced-motion: reduce` 에서 멈춘다.**
143
+
144
+ ⚠**스피너는 그대로 돈다** — 갈림길은 *멈추냐 마냐*가 아니라 **어느 움직임이 의미를
145
+ 나르는가**였다. 스피너의 회전은 **신호**라 죽이면 진행 여부를 알 수 없다(WCAG 2.2.2 는
146
+ 로딩 표시처럼 움직임이 본질인 것을 예외로 둔다). 스켈레톤은 **기본값이 이미 정적**이고
147
+ 정지한 블록이 «로딩 중»을 그대로 나르므로 `pulse`/`shimmer` 는 장식이다.
148
+
149
+ ⚠시트의 reduce 규칙(`--u-duration-*: 0ms`)은 **`animation` 을 쓰는 자리에 닿지 않는다** —
150
+ 지속시간 축을 경유하지 않기 때문이다. 그래서 컴포넌트에서 명시적으로 멈춘다.
151
+
152
+ ### Changed
153
+
154
+ - ⚠**`u-dialog`·`u-drawer` 제목이 2px 작아진다** — `18px` 리터럴 → `--u-text-subtitle-size`(16px).
155
+
156
+ 종전에는 *다이얼로그 제목 18 · 얼럿 제목 16* 으로 **같은 위계가 두 값**이었다. 18px 는
157
+ 타입 스케일 7단에 **없는 단**이라 리터럴로만 존재할 수 있었고, 그래서 소비자가 스케일을
158
+ 조율해도 이 두 자리는 따라오지 않았다.
159
+
160
+ ⚠**단을 늘리지 않았다** — 18px 를 요구하는 타이포 자리는 전수 **둘뿐**이고(`u-copy-button`
161
+ 의 18px 는 아이콘 크기다), 단일 수요로 축을 늘리는 것은 선제 확장이다. 같은 패키지의
162
+ `u-alert` 가 이미 `subtitle` 을 쓰고 있어 **선례를 따랐다**.
163
+
164
+ ⚠**헤더의 닫기 버튼은 `font-size: inherit` 이라 함께 2px 작아진다.**
165
+
166
+ `tests/build/type-scale.test.ts` 가 *"제목 위계를 가진 컴포넌트가 같은 단을 읽는가"* 를
167
+ 대조한다 — 리터럴로 다시 갈라지면 발화한다.
168
+
169
+ ### Fixed
170
+
171
+ - **역할 색의 «의미 충돌» 검사를 뒀다**(`tests/build/role-color-collision.test.ts`).
172
+ 대비 검사는 각 역할색을 **자기 바탕에 대해** 재므로 *"저장(primary)과 삭제(danger)가
173
+ 같은 색"* 인 상태도 **양쪽 다 통과한다** — 대비는 *읽히는가*를 묻지 *구별되는가*를 묻지
174
+ 않는다. 이제 역할끼리의 거리를 CIELAB ΔE 로 잰다(임계 20).
175
+
176
+ ⚠**`primary` 와 `info` 는 기본값이 의도적으로 같으므로 면제**다(브랜딩은 `primary` 만
177
+ 바꾼다). 면제쌍의 **근거가 시트 주석에 남아 있는지**도 함께 잰다 — 근거 없는 면제가
178
+ 쌓이면 검사가 아무것도 지키지 않게 된다.
179
+
180
+ 실측: 면제쌍을 뺀 최소 거리가 **38.7**(라이트 `-strong` 의 warning/danger)이라 정상적인
181
+ 램프 조율에는 발화하지 않는다.
182
+
3
183
  ## [1.22.0] - 2026-08-03
4
184
 
5
185
  ### Added
package/README.md CHANGED
@@ -104,6 +104,38 @@ Theme.set('system');
104
104
 
105
105
  자세한 내용은 [docs/theming.md](./docs/theming.md)를 참고하세요.
106
106
 
107
+ ## Localization
108
+
109
+ 라이브러리가 **스스로 생성하는 문자열**(검증 메시지)은 내장 로케일 14종을 갖고 있으며,
110
+ 활성 로케일 하나로 전부 따라옵니다.
111
+
112
+ ```ts
113
+ import { Locale } from '@iyulab/components';
114
+
115
+ Locale.set('ko'); // 활성 로케일
116
+ Locale.register('nl', { valueMissing: '…' }); // 검증 메시지 override
117
+ ```
118
+
119
+ ### 네임스페이스 — 상위 패키지·앱의 문자열 (1.23.0~)
120
+
121
+ 검증 메시지 키셋은 **닫혀 있습니다**(9키). 그 위에 자기 문자열을 담으려면 네임스페이스를
122
+ 씁니다 — 키 유니온은 **쓰는 쪽이** 정하므로 라이브러리 키셋은 커지지 않습니다.
123
+
124
+ ```ts
125
+ const t = Locale.namespace<'empty' | 'loading'>('u-data-view');
126
+
127
+ t.register('en', { empty: 'No data', loading: 'Loading…' }); // 기본은 영어
128
+ t.register('ko', { empty: '데이터가 없습니다' }); // 필요한 언어만 추가
129
+
130
+ t.text('empty'); // 활성 로케일 기준
131
+ t.text('greet', { who: 'Ann' }); // {name} 치환
132
+ ```
133
+
134
+ - 조회 사슬은 검증 메시지와 같습니다: **정확 일치 → base 언어(`ko-KR` → `ko`) → `en`**.
135
+ - 같은 이름의 네임스페이스는 **같은 저장소**를 가리킵니다(모듈 어디서 만들어도 됩니다).
136
+ - 사슬에 없는 키는 **키 자체**를 돌려줍니다 — 조용히 빈 문자열이 되지 않습니다.
137
+ - `Locale.set()` 하나로 검증 메시지와 네임스페이스가 함께 전환됩니다.
138
+
107
139
  ## Documentation
108
140
 
109
141
  | 문서 | 내용 |
@@ -65,6 +65,24 @@ export declare abstract class UOverlayElement extends UElement {
65
65
  requestClose(source: string): void;
66
66
  /** 오버레이가 열릴 때 설정을 적용합니다. */
67
67
  private setup;
68
+ /**
69
+ * 열릴 때 포커스를 받을 요소를 정한다 — **입력을 먼저 본다.**
70
+ *
71
+ * 순서: `[autofocus]` → 첫 입력 컨트롤 → (없으면) focus-trap 기본값(첫 tabbable).
72
+ *
73
+ * 🔴**두 분기가 «각각» 무엇을 하는지 되돌려 재서 갈랐다** — 이 함수는 그 기록이다.
74
+ *
75
+ * `[autofocus]` → `u-drawer` 에서 갈린다. `focus-trap` 은 그 속성을 존중하지 않는다
76
+ * (v7 `getInitialFocusNode` 는 «첫 tabbable» 로 폴백한다).
77
+ * 첫 입력 우선 → `u-dialog` 에서 갈린다. 버튼이 DOM 순서상 앞이면 기본값은 **버튼**을
78
+ * 잡는다(실측: 제거하면 `button#b1`, 복원하면 `input`).
79
+ *
80
+ * ⚠**`u-drawer` 만으로는 두 번째 분기를 판별할 수 없었다** — 슬롯 배치 때문에 기본값도
81
+ * 우연히 첫 입력에 떨어진다. *"되돌려도 통과한다"* 를 *"이 코드는 아무것도 안 한다"* 로
82
+ * 읽을 뻔했고, **판별하는 입력을 찾은 뒤에야** 갈렸다.
83
+ * (`tests/browser/overlay-initial-focus.browser.test.ts` 가 둘 다 감시한다.)
84
+ */
85
+ private resolveInitialFocus;
68
86
  /** 오버레이가 닫힐 때 설정을 해제합니다. */
69
87
  private cleanup;
70
88
  /** host 자체를 클릭한 경우(백드롭)만 닫기 요청 */
@@ -102,12 +102,36 @@ var UOverlayElement = class extends UElement {
102
102
  clickOutsideDeactivates: false,
103
103
  allowOutsideClick: true,
104
104
  fallbackFocus: this,
105
+ initialFocus: () => this.resolveInitialFocus(),
105
106
  trapStack: OverlayManager.trapStack,
106
107
  tabbableOptions: { getShadowRoot: true }
107
108
  });
108
109
  this.focusTrap.activate();
109
110
  }
110
111
  }
112
+ /**
113
+ * 열릴 때 포커스를 받을 요소를 정한다 — **입력을 먼저 본다.**
114
+ *
115
+ * 순서: `[autofocus]` → 첫 입력 컨트롤 → (없으면) focus-trap 기본값(첫 tabbable).
116
+ *
117
+ * 🔴**두 분기가 «각각» 무엇을 하는지 되돌려 재서 갈랐다** — 이 함수는 그 기록이다.
118
+ *
119
+ * `[autofocus]` → `u-drawer` 에서 갈린다. `focus-trap` 은 그 속성을 존중하지 않는다
120
+ * (v7 `getInitialFocusNode` 는 «첫 tabbable» 로 폴백한다).
121
+ * 첫 입력 우선 → `u-dialog` 에서 갈린다. 버튼이 DOM 순서상 앞이면 기본값은 **버튼**을
122
+ * 잡는다(실측: 제거하면 `button#b1`, 복원하면 `input`).
123
+ *
124
+ * ⚠**`u-drawer` 만으로는 두 번째 분기를 판별할 수 없었다** — 슬롯 배치 때문에 기본값도
125
+ * 우연히 첫 입력에 떨어진다. *"되돌려도 통과한다"* 를 *"이 코드는 아무것도 안 한다"* 로
126
+ * 읽을 뻔했고, **판별하는 입력을 찾은 뒤에야** 갈렸다.
127
+ * (`tests/browser/overlay-initial-focus.browser.test.ts` 가 둘 다 감시한다.)
128
+ */
129
+ resolveInitialFocus() {
130
+ const explicit = this.querySelector("[autofocus]");
131
+ if (explicit) return explicit;
132
+ const control = this.querySelector("input, select, textarea, u-input, u-textarea, u-select, u-checkbox, u-radio, u-switch, u-slider");
133
+ if (control) return control;
134
+ }
111
135
  /** 오버레이가 닫힐 때 설정을 해제합니다. */
112
136
  cleanup() {
113
137
  this.focusTrap?.deactivate();
@@ -22,6 +22,11 @@ export declare class UBadge extends UElement {
22
22
  color: BadgeColor;
23
23
  /** 부모 요소 기준 배치 위치 (설정 시 position: absolute) */
24
24
  anchor?: BadgeAnchor;
25
+ /**
26
+ * 상태를 **색과 무관하게** 나르는 아이콘을 붙입니다(`info`·`success`·`warning`·`danger`).
27
+ * 의미가 없는 색(`neutral`·`primary`·장식 축)과 `variant="dot"` 에서는 그리지 않습니다.
28
+ */
29
+ icon: boolean;
25
30
  render(): import('lit-html').TemplateResult<1> | typeof nothing;
26
31
  }
27
32
  declare global {
@@ -1,6 +1,8 @@
1
1
  import { UElement } from "../UElement.js";
2
2
  import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js";
3
3
  import __decorate from "../../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js";
4
+ import "../icon/UIcon.js";
5
+ import { statusIcon } from "../../utilities/statusIcon.js";
4
6
  import { styles } from "./UBadge.styles.js";
5
7
  import { html, nothing } from "lit";
6
8
  import { customElement, property } from "lit/decorators.js";
@@ -10,14 +12,17 @@ var UBadge = class UBadge extends UElement {
10
12
  super(..._args);
11
13
  this.variant = "pill";
12
14
  this.color = "blue";
15
+ this.icon = false;
13
16
  }
14
17
  static {
15
18
  this.styles = [super.styles, styles];
16
19
  }
17
20
  render() {
18
21
  if (this.variant === "dot") return nothing;
22
+ const iconName = this.icon ? statusIcon(this.color) : void 0;
19
23
  return html`
20
24
  <div class="base" part="base">
25
+ ${iconName ? html`<u-icon class="icon" part="icon" lib="internal" name=${iconName}></u-icon>` : nothing}
21
26
  <slot name="prefix"></slot>
22
27
  <slot></slot>
23
28
  <slot name="suffix"></slot>
@@ -37,6 +42,10 @@ __decorate([property({
37
42
  type: String,
38
43
  reflect: true
39
44
  }), __decorateMetadata("design:type", Object)], UBadge.prototype, "anchor", void 0);
45
+ __decorate([property({
46
+ type: Boolean,
47
+ reflect: true
48
+ }), __decorateMetadata("design:type", Object)], UBadge.prototype, "icon", void 0);
40
49
  UBadge = __decorate([customElement("u-badge")], UBadge);
41
50
  //#endregion
42
51
  export { UBadge };
@@ -140,6 +140,12 @@ var styles = css`
140
140
  transform: translate(-50%, 50%);
141
141
  }
142
142
 
143
+ /* 상태 아이콘 — prefix 슬롯과 같은 간격을 쓴다. */
144
+ .icon {
145
+ flex-shrink: 0;
146
+ margin-right: 0.25em;
147
+ }
148
+
143
149
  /* === Slots === */
144
150
  ::slotted([slot="prefix"]) {
145
151
  margin-right: 0.25em;
@@ -89,7 +89,7 @@ var styles = css`
89
89
  justify-content: space-between;
90
90
  gap: var(--u-space-sm, 8px);
91
91
  padding: var(--u-space-md, 12px) var(--u-space-lg, 16px);
92
- font-size: 18px;
92
+ font-size: var(--u-text-subtitle-size, 16px);
93
93
  font-weight: 600;
94
94
  line-height: 1.3;
95
95
  border-bottom: 1px solid var(--u-border-color, #E0E0E0);
@@ -77,7 +77,7 @@ var styles = css`
77
77
  justify-content: space-between;
78
78
  gap: var(--u-space-sm, 8px);
79
79
  padding: var(--u-space-md, 12px) var(--u-space-lg, 16px);
80
- font-size: 18px;
80
+ font-size: var(--u-text-subtitle-size, 16px);
81
81
  font-weight: 600;
82
82
  line-height: 1.3;
83
83
  border-bottom: 1px solid var(--u-border-color, #E0E0E0);
@@ -0,0 +1,38 @@
1
+ import { UElement } from '../UElement.js';
2
+ /**
3
+ * 제목을 눌러 본문을 접고 펴는 디스클로저(익스팬더) 컴포넌트입니다.
4
+ *
5
+ * @slot - 펼쳤을 때 보이는 본문
6
+ * @slot header - 제목 영역을 통째로 대체하는 콘텐츠 (`label` 보다 우선)
7
+ * @slot suffix - 제목 우측에 놓이는 콘텐츠 (상태 배지·보조 액션 등)
8
+ *
9
+ * @csspart header - 제목 버튼 영역
10
+ * @csspart icon - 펼침 표시 아이콘
11
+ * @csspart label - 제목 텍스트 영역
12
+ * @csspart content - 본문을 감싸는 영역
13
+ *
14
+ * @event expand - 펼쳐질 때 발생 (취소 가능)
15
+ * @event collapse - 접힐 때 발생 (취소 가능)
16
+ */
17
+ export declare class UExpander extends UElement {
18
+ static styles: import('lit').CSSResultGroup[];
19
+ /** 펼침 여부 */
20
+ open: boolean;
21
+ /** 비활성화 여부 */
22
+ disabled: boolean;
23
+ /** 제목 텍스트 (`header` 슬롯이 있으면 그쪽이 이깁니다) */
24
+ label: string;
25
+ render(): import('lit-html').TemplateResult<1>;
26
+ /** 펼칩니다. `expand` 이벤트가 취소되면 아무 일도 하지 않고 `false` 를 돌려줍니다. */
27
+ expand(): boolean;
28
+ /** 접습니다. `collapse` 이벤트가 취소되면 아무 일도 하지 않고 `false` 를 돌려줍니다. */
29
+ collapse(): boolean;
30
+ /** 현재 상태의 반대로 바꿉니다. */
31
+ toggle(): boolean;
32
+ private handleClick;
33
+ }
34
+ declare global {
35
+ interface HTMLElementTagNameMap {
36
+ 'u-expander': UExpander;
37
+ }
38
+ }
@@ -0,0 +1,78 @@
1
+ import { UElement } from "../UElement.js";
2
+ import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js";
3
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js";
4
+ import "../icon/UIcon.js";
5
+ import { styles } from "./UExpander.styles.js";
6
+ import { html } from "lit";
7
+ import { customElement, property } from "lit/decorators.js";
8
+ //#region src/components/expander/UExpander.ts
9
+ var UExpander = class UExpander extends UElement {
10
+ constructor(..._args) {
11
+ super(..._args);
12
+ this.open = false;
13
+ this.disabled = false;
14
+ this.label = "";
15
+ this.handleClick = () => {
16
+ this.toggle();
17
+ };
18
+ }
19
+ static {
20
+ this.styles = [super.styles, styles];
21
+ }
22
+ render() {
23
+ return html`
24
+ <div class="frame">
25
+ <button class="header" part="header" type="button"
26
+ aria-expanded=${this.open ? "true" : "false"}
27
+ aria-controls="content"
28
+ ?disabled=${this.disabled}
29
+ @click=${this.handleClick}
30
+ >
31
+ <u-icon class="icon" part="icon" lib="internal" name="chevron-right"></u-icon>
32
+ <span class="label" part="label"><slot name="header">${this.label}</slot></span>
33
+ <slot name="suffix"></slot>
34
+ </button>
35
+
36
+ <div class="content" part="content" id="content" role="region">
37
+ <div class="inner">
38
+ <div class="body"><slot></slot></div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ `;
43
+ }
44
+ /** 펼칩니다. `expand` 이벤트가 취소되면 아무 일도 하지 않고 `false` 를 돌려줍니다. */
45
+ expand() {
46
+ if (this.disabled || this.open) return false;
47
+ if (this.fire("expand")) {
48
+ this.open = true;
49
+ return true;
50
+ }
51
+ return false;
52
+ }
53
+ /** 접습니다. `collapse` 이벤트가 취소되면 아무 일도 하지 않고 `false` 를 돌려줍니다. */
54
+ collapse() {
55
+ if (this.disabled || !this.open) return false;
56
+ if (this.fire("collapse")) {
57
+ this.open = false;
58
+ return true;
59
+ }
60
+ return false;
61
+ }
62
+ /** 현재 상태의 반대로 바꿉니다. */
63
+ toggle() {
64
+ return this.open ? this.collapse() : this.expand();
65
+ }
66
+ };
67
+ __decorate([property({
68
+ type: Boolean,
69
+ reflect: true
70
+ }), __decorateMetadata("design:type", Object)], UExpander.prototype, "open", void 0);
71
+ __decorate([property({
72
+ type: Boolean,
73
+ reflect: true
74
+ }), __decorateMetadata("design:type", Object)], UExpander.prototype, "disabled", void 0);
75
+ __decorate([property({ type: String }), __decorateMetadata("design:type", Object)], UExpander.prototype, "label", void 0);
76
+ UExpander = __decorate([customElement("u-expander")], UExpander);
77
+ //#endregion
78
+ export { UExpander };
@@ -0,0 +1 @@
1
+ export declare const styles: import('lit').CSSResult;
@@ -0,0 +1,95 @@
1
+ import { css } from "lit";
2
+ //#region src/components/expander/UExpander.styles.ts
3
+ var styles = css`
4
+ :host {
5
+ display: block;
6
+ }
7
+ :host([disabled]) {
8
+ opacity: 0.5;
9
+ }
10
+
11
+ /* ⚠테두리·면은 :host 가 아니라 내부 래퍼가 그린다 — :host 에 두면 소비 앱의 CSS
12
+ 리셋(* { border: 0 })이 «에러 없이» 지운다(Tailwind preflight 가 가장 흔한 경우다).
13
+ 이 리포는 그 결함을 9개 컴포넌트에서 이미 걷어냈고 회귀 테스트가 목록을 0 으로 잠근다
14
+ — tests/build/host-layout-reset.test.ts */
15
+ .frame {
16
+ border: 1px solid var(--u-border-color, #E0E0E0);
17
+ border-radius: var(--u-radius-md, 4px);
18
+ background-color: var(--u-bg-color, #FFFFFF);
19
+ }
20
+
21
+ .header {
22
+ display: flex;
23
+ align-items: center;
24
+ gap: 0.5rem;
25
+ width: 100%;
26
+ box-sizing: border-box;
27
+ padding: var(--u-space-sm, 8px) var(--u-space-md, 12px);
28
+ border: none;
29
+ border-radius: inherit;
30
+ background: none;
31
+ color: var(--u-txt-color, #212121);
32
+ font: inherit;
33
+ font-size: var(--u-text-body-size, 14px);
34
+ font-weight: var(--u-text-subtitle-weight, 600);
35
+ text-align: start;
36
+ cursor: pointer;
37
+ }
38
+ .header:disabled {
39
+ cursor: default;
40
+ }
41
+ .header:focus-visible {
42
+ outline: 2px solid var(--u-primary-color-strong, #1565C0);
43
+ outline-offset: -2px;
44
+ }
45
+
46
+ .icon {
47
+ flex: none;
48
+ color: var(--u-txt-color-weak, #757575);
49
+ transition: transform var(--u-duration-normal, 220ms) var(--u-ease-standard, cubic-bezier(0.2, 0, 0, 1));
50
+ }
51
+ :host([open]) .icon {
52
+ transform: rotate(90deg);
53
+ }
54
+
55
+ .label {
56
+ flex: 1 1 auto;
57
+ min-width: 0;
58
+ }
59
+
60
+ /* 높이 애니메이션은 grid 트랙(0fr ↔ 1fr)으로 한다 — 콘텐츠 높이를 JS 로 재지 않는다.
61
+ ⚠전환을 지원하지 않는 엔진에서는 «즉시» 열리고 닫힐 뿐 접힘 자체는 성립한다
62
+ (0fr 은 그 엔진에서도 유효한 트랙 크기다).
63
+ ★visibility 를 함께 옮기는 것이 요점이다 — overflow: hidden 만으로는 접힌 콘텐츠가
64
+ 여전히 접근성 트리와 탭 순서에 남는다. */
65
+ .content {
66
+ display: grid;
67
+ grid-template-rows: 0fr;
68
+ visibility: hidden;
69
+ transition:
70
+ grid-template-rows var(--u-duration-normal, 220ms) var(--u-ease-standard, cubic-bezier(0.2, 0, 0, 1)),
71
+ visibility var(--u-duration-normal, 220ms);
72
+ }
73
+ :host([open]) .content {
74
+ grid-template-rows: 1fr;
75
+ visibility: visible;
76
+ }
77
+ .inner {
78
+ overflow: hidden;
79
+ }
80
+ .body {
81
+ padding: 0 var(--u-space-md, 12px) var(--u-space-md, 12px);
82
+ }
83
+
84
+ /* 이 전환은 «장식»이다 — 접힘/펼침의 결과는 정지 상태에서도 그대로 읽힌다.
85
+ ⚠시트의 reduce 규칙(지속시간 축 0)에 기대지 않고 여기서도 누른다: 소비자가 토큰
86
+ 시트를 로드하지 않으면 use-site 폴백 리터럴이 살아남아 그 규칙이 닿지 못한다. */
87
+ @media (prefers-reduced-motion: reduce) {
88
+ .icon,
89
+ .content {
90
+ transition-duration: 0s;
91
+ }
92
+ }
93
+ `;
94
+ //#endregion
95
+ export { styles };
@@ -9,8 +9,6 @@ export declare class UPanel extends UElement {
9
9
  static styles: import('lit').CSSResultGroup[];
10
10
  /** 패널 비활성화 여부 */
11
11
  disabled: boolean;
12
- /** 패널 접기 가능 여부 */
13
- collapsible: boolean;
14
12
  /** 패널 고유값 (매칭 시 사용) */
15
13
  value: string;
16
14
  render(): import('lit-html').TemplateResult<1>;
@@ -9,7 +9,6 @@ var UPanel = class UPanel extends UElement {
9
9
  constructor(..._args) {
10
10
  super(..._args);
11
11
  this.disabled = false;
12
- this.collapsible = false;
13
12
  this.value = "";
14
13
  }
15
14
  static {
@@ -23,10 +22,6 @@ __decorate([property({
23
22
  type: Boolean,
24
23
  reflect: true
25
24
  }), __decorateMetadata("design:type", Object)], UPanel.prototype, "disabled", void 0);
26
- __decorate([property({
27
- type: Boolean,
28
- reflect: true
29
- }), __decorateMetadata("design:type", Object)], UPanel.prototype, "collapsible", void 0);
30
25
  __decorate([property({
31
26
  type: String,
32
27
  reflect: true
@@ -22,6 +22,13 @@ export declare class USkeleton extends UElement {
22
22
  width?: string;
23
23
  /** 높이 (CSS 값) */
24
24
  height?: string;
25
+ /**
26
+ * 여러 줄 자리표시자로 그릴 줄 수. **2 이상일 때만** 호스트가 «막대들의 통»이 되고,
27
+ * 마지막 줄은 짧게(`--skeleton-last-line-width`, 기본 60%) 그린다 — 문단의 끝을 그렇게 읽는다.
28
+ *
29
+ * ⚠**단일 막대(기본)의 모양은 바꾸지 않는다** — 이 속성이 없으면 종전과 완전히 같다.
30
+ */
31
+ lines?: number;
25
32
  protected updated(changedProperties: PropertyValues): void;
26
33
  render(): import('lit-html').TemplateResult<1>;
27
34
  }
@@ -20,7 +20,9 @@ var USkeleton = class USkeleton extends UElement {
20
20
  if (changedProperties.has("height") && this.height) this.style.setProperty("--skeleton-height", this.height);
21
21
  }
22
22
  render() {
23
- return html`<slot></slot>`;
23
+ const n = this.lines ?? 0;
24
+ if (n < 2) return html`<slot></slot>`;
25
+ return html`${Array.from({ length: n }, () => html`<span class="line" part="line"></span>`)}`;
24
26
  }
25
27
  };
26
28
  __decorate([property({
@@ -33,6 +35,10 @@ __decorate([property({
33
35
  }), __decorateMetadata("design:type", Object)], USkeleton.prototype, "effect", void 0);
34
36
  __decorate([property({ type: String }), __decorateMetadata("design:type", String)], USkeleton.prototype, "width", void 0);
35
37
  __decorate([property({ type: String }), __decorateMetadata("design:type", String)], USkeleton.prototype, "height", void 0);
38
+ __decorate([property({
39
+ type: Number,
40
+ reflect: true
41
+ }), __decorateMetadata("design:type", Number)], USkeleton.prototype, "lines", void 0);
36
42
  USkeleton = __decorate([customElement("u-skeleton")], USkeleton);
37
43
  //#endregion
38
44
  export { USkeleton };