@iyulab/components 1.37.1 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/dist/utilities/Locale.d.ts +1 -1
- package/dist/utilities/Locale.js +13 -3
- package/dist/utilities/OverlayManager.d.ts +36 -4
- package/dist/utilities/OverlayManager.js +39 -4
- package/dist/utilities/Toast.d.ts +21 -4
- package/dist/utilities/Toast.js +27 -19
- package/dist/utilities/format.d.ts +7 -2
- package/dist/utilities/format.js +7 -2
- package/package.json +1 -1
- package/skills/iyulab-components/SKILL.md +1 -0
- package/skills/iyulab-components/references/utilities/browser-storage.md +5 -0
- package/skills/iyulab-components/references/utilities/elements.md +17 -0
- package/skills/iyulab-components/references/utilities/format.md +73 -0
- package/skills/iyulab-components/references/utilities/locale.md +5 -1
- package/skills/iyulab-components/references/utilities/overlay-manager.md +16 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.39.0] - 2026-09-08
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **A toast raised while a modal is open is no longer hidden behind it.** `Toast` hardcoded
|
|
8
|
+
`z-index: 9999` on its container while `OverlayManager` assigned open overlays a value
|
|
9
|
+
*above* 9999, so a toast could never appear over an open `u-dialog` or `u-drawer` — the
|
|
10
|
+
most common place an application reports a failure. Stacking is now a contract owned by
|
|
11
|
+
`OverlayManager`: overlays are assigned from a bounded band, and the notification layer sits
|
|
12
|
+
above that band by construction. Read `OverlayManager.notificationZIndex` instead of
|
|
13
|
+
hardcoding a value for any surface that must stay visible over an overlay.
|
|
14
|
+
|
|
15
|
+
- **`Toast` no longer returns a promise that never settles, and no longer drops toasts
|
|
16
|
+
silently.** The container cache was keyed by a string derived from the target
|
|
17
|
+
(`"<position>@<id>"`), which collided when a different element reused the same `id`, never
|
|
18
|
+
cached at all for targets without an `id` (the fallback embedded `Date.now()`), and made the
|
|
19
|
+
cleanup path recompute a key that could miss. A cached container that had left the document
|
|
20
|
+
was still handed back, and an element appended to a detached container never connects — so
|
|
21
|
+
its `updateComplete` never resolves and `await Toast.success(...)` hung forever with no error
|
|
22
|
+
and no toast. Containers are now keyed by the target element itself and validated on every
|
|
23
|
+
cache hit. Passing a target that is not in the document now warns and returns instead of
|
|
24
|
+
hanging.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **Overlay z-index is derived from how many overlays are open at once, not from how many have
|
|
29
|
+
ever been opened.** The previous counter increased monotonically and never reset, leaving the
|
|
30
|
+
overlay band unbounded. Relative stacking between simultaneously open overlays is unchanged.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **`OverlayManager.notificationZIndex`** — the z-index of the notification layer, guaranteed to
|
|
35
|
+
sit above the entire overlay band.
|
|
36
|
+
|
|
37
|
+
## [1.38.0] - 2026-09-08
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **`<html lang>` now takes precedence over the browser language when the initial
|
|
42
|
+
locale is detected.** `detectLocale()` consulted `navigator.language` first, which
|
|
43
|
+
made the `document.documentElement.lang` branch unreachable in a browser: a document
|
|
44
|
+
declaring `lang="en"` still emitted accessible names in the browser's language, and a
|
|
45
|
+
screen reader then announced them under English pronunciation rules (WCAG 3.1.1 /
|
|
46
|
+
3.1.2). The `lang` attribute is the author's declaration of the document's language;
|
|
47
|
+
the browser language is the fallback for when that declaration is absent, not an
|
|
48
|
+
override for it. **Only documents that actually set `lang` change behaviour** — an
|
|
49
|
+
absent or empty `lang` falls back to the browser language exactly as before, so apps
|
|
50
|
+
that never set it are unaffected. `Locale.set()` still overrides both. The Node 21+
|
|
51
|
+
guard on the `navigator` branch is unchanged.
|
|
52
|
+
|
|
53
|
+
### Documentation
|
|
54
|
+
|
|
55
|
+
- `formatNumber`, `formatCurrency` and `formatDate` were public through the barrel but
|
|
56
|
+
appeared in no published document; they are now documented.
|
|
57
|
+
- `isCoarsePointer` is documented alongside the other element helpers.
|
|
58
|
+
- `formatDate`'s JSDoc cross-referenced a `formatCurrency` fallback that does not exist.
|
|
59
|
+
`formatCurrency` throws on an invalid currency code, and the asymmetry between the two
|
|
60
|
+
is deliberate; both are now described as they behave.
|
|
61
|
+
- The `BrowserStorage` reference notes that the constructor throws outside a browser.
|
|
62
|
+
|
|
3
63
|
## [1.37.1] - 2026-09-07
|
|
4
64
|
|
|
5
65
|
### Fixed
|
|
@@ -47,7 +47,7 @@ export declare class Locale {
|
|
|
47
47
|
private constructor();
|
|
48
48
|
/** 전역 활성 로케일을 지정합니다. */
|
|
49
49
|
static set(locale: LocaleTag): void;
|
|
50
|
-
/** 전역 활성 로케일을 반환합니다 (초기값은 브라우저 언어 자동 감지, 실패 시 'en'). */
|
|
50
|
+
/** 전역 활성 로케일을 반환합니다 (초기값은 `<html lang>` → 브라우저 언어 자동 감지, 실패 시 'en'). */
|
|
51
51
|
static get(): LocaleTag;
|
|
52
52
|
/**
|
|
53
53
|
* 로케일 하나의 메시지 테이블(전체 또는 일부)을 등록합니다.
|
package/dist/utilities/Locale.js
CHANGED
|
@@ -32,10 +32,20 @@ var builtins = new Map(Object.entries(/* #__PURE__ */ Object.assign({
|
|
|
32
32
|
return [(path.split("/").pop()?.replace(".json", "") || "").toLowerCase(), mod];
|
|
33
33
|
}));
|
|
34
34
|
var overrides = /* @__PURE__ */ new Map();
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* 초기 로케일을 `document.documentElement.lang` → `navigator.language` → `'en'` 순으로 추측한다.
|
|
37
|
+
*
|
|
38
|
+
* ⚠**순서가 계약이다.** `<html lang>` 은 HTML 명세상 **문서 언어에 대한 저자의 선언**이고,
|
|
39
|
+
* 보조기술은 그것으로 발음 규칙을 고른다(WCAG 3.1.1 Language of Page / 3.1.2 Language of
|
|
40
|
+
* Parts). `navigator.language` 는 그 선언이 **없을 때**의 사용자 선호 폴백이지 저자 선언을
|
|
41
|
+
* 덮어쓸 근거가 아니다 — 덮어쓰면 `lang="en"` 문서가 다른 언어의 접근성 이름을 내보내고,
|
|
42
|
+
* 스크린리더는 그것을 영어 발음 규칙으로 읽으려 한다.
|
|
43
|
+
*
|
|
44
|
+
* 자동 감지가 맞지 않는 앱은 `Locale.set()` 으로 언제든 덮어쓸 수 있다.
|
|
45
|
+
*/
|
|
36
46
|
function detectLocale() {
|
|
37
|
-
if (typeof window !== "undefined" && typeof navigator !== "undefined" && navigator.language) return navigator.language;
|
|
38
47
|
if (typeof document !== "undefined" && document.documentElement?.lang) return document.documentElement.lang;
|
|
48
|
+
if (typeof window !== "undefined" && typeof navigator !== "undefined" && navigator.language) return navigator.language;
|
|
39
49
|
return "en";
|
|
40
50
|
}
|
|
41
51
|
var active = detectLocale();
|
|
@@ -72,7 +82,7 @@ var Locale = class {
|
|
|
72
82
|
static set(locale) {
|
|
73
83
|
active = locale;
|
|
74
84
|
}
|
|
75
|
-
/** 전역 활성 로케일을 반환합니다 (초기값은 브라우저 언어 자동 감지, 실패 시 'en'). */
|
|
85
|
+
/** 전역 활성 로케일을 반환합니다 (초기값은 `<html lang>` → 브라우저 언어 자동 감지, 실패 시 'en'). */
|
|
76
86
|
static get() {
|
|
77
87
|
return active;
|
|
78
88
|
}
|
|
@@ -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
|
-
/**
|
|
14
|
-
private static
|
|
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.
|
|
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(
|
|
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,13 @@ 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
|
-
|
|
52
|
-
|
|
62
|
+
/**
|
|
63
|
+
* 위치에 맞는 컨테이너 엘리먼트를 가져오거나, 생성합니다.
|
|
64
|
+
*
|
|
65
|
+
* ⚠**캐시 적중은 그 컨테이너가 «여전히 쓸 수 있는가»를 확인한 뒤에만 유효하다.** 호스트가
|
|
66
|
+
* 갈아끼워지면(`body.innerHTML = ''`, 셸 재구축) 캐시된 컨테이너는 문서에서 떨어진 채
|
|
67
|
+
* 남는데, 거기에 append 된 엘리먼트는 **연결되지 않아 `updateComplete` 가 영원히 해소되지
|
|
68
|
+
* 않는다**. 낡은 항목은 버리고 새로 만든다.
|
|
69
|
+
*/
|
|
53
70
|
private static getOrCreateContainer;
|
|
54
71
|
}
|
package/dist/utilities/Toast.js
CHANGED
|
@@ -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
|
|
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,36 +50,46 @@ 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(
|
|
70
|
+
this.containers.get(target)?.delete(position);
|
|
71
71
|
}
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
/**
|
|
75
|
+
* 위치에 맞는 컨테이너 엘리먼트를 가져오거나, 생성합니다.
|
|
76
|
+
*
|
|
77
|
+
* ⚠**캐시 적중은 그 컨테이너가 «여전히 쓸 수 있는가»를 확인한 뒤에만 유효하다.** 호스트가
|
|
78
|
+
* 갈아끼워지면(`body.innerHTML = ''`, 셸 재구축) 캐시된 컨테이너는 문서에서 떨어진 채
|
|
79
|
+
* 남는데, 거기에 append 된 엘리먼트는 **연결되지 않아 `updateComplete` 가 영원히 해소되지
|
|
80
|
+
* 않는다**. 낡은 항목은 버리고 새로 만든다.
|
|
81
|
+
*/
|
|
79
82
|
static getOrCreateContainer(position, target) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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;
|
|
@@ -114,7 +122,7 @@ var Toast = class {
|
|
|
114
122
|
}
|
|
115
123
|
container.style.transform = transformParts.length ? transformParts.join(" ") : "";
|
|
116
124
|
target.appendChild(container);
|
|
117
|
-
|
|
125
|
+
byPosition.set(position, container);
|
|
118
126
|
return container;
|
|
119
127
|
}
|
|
120
128
|
};
|
|
@@ -19,7 +19,12 @@ export declare function formatCurrency(value: number, currency: string, options?
|
|
|
19
19
|
* already-Invalid `Date`) degrades to `String(value)` rather than throwing —
|
|
20
20
|
* `Intl.DateTimeFormat.format()` throws `RangeError` on an Invalid Date, and this
|
|
21
21
|
* utility is called from render paths where an uncaught throw blanks the whole
|
|
22
|
-
* component.
|
|
23
|
-
*
|
|
22
|
+
* component.
|
|
23
|
+
*
|
|
24
|
+
* ⚠This degrade is **deliberately not symmetric** with {@link formatCurrency}, which
|
|
25
|
+
* throws `RangeError` on an invalid currency code. A date arrives as *data* — from an
|
|
26
|
+
* API, a user, a stale cache — so a bad one is an expected runtime state. A currency
|
|
27
|
+
* code is written by the developer at the call site, so a bad one is a bug that should
|
|
28
|
+
* surface at the first render rather than be papered over with a wrong-looking amount.
|
|
24
29
|
*/
|
|
25
30
|
export declare function formatDate(value: Date | string, options?: Intl.DateTimeFormatOptions, locale?: LocaleTag): string;
|
package/dist/utilities/format.js
CHANGED
|
@@ -49,8 +49,13 @@ function formatCurrency(value, currency, options, locale) {
|
|
|
49
49
|
* already-Invalid `Date`) degrades to `String(value)` rather than throwing —
|
|
50
50
|
* `Intl.DateTimeFormat.format()` throws `RangeError` on an Invalid Date, and this
|
|
51
51
|
* utility is called from render paths where an uncaught throw blanks the whole
|
|
52
|
-
* component.
|
|
53
|
-
*
|
|
52
|
+
* component.
|
|
53
|
+
*
|
|
54
|
+
* ⚠This degrade is **deliberately not symmetric** with {@link formatCurrency}, which
|
|
55
|
+
* throws `RangeError` on an invalid currency code. A date arrives as *data* — from an
|
|
56
|
+
* API, a user, a stale cache — so a bad one is an expected runtime state. A currency
|
|
57
|
+
* code is written by the developer at the call site, so a bad one is a bug that should
|
|
58
|
+
* surface at the first render rather than be papered over with a wrong-looking amount.
|
|
54
59
|
*/
|
|
55
60
|
function formatDate(value, options, locale) {
|
|
56
61
|
const date = resolve(value);
|
package/package.json
CHANGED
|
@@ -126,6 +126,7 @@ import { UButton, UInput } from '@iyulab/components/react';
|
|
|
126
126
|
- [`converters`](./references/utilities/converters.md) — Lit property attribute converters (array, JSON, date, url…)
|
|
127
127
|
- [`Locale`](./references/utilities/locale.md) — Validation-message locale registry and lookup utility
|
|
128
128
|
- [`elements`](./references/utilities/elements.md) — Shadow-DOM-aware DOM query helpers
|
|
129
|
+
- [`format`](./references/utilities/format.md) — Locale-aware number, currency and date formatting
|
|
129
130
|
- [`OverlayManager`](./references/utilities/overlay-manager.md) — Internal overlay stack and z-index manager
|
|
130
131
|
|
|
131
132
|
---
|
|
@@ -11,6 +11,11 @@ Unified key-value storage API over `localStorage` or browser cookies.
|
|
|
11
11
|
writing. `new BrowserStorage({ type: 'cookie' })` throws `"Cookies are not supported in this
|
|
12
12
|
browser."` where it's unavailable; `localStorage` has no such restriction.
|
|
13
13
|
|
|
14
|
+
⚠ The constructor also throws `"BrowserStorage can only be used in a browser environment."`
|
|
15
|
+
when there is no `window` at all. Construct it inside a browser-only path (an event handler,
|
|
16
|
+
`connectedCallback`, an effect) rather than at module scope, or an SSR/prerender build fails
|
|
17
|
+
on import alone.
|
|
18
|
+
|
|
14
19
|
## Usage
|
|
15
20
|
|
|
16
21
|
```ts
|
|
@@ -37,3 +37,20 @@ Same as `querySelectorWithin` but returns all matches.
|
|
|
37
37
|
```ts
|
|
38
38
|
const inputs = querySelectorAllWithin(this, 'u-input');
|
|
39
39
|
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
### `isCoarsePointer(event)`
|
|
44
|
+
|
|
45
|
+
Returns `true` for a pointer with no lasting hover state — touch and pen.
|
|
46
|
+
|
|
47
|
+
Such a pointer fires `pointerleave` immediately after `pointerenter` on a tap, so a
|
|
48
|
+
hover-triggered surface that does not check this opens and closes in the same gesture.
|
|
49
|
+
Branch on it before treating `pointerenter` as "the user is hovering here".
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
private onPointerEnter(e: PointerEvent) {
|
|
53
|
+
if (isCoarsePointer(e)) return; // let the click/tap handler own this instead
|
|
54
|
+
this.open = true;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# format
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import { formatNumber, formatCurrency, formatDate } from '@iyulab/components';
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Thin wrappers over `Intl.NumberFormat` / `Intl.DateTimeFormat` that default to the
|
|
8
|
+
library's active locale (`Locale.get()`), so numbers and dates rendered by your app match
|
|
9
|
+
the ones the components render themselves.
|
|
10
|
+
|
|
11
|
+
They add no formatting rules of their own — everything `Intl` accepts is passed straight
|
|
12
|
+
through. Reach for them instead of calling `Intl` directly when you want the active locale
|
|
13
|
+
applied without threading it through every call site.
|
|
14
|
+
|
|
15
|
+
## Functions
|
|
16
|
+
|
|
17
|
+
### `formatNumber(value, options?, locale?)`
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
formatNumber(1234.5); // active locale
|
|
21
|
+
formatNumber(0.42, { style: 'percent' }); // '42%'
|
|
22
|
+
formatNumber(1234.5, { maximumFractionDigits: 0 }, 'de-DE');
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`options` is `Intl.NumberFormatOptions`. `locale` overrides the active locale for this call
|
|
26
|
+
only.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
### `formatCurrency(value, currency, options?, locale?)`
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
formatCurrency(1234.5, 'USD'); // '$1,234.50' in an en locale
|
|
34
|
+
formatCurrency(1234.5, 'KRW');
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`currency` is **required and has no default** — which currency an amount is in is domain
|
|
38
|
+
knowledge this utility will not guess.
|
|
39
|
+
|
|
40
|
+
⚠ An invalid currency code throws `RangeError` rather than degrading (see the note under
|
|
41
|
+
`formatDate`). Pass a valid ISO 4217 code.
|
|
42
|
+
|
|
43
|
+
If `options` contains `currency` or `style`, those win over the `currency` argument.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
### `formatDate(value, options?, locale?)`
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
formatDate(new Date());
|
|
51
|
+
formatDate('2026-03-14', { dateStyle: 'long' });
|
|
52
|
+
formatDate(order.createdAt, { dateStyle: 'short', timeStyle: 'short' });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Accepts a `Date` or an ISO `YYYY-MM-DD` string. **The string form is parsed as local time,
|
|
56
|
+
not UTC** — `'2026-03-14'` is midnight where the user is, so a date never shifts a day
|
|
57
|
+
across time zones the way `new Date('2026-03-14')` does.
|
|
58
|
+
|
|
59
|
+
A value that cannot be resolved to a real date — a malformed string, or an already-invalid
|
|
60
|
+
`Date` — returns `String(value)` instead of throwing. `Intl.DateTimeFormat.format()` throws
|
|
61
|
+
`RangeError` on an invalid date, and this runs inside render paths where an uncaught throw
|
|
62
|
+
blanks the whole component; showing the raw value is the lesser failure.
|
|
63
|
+
|
|
64
|
+
That degrade is **deliberately not symmetric** with `formatCurrency`: a date arrives as
|
|
65
|
+
data (an API, a user, a stale cache), so a bad one is an expected runtime state; a currency
|
|
66
|
+
code is written at the call site, so a bad one is a bug worth surfacing.
|
|
67
|
+
|
|
68
|
+
## Locale
|
|
69
|
+
|
|
70
|
+
All three read `Locale.get()` when `locale` is omitted, so they follow whatever
|
|
71
|
+
[`Locale`](./locale.md) resolved — `<html lang>` first, then the browser language. Pass
|
|
72
|
+
`locale` explicitly only when one value must be formatted differently from the rest of the
|
|
73
|
+
page (a currency shown in its home locale, for example).
|
|
@@ -10,7 +10,11 @@ Locale registry utility for library-generated validation messages.
|
|
|
10
10
|
|
|
11
11
|
Built in: `en`, `ko`, `ja`, `zh-CN`, `zh-TW`, `es`, `fr`, `de`, `pt-BR`, `vi`, `th`, `id`, `ru`, `ar`.
|
|
12
12
|
|
|
13
|
-
Initial locale is auto-detected from `
|
|
13
|
+
Initial locale is auto-detected from `document.documentElement.lang` first, then
|
|
14
|
+
`navigator.language`, with English fallback. `<html lang>` wins because it is the author's
|
|
15
|
+
declaration of the document's language and is what assistive technology uses to pick
|
|
16
|
+
pronunciation rules (WCAG 3.1.1 / 3.1.2); the browser language is the user-preference
|
|
17
|
+
fallback for when no such declaration exists. Call `Locale.set()` to override either.
|
|
14
18
|
|
|
15
19
|
## API
|
|
16
20
|
|
|
@@ -4,7 +4,20 @@
|
|
|
4
4
|
import { OverlayManager } from '@iyulab/components';
|
|
5
5
|
```
|
|
6
6
|
|
|
7
|
-
Internal static manager
|
|
7
|
+
Internal static manager that **owns the layer scale for every stacking surface**. Used by `UOverlayElement` (`u-dialog`, `u-drawer`) to track open overlays, assign z-index, and manage body scroll locking — and read by `Toast` for the notification layer.
|
|
8
|
+
|
|
9
|
+
## Layer scale
|
|
10
|
+
|
|
11
|
+
Stacking is a contract, not a negotiation between components. Two bands exist, and the notification band is **always above** the overlay band:
|
|
12
|
+
|
|
13
|
+
| Band | Value | Owned by |
|
|
14
|
+
|------|-------|----------|
|
|
15
|
+
| Overlay | `9999 + depth`, capped at `9999 + 1000` | `UOverlayElement` subclasses (`u-dialog`, `u-drawer`, custom overlays) |
|
|
16
|
+
| **Notification** | `OverlayManager.notificationZIndex` | `Toast` |
|
|
17
|
+
|
|
18
|
+
Overlay z-index comes from **how many overlays are open at once** (stack depth), not from how many have ever been opened. That is what bounds the band — and a bounded band is what lets the notification layer sit above it by construction. More than 1000 simultaneous overlays share the top value: they stop stacking relative to each other, but notifications stay above them.
|
|
19
|
+
|
|
20
|
+
> **Do not hardcode a z-index for a surface that must appear above overlays.** Read `OverlayManager.notificationZIndex` so the value follows the scale.
|
|
8
21
|
|
|
9
22
|
> **Note:** You generally do not need to use `OverlayManager` directly. It is called automatically by overlay components. Only use it if you are building a custom overlay component that extends `UOverlayElement`.
|
|
10
23
|
|
|
@@ -12,10 +25,11 @@ Internal static manager for the overlay stack. Used by `UOverlayElement` (`u-dia
|
|
|
12
25
|
|
|
13
26
|
| Member | Type / Returns | Description |
|
|
14
27
|
|--------|----------------|-------------|
|
|
15
|
-
| `OverlayManager.add(overlay, lockBody?)` | `void` | Register an overlay;
|
|
28
|
+
| `OverlayManager.add(overlay, lockBody?)` | `void` | Register an overlay; assigns a z-index from the overlay band by stack depth; optionally locks body scroll |
|
|
16
29
|
| `OverlayManager.remove(overlay, lockBody?)` | `void` | Unregister an overlay; releases scroll lock if no overlays remain |
|
|
17
30
|
| `OverlayManager.isTopmost(overlay)` | `boolean` | Returns `true` if the overlay is the topmost in the stack (used for ESC key handling) |
|
|
18
31
|
| `OverlayManager.size` | `number` | Number of currently open overlays |
|
|
32
|
+
| `OverlayManager.notificationZIndex` | `number` | z-index of the notification layer — guaranteed above the whole overlay band. Use this for toasts, snackbars, and anything else that must remain visible over an open modal |
|
|
19
33
|
| `OverlayManager.trapStack` | `FocusTrap[]` | Shared focus-trap stack (used internally by focus-trap library) |
|
|
20
34
|
|
|
21
35
|
## Example (custom overlay)
|