@iyulab/components 1.38.0 → 1.40.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 (109) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/README.md +24 -0
  3. package/dist/_virtual/{_@oxc-project_runtime@0.148.0 → _@oxc-project_runtime@0.149.0}/helpers/esm/decorate.js +1 -1
  4. package/dist/_virtual/{_@oxc-project_runtime@0.148.0 → _@oxc-project_runtime@0.149.0}/helpers/esm/decorateMetadata.js +1 -1
  5. package/dist/_virtual/_glob-assets_raw/dark.css.e8ac080d.js +1 -1
  6. package/dist/_virtual/_glob-assets_raw/light.css.637cb2f8.js +1 -1
  7. package/dist/assets/locales/ar.json.js +6 -2
  8. package/dist/assets/locales/de.json.js +6 -2
  9. package/dist/assets/locales/en.json.js +6 -2
  10. package/dist/assets/locales/es.json.js +6 -2
  11. package/dist/assets/locales/fr.json.js +6 -2
  12. package/dist/assets/locales/id.json.js +6 -2
  13. package/dist/assets/locales/ja.json.js +6 -2
  14. package/dist/assets/locales/ko.json.js +6 -2
  15. package/dist/assets/locales/pt-BR.json.js +6 -2
  16. package/dist/assets/locales/ru.json.js +6 -2
  17. package/dist/assets/locales/th.json.js +6 -2
  18. package/dist/assets/locales/vi.json.js +6 -2
  19. package/dist/assets/locales/zh-CN.json.js +6 -2
  20. package/dist/assets/locales/zh-TW.json.js +6 -2
  21. package/dist/components/UFloatingElement.js +2 -2
  22. package/dist/components/UFormControlElement.js +2 -2
  23. package/dist/components/UOverlayElement.js +2 -2
  24. package/dist/components/alert/UAlert.js +2 -2
  25. package/dist/components/avatar/UAvatar.js +2 -2
  26. package/dist/components/badge/UBadge.js +2 -2
  27. package/dist/components/breadcrumb/UBreadcrumb.js +1 -1
  28. package/dist/components/breadcrumb/UBreadcrumb.styles.js +1 -1
  29. package/dist/components/breadcrumb-item/UBreadcrumbItem.js +2 -2
  30. package/dist/components/breadcrumb-item/UBreadcrumbItem.styles.js +1 -1
  31. package/dist/components/button/UButton.js +2 -2
  32. package/dist/components/button-group/UButtonGroup.js +2 -2
  33. package/dist/components/card/UCard.js +2 -2
  34. package/dist/components/carousel/UCarousel.js +2 -2
  35. package/dist/components/checkbox/UCheckbox.js +2 -2
  36. package/dist/components/checkbox/UCheckbox.styles.js +7 -1
  37. package/dist/components/chip/UChip.js +2 -2
  38. package/dist/components/chip/UChip.styles.js +7 -0
  39. package/dist/components/copy-button/UCopyButton.js +2 -2
  40. package/dist/components/date-picker/UDatePicker.js +2 -2
  41. package/dist/components/date-picker/UDatePicker.styles.js +2 -2
  42. package/dist/components/dialog/UDialog.js +2 -2
  43. package/dist/components/divider/UDivider.js +2 -2
  44. package/dist/components/divider/UDivider.styles.js +1 -1
  45. package/dist/components/drawer/UDrawer.js +2 -2
  46. package/dist/components/expander/UExpander.js +2 -2
  47. package/dist/components/expander/UExpander.styles.js +1 -1
  48. package/dist/components/field/UField.js +2 -2
  49. package/dist/components/field/UField.styles.js +1 -1
  50. package/dist/components/file-input/UFileInput.js +2 -2
  51. package/dist/components/file-input/UFileInput.styles.js +2 -2
  52. package/dist/components/form/UForm.js +2 -2
  53. package/dist/components/icon/UIcon.js +3 -3
  54. package/dist/components/icon-button/UIconButton.js +2 -2
  55. package/dist/components/input/UInput.d.ts +15 -1
  56. package/dist/components/input/UInput.js +62 -12
  57. package/dist/components/input/UInput.styles.js +25 -6
  58. package/dist/components/menu/UMenu.js +2 -2
  59. package/dist/components/menu-item/UMenuItem.js +2 -2
  60. package/dist/components/option/UOption.js +2 -2
  61. package/dist/components/panel/UPanel.js +2 -2
  62. package/dist/components/popover/UPopover.js +23 -7
  63. package/dist/components/progress-bar/UProgressBar.js +2 -2
  64. package/dist/components/progress-ring/UProgressRing.js +2 -2
  65. package/dist/components/radio/URadio.js +2 -2
  66. package/dist/components/rating/URating.js +2 -2
  67. package/dist/components/rating/URating.styles.js +6 -0
  68. package/dist/components/select/USelect.d.ts +7 -1
  69. package/dist/components/select/USelect.js +19 -8
  70. package/dist/components/select/USelect.styles.js +18 -5
  71. package/dist/components/skeleton/USkeleton.js +2 -2
  72. package/dist/components/slider/USlider.d.ts +3 -1
  73. package/dist/components/slider/USlider.js +2 -2
  74. package/dist/components/slider/USlider.styles.js +19 -2
  75. package/dist/components/spinner/USpinner.js +2 -2
  76. package/dist/components/split-panel/USplitPanel.d.ts +37 -2
  77. package/dist/components/split-panel/USplitPanel.js +120 -11
  78. package/dist/components/split-panel/USplitPanel.styles.js +37 -6
  79. package/dist/components/switch/USwitch.js +2 -2
  80. package/dist/components/switch/USwitch.styles.js +8 -3
  81. package/dist/components/tab/UTab.js +2 -2
  82. package/dist/components/tab/UTab.styles.js +1 -1
  83. package/dist/components/tab-panel/UTabPanel.js +3 -3
  84. package/dist/components/tag/UTag.js +2 -2
  85. package/dist/components/text/UText.js +2 -2
  86. package/dist/components/text/UText.styles.js +1 -1
  87. package/dist/components/textarea/UTextarea.d.ts +5 -0
  88. package/dist/components/textarea/UTextarea.js +2 -2
  89. package/dist/components/textarea/UTextarea.styles.js +6 -3
  90. package/dist/components/tooltip/UTooltip.js +2 -2
  91. package/dist/components/tree/UTree.js +2 -2
  92. package/dist/components/tree-item/UTreeItem.js +2 -2
  93. package/dist/components/tree-item/UTreeItem.styles.js +50 -13
  94. package/dist/styles/dark.css +4 -1
  95. package/dist/styles/light.css +7 -4
  96. package/dist/utilities/Locale.d.ts +1 -1
  97. package/dist/utilities/OverlayManager.d.ts +36 -4
  98. package/dist/utilities/OverlayManager.js +39 -4
  99. package/dist/utilities/Toast.d.ts +55 -4
  100. package/dist/utilities/Toast.js +84 -19
  101. package/package.json +4 -4
  102. package/skills/iyulab-components/references/components/dialog.md +19 -0
  103. package/skills/iyulab-components/references/components/input.md +17 -0
  104. package/skills/iyulab-components/references/components/panel.md +12 -0
  105. package/skills/iyulab-components/references/components/select.md +9 -0
  106. package/skills/iyulab-components/references/components/slider.md +1 -1
  107. package/skills/iyulab-components/references/components/split-panel.md +41 -3
  108. package/skills/iyulab-components/references/components/tab-panel.md +18 -0
  109. package/skills/iyulab-components/references/utilities/overlay-manager.md +16 -2
@@ -58,49 +58,86 @@ var styles = css`
58
58
  white-space: nowrap;
59
59
  }
60
60
 
61
- /* ── toggler ── */
61
+ /* ── toggler ──
62
+ The box that takes the pointer is 24x24 (WCAG 2.2 SC 2.5.8); the square you see is still
63
+ 18x18, drawn by the pseudo-element. The negative margins give the extra 3px on each side
64
+ back, so the glyph, the label and the row height sit exactly where they did — the extra
65
+ area lands on the header's own padding and the gap before the label, never on another
66
+ target. */
62
67
  .prefix-toggler {
68
+ position: relative;
69
+ box-sizing: border-box;
63
70
  flex-shrink: 0;
64
- margin-right: 4px;
71
+ margin: -3px 1px -3px -3px;
65
72
  display: inline-flex;
66
73
  align-items: center;
67
74
  justify-content: center;
68
- width: 18px;
69
- height: 18px;
75
+ width: 24px;
76
+ height: 24px;
77
+ }
78
+ .prefix-toggler::before {
79
+ content: "";
80
+ position: absolute;
81
+ inset: 3px;
70
82
  border-radius: var(--u-radius-sm, 3px);
83
+ transition: background-color var(--u-duration-fast, 140ms) var(--u-ease-standard, cubic-bezier(0.2, 0, 0, 1));
84
+ }
85
+ .prefix-toggler > * {
86
+ position: relative;
71
87
  }
72
88
  .prefix-toggler u-icon {
73
89
  font-size: 12px;
74
90
  pointer-events: none;
75
91
  }
76
- :host(:not([disabled])[trigger="icon"]) .prefix-toggler:hover,
77
- :host(:not([disabled])[trigger="icon"]:focus-visible) .prefix-toggler {
92
+ :host(:not([disabled])[trigger="icon"]) .prefix-toggler:hover::before,
93
+ :host(:not([disabled])[trigger="icon"]:focus-visible) .prefix-toggler::before {
78
94
  background-color: var(--u-bg-color-hover, #F5F5F5);
79
95
  }
96
+ /* A checkable row puts the checkbox 4px after the toggle. Centred 24px areas for both would
97
+ overlap by 3px, so here the toggle takes its extra width entirely on the left (header
98
+ padding), and the padding keeps the glyph centred on its visible square. */
99
+ :host([checkable]) .prefix-toggler {
100
+ margin: -3px 4px -3px -6px;
101
+ padding-left: 6px;
102
+ }
103
+ :host([checkable]) .prefix-toggler::before {
104
+ inset: 3px 0 3px 6px;
105
+ }
80
106
 
81
- /* ── checkbox ── */
107
+ /* ── checkbox ──
108
+ Same split as the toggle: a 24x24 box takes the pointer, the 16x16 box you see is the
109
+ pseudo-element, and the negative margins keep every visible edge where it was. */
82
110
  .prefix-checkbox {
111
+ position: relative;
112
+ box-sizing: border-box;
83
113
  flex-shrink: 0;
84
- margin-right: 4px;
114
+ margin: -4px 0 -4px -4px;
85
115
  display: inline-flex;
86
116
  align-items: center;
87
117
  justify-content: center;
88
- width: 16px;
89
- height: 16px;
118
+ width: 24px;
119
+ height: 24px;
120
+ cursor: pointer;
121
+ }
122
+ .prefix-checkbox::before {
123
+ content: "";
124
+ position: absolute;
125
+ inset: 4px;
126
+ box-sizing: border-box;
90
127
  border-radius: var(--u-radius-sm, 3px);
91
128
  border: 1.5px solid var(--u-input-border-color, #E0E0E0);
92
129
  background-color: transparent;
93
130
  transition: background-color var(--u-duration-fast, 140ms) var(--u-ease-standard, cubic-bezier(0.2, 0, 0, 1)), border-color var(--u-duration-fast, 140ms) var(--u-ease-standard, cubic-bezier(0.2, 0, 0, 1));
94
- cursor: pointer;
95
131
  }
96
132
  .prefix-checkbox u-icon {
133
+ position: relative;
97
134
  visibility: hidden;
98
135
  font-size: 12px;
99
136
  color: #fff;
100
137
  pointer-events: none;
101
138
  }
102
- .prefix-checkbox[checked],
103
- .prefix-checkbox[indeterminate] {
139
+ .prefix-checkbox[checked]::before,
140
+ .prefix-checkbox[indeterminate]::before {
104
141
  border-color: var(--tree-item-color);
105
142
  background-color: var(--tree-item-color);
106
143
  }
@@ -242,7 +242,10 @@
242
242
  --u-txt-color-hover: var(--u-primary-color-strong);
243
243
  --u-txt-color-active: var(--u-primary-color-strong);
244
244
  --u-txt-color-disabled: var(--u-neutral-500);
245
- --u-txt-color-weak: var(--u-neutral-700);
245
+ /* 라이트와 같은 기준 — 놓이는 모든 면에서 AA. neutral-700(#8A8A8A)은 바탕 5.43 ·
246
+ 올림면(neutral-300) 4.16 · active 면(neutral-400) 3.15 로 미달이었다.
247
+ neutral-800(#B0B0B0) = 8.64 · 6.62 · 5.01. 본문(neutral-900)과의 대비비 1.46. */
248
+ --u-txt-color-weak: var(--u-neutral-800);
246
249
  --u-txt-color-strong: var(--u-neutral-1000);
247
250
 
248
251
  --u-link-txt-color: var(--u-primary-color-strong);
@@ -252,10 +252,13 @@
252
252
  --u-txt-color-hover: var(--u-primary-color-strong);
253
253
  --u-txt-color-active: var(--u-primary-color-strong);
254
254
  --u-txt-color-disabled: var(--u-neutral-400);
255
- /* neutral-500(#9E9E9E)은 흰 배경 대비 2.68 로 WCAG AA(4.5) 미달이었다.
256
- neutral-600(#757575) = 4.61 로 통과한다. 다크 매핑(neutral-700 = 5.43)은
257
- 원래 정상이라 손대지 않는다 — 라이트 한쪽만의 결함이었다. */
258
- --u-txt-color-weak: var(--u-neutral-600);
255
+ /* 보조 텍스트는 «놓이는 모든 면»에서 AA(4.5)를 받쳐야 한다 — 바탕만이 아니다.
256
+ neutral-600(#757575)은 바탕 4.61 에서만 통과하고 올림면(neutral-50) 4.41 ·
257
+ active 면(neutral-200) 3.97 로 미달이었다(표 머리·카드 위 보조 텍스트).
258
+ neutral-700(#616161) = 바탕 6.19 · 올림면 5.93 · active 5.34.
259
+ 본문(neutral-900)과의 대비비는 2.60 으로, 보조 텍스트를 모든 표면 위에서
260
+ 읽히게 보장하는 흔한 디자인 시스템 기준(본문 대비 약 1.3~1.8)보다 위계가 넓다. */
261
+ --u-txt-color-weak: var(--u-neutral-700);
259
262
  --u-txt-color-strong: var(--u-neutral-1000);
260
263
 
261
264
  --u-link-txt-color: var(--u-primary-color-strong);
@@ -12,7 +12,7 @@ export type SupportedLocale = 'en' | 'ko' | 'ja' | 'zh-CN' | 'zh-TW' | 'es' | 'f
12
12
  /** 내장 로케일은 자동완성되지만, 임의의 BCP47 태그도 그대로 받는다. */
13
13
  export type LocaleTag = SupportedLocale | (string & {});
14
14
  /** 라이브러리가 제공하는 chrome 문자열 키. 지금은 검증 메시지뿐이지만 이후 다른 UI 문구도 추가될 수 있다. */
15
- export type LocaleMessageKey = 'valueMissing' | 'badInput' | 'typeMismatch' | 'patternMismatch' | 'rangeUnderflow' | 'rangeOverflow' | 'stepMismatch' | 'tooShort' | 'tooLong' | 'increment' | 'decrement' | 'clear' | 'today' | 'time' | 'showPassword' | 'hidePassword' | 'previousSlide' | 'nextSlide' | 'goToSlide' | 'chooseDate' | 'previousMonth' | 'nextMonth' | 'close' | 'copy' | 'copied' | 'chooseFile' | 'noFileChosen' | 'filesSelected';
15
+ export type LocaleMessageKey = 'valueMissing' | 'badInput' | 'typeMismatch' | 'patternMismatch' | 'rangeUnderflow' | 'rangeOverflow' | 'stepMismatch' | 'tooShort' | 'tooLong' | 'increment' | 'decrement' | 'clear' | 'today' | 'time' | 'showPassword' | 'hidePassword' | 'previousSlide' | 'nextSlide' | 'goToSlide' | 'chooseDate' | 'previousMonth' | 'nextMonth' | 'close' | 'copy' | 'copied' | 'chooseFile' | 'noFileChosen' | 'filesSelected' | 'resizePanels' | 'search';
16
16
  type LocaleTable = Record<LocaleMessageKey, string>;
17
17
  /**
18
18
  * 한 패키지(또는 앱 영역)의 문자열 묶음.
@@ -1,17 +1,49 @@
1
1
  import { FocusTrap } from 'focus-trap';
2
2
  /**
3
- * OverlayManager는 열린 오버레이의 스택을 관리합니다.
3
+ * OverlayManager는 **겹치는 표면 전체의 층(layer)을 소유**합니다.
4
4
  *
5
- * - 오버레이 스택 순서 관리 (z-index 자동 증가)
5
+ * - 오버레이 스택 순서 관리 (z-index 자동 할당)
6
+ * - **알림 층 z-index 제공** — 알림은 오버레이의 형제가 아니라 그 «위» 채널이다
6
7
  * - body scroll lock 참조 카운팅
7
8
  * - topmost 판별 (ESC 키 처리용)
8
9
  * - focus-trap trapStack 공유
10
+ *
11
+ * ## 층 스케일 — 왜 두 개의 «띠»인가
12
+ *
13
+ * 겹침을 컴포넌트마다 따로 정하면 어긋난다. 실측(2026-09-08)으로 `Toast` 가 자기 컨테이너에
14
+ * `z-index: 9999` 를 박고 있었고 이 매니저가 오버레이에 **9999 «초과»** 를 주고 있어,
15
+ * ***모달 안에서 띄운 오류 토스트가 구조적으로 항상 가려졌다.*** 알림이 도달하지 않는 것은
16
+ * 조용한 실패라 아무도 보지 못했다.
17
+ *
18
+ * ⇒ 층을 **이름 있는 띠**로 고정한다(디자인 시스템의 표준 관용구다 — 겹침은 협상이 아니라
19
+ * 계약이다):
20
+ *
21
+ * | 띠 | 범위 | 소유 |
22
+ * |---|---|---|
23
+ * | 오버레이 | `9999` ~ `9999 + OVERLAY_BAND` | `u-dialog`·`u-drawer` 등 `UOverlayElement` 계열 |
24
+ * | **알림** | `notificationZIndex` | `Toast` — **오버레이 띠보다 항상 위** |
25
+ *
26
+ * ⚠**띠에 상한이 있는 것이 이 계약의 핵심이다.** 종전 `zCounter` 는 «지금까지 열린 총합»이라
27
+ * 단조 증가해 **상한이 없었고**, 상한이 없으면 «알림이 항상 위»를 어떤 상수로도 보장할 수
28
+ * 없다. 이제 **동시에 열린 깊이**(`stack.length`)로 할당하므로 띠 안에 갇힌다.
29
+ * 동시 오버레이가 `OVERLAY_BAND` 를 넘으면 그 이상은 같은 값을 공유한다(서로 간 겹침 순서만
30
+ * 포기하고, **알림이 위**라는 불변식은 유지된다) — 실제 앱이 도달하는 상태가 아니지만,
31
+ * 도달하더라도 무엇이 깨지는지 정해져 있는 편이 낫다.
9
32
  */
10
33
  export declare class OverlayManager {
11
34
  /** 열린 오버레이 스택 */
12
35
  private static readonly stack;
13
- /** z-index 카운터 */
14
- private static zCounter;
36
+ /** 오버레이 띠의 시작 값 */
37
+ private static readonly OVERLAY_BASE;
38
+ /** 오버레이 띠의 폭 — 이 수를 넘는 «동시» 오버레이는 최상단 값을 공유한다 */
39
+ private static readonly OVERLAY_BAND;
40
+ /**
41
+ * 알림 층의 z-index. **오버레이 띠 전체보다 항상 위**임이 보장된다.
42
+ *
43
+ * 토스트·스낵바처럼 «오버레이 위에서도 반드시 보여야 하는» 표면이 쓴다. 직접 상수를
44
+ * 박지 말고 이 값을 읽을 것 — 그래야 띠가 조정돼도 따라온다.
45
+ */
46
+ static get notificationZIndex(): number;
15
47
  /** body scroll lock 참조 카운트 */
16
48
  private static lockCount;
17
49
  /** scroll lock 이전 body overflow 값 */
@@ -1,18 +1,53 @@
1
1
  //#region src/utilities/OverlayManager.ts
2
2
  /**
3
- * OverlayManager는 열린 오버레이의 스택을 관리합니다.
3
+ * OverlayManager는 **겹치는 표면 전체의 층(layer)을 소유**합니다.
4
4
  *
5
- * - 오버레이 스택 순서 관리 (z-index 자동 증가)
5
+ * - 오버레이 스택 순서 관리 (z-index 자동 할당)
6
+ * - **알림 층 z-index 제공** — 알림은 오버레이의 형제가 아니라 그 «위» 채널이다
6
7
  * - body scroll lock 참조 카운팅
7
8
  * - topmost 판별 (ESC 키 처리용)
8
9
  * - focus-trap trapStack 공유
10
+ *
11
+ * ## 층 스케일 — 왜 두 개의 «띠»인가
12
+ *
13
+ * 겹침을 컴포넌트마다 따로 정하면 어긋난다. 실측(2026-09-08)으로 `Toast` 가 자기 컨테이너에
14
+ * `z-index: 9999` 를 박고 있었고 이 매니저가 오버레이에 **9999 «초과»** 를 주고 있어,
15
+ * ***모달 안에서 띄운 오류 토스트가 구조적으로 항상 가려졌다.*** 알림이 도달하지 않는 것은
16
+ * 조용한 실패라 아무도 보지 못했다.
17
+ *
18
+ * ⇒ 층을 **이름 있는 띠**로 고정한다(디자인 시스템의 표준 관용구다 — 겹침은 협상이 아니라
19
+ * 계약이다):
20
+ *
21
+ * | 띠 | 범위 | 소유 |
22
+ * |---|---|---|
23
+ * | 오버레이 | `9999` ~ `9999 + OVERLAY_BAND` | `u-dialog`·`u-drawer` 등 `UOverlayElement` 계열 |
24
+ * | **알림** | `notificationZIndex` | `Toast` — **오버레이 띠보다 항상 위** |
25
+ *
26
+ * ⚠**띠에 상한이 있는 것이 이 계약의 핵심이다.** 종전 `zCounter` 는 «지금까지 열린 총합»이라
27
+ * 단조 증가해 **상한이 없었고**, 상한이 없으면 «알림이 항상 위»를 어떤 상수로도 보장할 수
28
+ * 없다. 이제 **동시에 열린 깊이**(`stack.length`)로 할당하므로 띠 안에 갇힌다.
29
+ * 동시 오버레이가 `OVERLAY_BAND` 를 넘으면 그 이상은 같은 값을 공유한다(서로 간 겹침 순서만
30
+ * 포기하고, **알림이 위**라는 불변식은 유지된다) — 실제 앱이 도달하는 상태가 아니지만,
31
+ * 도달하더라도 무엇이 깨지는지 정해져 있는 편이 낫다.
9
32
  */
10
33
  var OverlayManager = class {
11
34
  static {
12
35
  this.stack = [];
13
36
  }
14
37
  static {
15
- this.zCounter = 0;
38
+ this.OVERLAY_BASE = 9999;
39
+ }
40
+ static {
41
+ this.OVERLAY_BAND = 1e3;
42
+ }
43
+ /**
44
+ * 알림 층의 z-index. **오버레이 띠 전체보다 항상 위**임이 보장된다.
45
+ *
46
+ * 토스트·스낵바처럼 «오버레이 위에서도 반드시 보여야 하는» 표면이 쓴다. 직접 상수를
47
+ * 박지 말고 이 값을 읽을 것 — 그래야 띠가 조정돼도 따라온다.
48
+ */
49
+ static get notificationZIndex() {
50
+ return this.OVERLAY_BASE + this.OVERLAY_BAND + 1;
16
51
  }
17
52
  static {
18
53
  this.lockCount = 0;
@@ -38,7 +73,7 @@ var OverlayManager = class {
38
73
  */
39
74
  static add(overlay, lockBody = true) {
40
75
  this.stack.push(overlay);
41
- overlay.style.zIndex = String(9999 + ++this.zCounter);
76
+ overlay.style.zIndex = String(this.OVERLAY_BASE + Math.min(this.stack.length, this.OVERLAY_BAND));
42
77
  if (lockBody) {
43
78
  if (this.lockCount === 0) {
44
79
  this.savedOverflow = document.body.style.overflow;
@@ -20,8 +20,20 @@ export interface ToastOptions {
20
20
  * 토스트 알림 유틸리티 클래스입니다.
21
21
  */
22
22
  export declare class Toast {
23
+ /**
24
+ * 위치별 컨테이너를 **타깃 엘리먼트 자신에 키잉**한다.
25
+ *
26
+ * 종전에는 `"<position>@<id>"` 문자열이 키였고, 그 파생이 세 가지를 동시에 깨뜨렸다:
27
+ * ⑴같은 `id`를 가진 «다른» 엘리먼트가 같은 키로 충돌한다(SPA 라우트 교체가 화면
28
+ * 엘리먼트를 같은 `id`로 다시 만드는 것은 평범한 패턴이다) ⑵`id`가 없으면 폴백이
29
+ * `el-${Date.now()}` 라 호출마다 키가 달라져 캐시가 성립하지 않고 컨테이너가 쌓인다
30
+ * ⑶그 불안정한 키를 `hide` 정리 경로가 **다시 계산**하므로 `containers.delete()` 가
31
+ * 빗나가 항목이 영구히 남는다.
32
+ *
33
+ * `WeakMap` 은 셋을 전부 구조적으로 없앤다 — 엘리먼트 동일성이 곧 키라 충돌이 불가능하고,
34
+ * `id` 유무에 의존하지 않으며, 타깃이 버려지면 그 항목도 함께 수거된다.
35
+ */
23
36
  private static containers;
24
- private static elements;
25
37
  /**
26
38
  * 모든 Toast 호출에 적용될 전역 기본 옵션입니다. 개별 호출의 `options`가 우선합니다.
27
39
  *
@@ -47,8 +59,47 @@ export declare class Toast {
47
59
  static error(content: string, options?: ToastOptions): Promise<void>;
48
60
  /** 토스트 알림을 생성합니다. */
49
61
  static show(status?: AlertStatus, content?: string, options?: ToastOptions): Promise<void>;
50
- /** 컨테이너 키를 생성합니다. */
51
- private static getContainerKey;
52
- /** 위치에 맞는 컨테이너 엘리먼트를 가져오거나, 생성합니다. */
62
+ /**
63
+ * 위치에 맞는 컨테이너 엘리먼트를 가져오거나, 생성합니다.
64
+ *
65
+ * ⚠**캐시 적중은 그 컨테이너가 «여전히 쓸 수 있는가»를 확인한 뒤에만 유효하다.** 호스트가
66
+ * 갈아끼워지면(`body.innerHTML = ''`, 셸 재구축) 캐시된 컨테이너는 문서에서 떨어진 채
67
+ * 남는데, 거기에 append 된 엘리먼트는 **연결되지 않아 `updateComplete` 가 영원히 해소되지
68
+ * 않는다**. 낡은 항목은 버리고 새로 만든다.
69
+ */
53
70
  private static getOrCreateContainer;
71
+ /**
72
+ * 알림 컨테이너를 **네이티브 top layer** 로 올린다(`popover="manual"`).
73
+ *
74
+ * ## 왜 — z-index 로는 원리적으로 닿지 않는 자리가 있다
75
+ *
76
+ * `OverlayManager` 의 «이름 있는 띠» 계약은 **우리 오버레이보다 위**를 보장한다(실측:
77
+ * 컨테이너 `z-index: 11000`). 그러나 ***top layer 는 z-index 축 밖에 있다*** — 소비앱이
78
+ * 네이티브 `<dialog>.showModal()` 을 열면 그 다이얼로그는 **어떤 z-index 보다도 위**에
79
+ * 서고, 우리 토스트는 가려진다. 실측(cycle-477, 크로미움 `elementFromPoint`):
80
+ *
81
+ * ```
82
+ * containerZIndex 11000 · notificationZIndex 11000 · hitTag "DIALOG" · toastReachable false
83
+ * ```
84
+ *
85
+ * 알림 채널의 제1원칙이 ***«지각되지 않는 경보는 경보가 아니다»*** 이므로, 그 채널은 겹침을
86
+ * **다투지 않고** 전용 층을 가져야 한다. `popover="manual"` 은 dismiss 계약도 포커스 트랩도
87
+ * 걸지 않고 **top layer 승격만** 하므로 이 표면에 정확히 맞는다.
88
+ *
89
+ * ## ⚠ 범위 — `document.body` 컨테이너만이다
90
+ *
91
+ * `target` 이 주어진 컨테이너는 `position: absolute` 로 **그 엘리먼트 기준**으로 놓인다.
92
+ * top layer 원소는 조상의 배치 문맥에서 떨어져 나오므로 그 좌표계가 통째로 깨진다 ⇒
93
+ * 타깃 기준 컨테이너는 **종전 z-index 경로를 그대로 쓴다.** 이것이 시범 범위이고,
94
+ * 오버레이 전면 전환(`u-dialog`·`u-drawer` 등)은 별개 결정이다(`ROADMAP.md` §C-B).
95
+ *
96
+ * ## ⚠ z-index 를 지우지 않는다
97
+ *
98
+ * `showPopover` 가 없는 브라우저(또는 호출이 거부되는 상태)에서는 **아무 일도 일어나지
99
+ * 않고 종전 동작이 그대로 남아야** 한다 — 그래서 승격은 순수 «추가»이고, 실패는 조용히
100
+ * 삼킨다(알림을 띄우려다 예외를 던지는 것은 이 표면에서 최악의 실패다).
101
+ */
102
+ private static preparePopover;
103
+ /** 연결된 뒤에만 부를 수 있다 — 실패하면 종전 동작(z-index 띠)으로 조용히 돌아간다. */
104
+ private static showTopLayer;
54
105
  }
@@ -1,3 +1,4 @@
1
+ import { OverlayManager } from "./OverlayManager.js";
1
2
  import { UAlert } from "../components/alert/UAlert.js";
2
3
  //#region src/utilities/Toast.ts
3
4
  /**
@@ -5,10 +6,7 @@ import { UAlert } from "../components/alert/UAlert.js";
5
6
  */
6
7
  var Toast = class {
7
8
  static {
8
- this.containers = /* @__PURE__ */ new Map();
9
- }
10
- static {
11
- this.elements = /* @__PURE__ */ new Set();
9
+ this.containers = /* @__PURE__ */ new WeakMap();
12
10
  }
13
11
  static {
14
12
  this.DefaultOptions = {};
@@ -52,39 +50,50 @@ var Toast = class {
52
50
  el.title = merged.title || "";
53
51
  el.closable = merged.closable ?? true;
54
52
  el.duration = merged.duration ?? 4e3;
55
- this.elements.add(el);
56
53
  const position = merged.position || "top-right";
57
54
  const target = merged.target || document.body;
58
55
  const container = this.getOrCreateContainer(position, target);
59
56
  container.appendChild(el);
57
+ if (!el.isConnected) {
58
+ container.removeChild(el);
59
+ console.warn("[@iyulab/components] Toast was not shown: the target element is not in the document.\n A toast can only render inside a connected element — pass a target that is attached,\n or omit `target` to use document.body.");
60
+ return;
61
+ }
60
62
  await el.updateComplete;
61
63
  el.show();
62
64
  el.addEventListener("hide", async (e) => {
63
65
  if (e.target !== el) return;
64
66
  await new Promise((resolve) => setTimeout(resolve, 200));
65
67
  el.remove();
66
- this.elements.delete(el);
67
68
  if (!container.hasChildNodes()) {
68
- const containerKey = this.getContainerKey(position, target);
69
69
  container.remove();
70
- this.containers.delete(containerKey);
70
+ this.containers.get(target)?.delete(position);
71
71
  }
72
72
  });
73
73
  }
74
- /** 컨테이너 키를 생성합니다. */
75
- static getContainerKey(position, target) {
76
- return `${position}@${target === document.body ? "body" : target.id || `el-${Date.now()}`}`;
77
- }
78
- /** 위치에 맞는 컨테이너 엘리먼트를 가져오거나, 생성합니다. */
74
+ /**
75
+ * 위치에 맞는 컨테이너 엘리먼트를 가져오거나, 생성합니다.
76
+ *
77
+ * ⚠**캐시 적중은 그 컨테이너가 «여전히 쓸 수 있는가»를 확인한 뒤에만 유효하다.** 호스트가
78
+ * 갈아끼워지면(`body.innerHTML = ''`, 셸 재구축) 캐시된 컨테이너는 문서에서 떨어진 채
79
+ * 남는데, 거기에 append 된 엘리먼트는 **연결되지 않아 `updateComplete` 가 영원히 해소되지
80
+ * 않는다**. 낡은 항목은 버리고 새로 만든다.
81
+ */
79
82
  static getOrCreateContainer(position, target) {
80
- const key = this.getContainerKey(position, target);
81
- let container = this.containers.get(key);
82
- if (container) return container;
83
- container = document.createElement("div");
84
- container.style.zIndex = "9999";
83
+ let byPosition = this.containers.get(target);
84
+ if (!byPosition) {
85
+ byPosition = /* @__PURE__ */ new Map();
86
+ this.containers.set(target, byPosition);
87
+ }
88
+ const cached = byPosition.get(position);
89
+ if (cached && cached.isConnected && cached.parentNode === target) return cached;
90
+ if (cached) byPosition.delete(position);
91
+ const container = document.createElement("div");
92
+ container.style.zIndex = String(OverlayManager.notificationZIndex);
85
93
  container.style.display = "flex";
86
94
  container.style.gap = "10px";
87
95
  const isTargeted = target !== document.body;
96
+ const popoverReady = isTargeted ? false : this.preparePopover(container);
88
97
  if (isTargeted) {
89
98
  container.style.position = "absolute";
90
99
  if (getComputedStyle(target).position === "static") target.style.position = "relative";
@@ -114,9 +123,65 @@ var Toast = class {
114
123
  }
115
124
  container.style.transform = transformParts.length ? transformParts.join(" ") : "";
116
125
  target.appendChild(container);
117
- this.containers.set(key, container);
126
+ if (popoverReady) this.showTopLayer(container);
127
+ byPosition.set(position, container);
118
128
  return container;
119
129
  }
130
+ /**
131
+ * 알림 컨테이너를 **네이티브 top layer** 로 올린다(`popover="manual"`).
132
+ *
133
+ * ## 왜 — z-index 로는 원리적으로 닿지 않는 자리가 있다
134
+ *
135
+ * `OverlayManager` 의 «이름 있는 띠» 계약은 **우리 오버레이보다 위**를 보장한다(실측:
136
+ * 컨테이너 `z-index: 11000`). 그러나 ***top layer 는 z-index 축 밖에 있다*** — 소비앱이
137
+ * 네이티브 `<dialog>.showModal()` 을 열면 그 다이얼로그는 **어떤 z-index 보다도 위**에
138
+ * 서고, 우리 토스트는 가려진다. 실측(cycle-477, 크로미움 `elementFromPoint`):
139
+ *
140
+ * ```
141
+ * containerZIndex 11000 · notificationZIndex 11000 · hitTag "DIALOG" · toastReachable false
142
+ * ```
143
+ *
144
+ * 알림 채널의 제1원칙이 ***«지각되지 않는 경보는 경보가 아니다»*** 이므로, 그 채널은 겹침을
145
+ * **다투지 않고** 전용 층을 가져야 한다. `popover="manual"` 은 dismiss 계약도 포커스 트랩도
146
+ * 걸지 않고 **top layer 승격만** 하므로 이 표면에 정확히 맞는다.
147
+ *
148
+ * ## ⚠ 범위 — `document.body` 컨테이너만이다
149
+ *
150
+ * `target` 이 주어진 컨테이너는 `position: absolute` 로 **그 엘리먼트 기준**으로 놓인다.
151
+ * top layer 원소는 조상의 배치 문맥에서 떨어져 나오므로 그 좌표계가 통째로 깨진다 ⇒
152
+ * 타깃 기준 컨테이너는 **종전 z-index 경로를 그대로 쓴다.** 이것이 시범 범위이고,
153
+ * 오버레이 전면 전환(`u-dialog`·`u-drawer` 등)은 별개 결정이다(`ROADMAP.md` §C-B).
154
+ *
155
+ * ## ⚠ z-index 를 지우지 않는다
156
+ *
157
+ * `showPopover` 가 없는 브라우저(또는 호출이 거부되는 상태)에서는 **아무 일도 일어나지
158
+ * 않고 종전 동작이 그대로 남아야** 한다 — 그래서 승격은 순수 «추가»이고, 실패는 조용히
159
+ * 삼킨다(알림을 띄우려다 예외를 던지는 것은 이 표면에서 최악의 실패다).
160
+ */
161
+ static preparePopover(container) {
162
+ if (typeof container.showPopover !== "function") return false;
163
+ container.setAttribute("popover", "manual");
164
+ container.style.inset = "auto";
165
+ container.style.margin = "0";
166
+ container.style.border = "none";
167
+ container.style.padding = "0";
168
+ container.style.background = "transparent";
169
+ container.style.color = "inherit";
170
+ container.style.overflow = "visible";
171
+ container.style.width = "auto";
172
+ container.style.height = "auto";
173
+ container.style.maxWidth = "none";
174
+ container.style.maxHeight = "none";
175
+ return true;
176
+ }
177
+ /** 연결된 뒤에만 부를 수 있다 — 실패하면 종전 동작(z-index 띠)으로 조용히 돌아간다. */
178
+ static showTopLayer(container) {
179
+ try {
180
+ container.showPopover();
181
+ } catch {
182
+ container.removeAttribute("popover");
183
+ }
184
+ }
120
185
  };
121
186
  //#endregion
122
187
  export { Toast };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iyulab/components",
3
3
  "description": "web-components library based on lit-element made by iyulab",
4
- "version": "1.38.0",
4
+ "version": "1.40.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "components",
@@ -83,16 +83,16 @@
83
83
  }
84
84
  },
85
85
  "devDependencies": {
86
- "@eslint/js": "^9.39.4",
86
+ "@eslint/js": "^10.0.1",
87
87
  "@types/node": "^26.1.1",
88
88
  "@types/react": "^19.2.14",
89
89
  "@vitest/browser-playwright": "^4.1.10",
90
- "eslint": "^9.39.4",
90
+ "eslint": "^10.1.0",
91
91
  "eslint-plugin-lit": "^2.3.1",
92
92
  "globals": "^17.7.0",
93
93
  "happy-dom": "^20.10.6",
94
94
  "react": "^19.2.7",
95
- "typescript": "^5.9.3",
95
+ "typescript": "^6.0.2",
96
96
  "typescript-eslint": "^8.64.0",
97
97
  "vite": "^8.1.4",
98
98
  "vite-plugin-dts": "^5.0.3",
@@ -25,6 +25,25 @@ For programmatic usage (`Dialog.alert`, `Dialog.confirm`, `Dialog.prompt`), see
25
25
 
26
26
  ---
27
27
 
28
+ ## Sizing
29
+
30
+ The panel is **content-sized, capped at 90%** of the dialog's own box — 90% of the height and of
31
+ the width. Measured in an 896px-tall viewport: a short dialog renders a 235px panel, and the same
32
+ dialog carrying 3,000px of body content stops at 806px.
33
+
34
+ Past that cap the **body is the only scroll region**. The `header` and `footer` slots are
35
+ `flex-shrink: 0`, so the title and the actions stay visible however long the content grows.
36
+
37
+ There is no size property. To make the dialog smaller than 90% of the viewport, give the **host** a
38
+ height — that is what the 90% is measured against:
39
+
40
+ ```css
41
+ u-dialog { height: 400px; } /* the panel then caps at 360px */
42
+ ```
43
+
44
+ ⚠ That lever only shrinks: a height on the host lowers the panel's maximum, it does not reserve a
45
+ panel of that size. A short dialog stays short.
46
+
28
47
  ## Slots
29
48
 
30
49
  | Name | Description |
@@ -81,6 +81,23 @@ for a KRW amount) is the consumer's call — the library does not infer one from
81
81
  | `input` | Fires on every keystroke |
82
82
  | `change` | Fires when value is committed |
83
83
 
84
+ ## Form submission
85
+
86
+ Pressing Enter in the field submits its `<form>`, as in a native text field (HTML implicit
87
+ submission): the form's first submit button is activated — `<button type="submit">` or
88
+ `<u-button type="submit">`; a disabled one does nothing — and with no submit button the form is
89
+ submitted only when it has no other single-line field. Enter that confirms an IME composition,
90
+ Enter with a modifier key, and Enter on a highlighted suggestion (`u-option`) do not submit.
91
+ Call `preventDefault()` on the `keydown` to cancel it. Do not also submit on Enter yourself.
92
+
93
+ ```html
94
+ <form @submit=${onSubmit}>
95
+ <u-input name="user" required></u-input>
96
+ <u-input name="password" type="password" required></u-input>
97
+ <u-button type="submit">Sign in</u-button>
98
+ </form>
99
+ ```
100
+
84
101
  ## Methods
85
102
 
86
103
  | Method | Description |
@@ -26,6 +26,18 @@ General-purpose content panel. Matches a tab or tree node when using `value`.
26
26
 
27
27
  ---
28
28
 
29
+ ## Sizing
30
+
31
+ `u-panel` has `overflow: auto` and no height of its own, so it behaves two ways — both intended:
32
+
33
+ - **No height** — it grows with its content. Nothing is clipped and no scrollbar appears.
34
+ - **A height (or a height-bearing parent)** — it becomes the scroll container for its content.
35
+
36
+ ```html
37
+ <u-panel style="height: 240px">…</u-panel> <!-- scrolls its content -->
38
+ <u-panel>…</u-panel> <!-- grows with its content -->
39
+ ```
40
+
29
41
  ## Slots
30
42
 
31
43
  | Name | Description |
@@ -70,6 +70,15 @@ Dropdown select with single or multiple selection, search, and clear support. Fo
70
70
  | `focus(options?)` | Focus the trigger |
71
71
  | `blur()` | Blur the trigger |
72
72
 
73
+ ## Keyboard & accessibility
74
+
75
+ The trigger is a `role="combobox"`. Without `searchable` the dropdown holds a `role="listbox"`
76
+ and opening it focuses the first option. With `searchable` the dropdown is a `role="dialog"`
77
+ (named by `label`, or "Search") that holds the search field and the listbox; opening it focuses
78
+ the search field, so typing filters at once. From the search field `ArrowDown`/`ArrowUp` move to
79
+ the first/last visible option; on an option, arrows, `Home`/`End`, `Enter`/`Space` (select) and
80
+ `Escape` (close) work as usual.
81
+
73
82
  ## CSS Parts
74
83
 
75
84
  | Part | Description |
@@ -84,7 +84,7 @@ Range slider. Single-thumb or dual-thumb (range) mode. Supports marks, value dis
84
84
  | `--slider-fill-color` | Track fill color |
85
85
  | `--slider-track-height` | Track height |
86
86
  | `--slider-track-color` | Track background color |
87
- | `--slider-thumb-size` | Thumb diameter |
87
+ | `--slider-thumb-size` | Thumb diameter (**visible circle only** — the pointer target keeps a 24x24 floor per WCAG 2.2 SC 2.5.8, so lowering this does not shrink it) |
88
88
  | `--slider-thumb-color` | Thumb fill color |
89
89
  | `--slider-thumb-border-color` | Thumb border color |
90
90
  | `--slider-mark-size` | Mark dot size |