@iyulab/components 1.37.1 → 1.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.38.0] - 2026-09-08
4
+
5
+ ### Changed
6
+
7
+ - **`<html lang>` now takes precedence over the browser language when the initial
8
+ locale is detected.** `detectLocale()` consulted `navigator.language` first, which
9
+ made the `document.documentElement.lang` branch unreachable in a browser: a document
10
+ declaring `lang="en"` still emitted accessible names in the browser's language, and a
11
+ screen reader then announced them under English pronunciation rules (WCAG 3.1.1 /
12
+ 3.1.2). The `lang` attribute is the author's declaration of the document's language;
13
+ the browser language is the fallback for when that declaration is absent, not an
14
+ override for it. **Only documents that actually set `lang` change behaviour** — an
15
+ absent or empty `lang` falls back to the browser language exactly as before, so apps
16
+ that never set it are unaffected. `Locale.set()` still overrides both. The Node 21+
17
+ guard on the `navigator` branch is unchanged.
18
+
19
+ ### Documentation
20
+
21
+ - `formatNumber`, `formatCurrency` and `formatDate` were public through the barrel but
22
+ appeared in no published document; they are now documented.
23
+ - `isCoarsePointer` is documented alongside the other element helpers.
24
+ - `formatDate`'s JSDoc cross-referenced a `formatCurrency` fallback that does not exist.
25
+ `formatCurrency` throws on an invalid currency code, and the asymmetry between the two
26
+ is deliberate; both are now described as they behave.
27
+ - The `BrowserStorage` reference notes that the constructor throws outside a browser.
28
+
3
29
  ## [1.37.1] - 2026-09-07
4
30
 
5
31
  ### 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
  * 로케일 하나의 메시지 테이블(전체 또는 일부)을 등록합니다.
@@ -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
- /** 브라우저 환경이면 `navigator.language`/`document.lang`으로 초기 로케일을 추측한다. */
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
  }
@@ -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. Same degrade-instead-of-throw contract as `formatCurrency`'s
23
- * missing-`currency` fallback.
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;
@@ -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. Same degrade-instead-of-throw contract as `formatCurrency`'s
53
- * missing-`currency` fallback.
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
@@ -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.37.1",
4
+ "version": "1.38.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "components",
@@ -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 `navigator.language` / `document.documentElement.lang`, with English fallback.
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