@nanahoshi/mona-ui 0.23.3 → 0.24.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 (107) hide show
  1. package/fesm2022/nanahoshi-mona-ui-auto-complete.mjs +2 -0
  2. package/fesm2022/nanahoshi-mona-ui-breadcrumb.mjs +2 -0
  3. package/fesm2022/nanahoshi-mona-ui-button-group.mjs +2 -0
  4. package/fesm2022/nanahoshi-mona-ui-calendar.mjs +13 -11
  5. package/fesm2022/nanahoshi-mona-ui-card.mjs +2 -0
  6. package/fesm2022/nanahoshi-mona-ui-chart.mjs +4 -2
  7. package/fesm2022/nanahoshi-mona-ui-chip.mjs +2 -0
  8. package/fesm2022/nanahoshi-mona-ui-color-gradient.mjs +5 -3
  9. package/fesm2022/nanahoshi-mona-ui-color-palette.mjs +2 -0
  10. package/fesm2022/nanahoshi-mona-ui-color-picker.mjs +2 -0
  11. package/fesm2022/nanahoshi-mona-ui-combo-box.mjs +2 -0
  12. package/fesm2022/nanahoshi-mona-ui-date-picker.mjs +2 -0
  13. package/fesm2022/nanahoshi-mona-ui-datetime-picker.mjs +2 -0
  14. package/fesm2022/nanahoshi-mona-ui-dialog.mjs +2 -0
  15. package/fesm2022/nanahoshi-mona-ui-dropdown-list.mjs +2 -0
  16. package/fesm2022/nanahoshi-mona-ui-dropdowns.mjs +5 -3
  17. package/fesm2022/nanahoshi-mona-ui-editor.mjs +5 -3
  18. package/fesm2022/nanahoshi-mona-ui-filter.mjs +3 -1
  19. package/fesm2022/{nanahoshi-mona-ui-grid-grid-footer-row.component-BSVlcB9_.mjs → nanahoshi-mona-ui-grid-grid-footer-row.component-CnPhVIr4.mjs} +2 -2
  20. package/fesm2022/{nanahoshi-mona-ui-grid-grid-list.component-B2pBOuLa.mjs → nanahoshi-mona-ui-grid-grid-list.component-DxY6bfTX.mjs} +4 -4
  21. package/fesm2022/{nanahoshi-mona-ui-grid-grid-row.component-Buf4evf5.mjs → nanahoshi-mona-ui-grid-grid-row.component-CaO5Lae6.mjs} +3 -3
  22. package/fesm2022/{nanahoshi-mona-ui-grid-grid-virtual-list.component-1RdqLEa9.mjs → nanahoshi-mona-ui-grid-grid-virtual-list.component-jNYtqg5B.mjs} +4 -4
  23. package/fesm2022/{nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-DeRc_QcP.mjs → nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-9FtfCutf.mjs} +12 -10
  24. package/fesm2022/nanahoshi-mona-ui-grid.mjs +1 -1
  25. package/fesm2022/nanahoshi-mona-ui-i18n-icu.mjs +888 -0
  26. package/fesm2022/nanahoshi-mona-ui-i18n.mjs +337 -104
  27. package/fesm2022/nanahoshi-mona-ui-internal-list.mjs +2 -0
  28. package/fesm2022/nanahoshi-mona-ui-internal-tree.mjs +2 -0
  29. package/fesm2022/nanahoshi-mona-ui-list-box.mjs +2 -0
  30. package/fesm2022/nanahoshi-mona-ui-locales-ar-sa.mjs +401 -0
  31. package/fesm2022/nanahoshi-mona-ui-locales-de-de.mjs +399 -0
  32. package/fesm2022/nanahoshi-mona-ui-locales-es-es.mjs +399 -0
  33. package/fesm2022/nanahoshi-mona-ui-locales-fr-fr.mjs +399 -0
  34. package/fesm2022/nanahoshi-mona-ui-locales-id-id.mjs +397 -0
  35. package/fesm2022/nanahoshi-mona-ui-locales-it-it.mjs +399 -0
  36. package/fesm2022/nanahoshi-mona-ui-locales-ja-jp.mjs +397 -0
  37. package/fesm2022/nanahoshi-mona-ui-locales-ko-kr.mjs +397 -0
  38. package/fesm2022/nanahoshi-mona-ui-locales-pt-br.mjs +399 -0
  39. package/fesm2022/nanahoshi-mona-ui-locales-ru-ru.mjs +401 -0
  40. package/fesm2022/nanahoshi-mona-ui-locales-tr-tr.mjs +397 -0
  41. package/fesm2022/nanahoshi-mona-ui-locales-zh-cn.mjs +397 -0
  42. package/fesm2022/nanahoshi-mona-ui-locales-zh-tw.mjs +397 -0
  43. package/fesm2022/nanahoshi-mona-ui-locales.mjs +76 -5060
  44. package/fesm2022/nanahoshi-mona-ui-multi-select.mjs +5 -3
  45. package/fesm2022/nanahoshi-mona-ui-notification.mjs +2 -0
  46. package/fesm2022/nanahoshi-mona-ui-numeric-text-box.mjs +299 -167
  47. package/fesm2022/nanahoshi-mona-ui-otp-input.mjs +2 -0
  48. package/fesm2022/nanahoshi-mona-ui-pager.mjs +5 -3
  49. package/fesm2022/nanahoshi-mona-ui-rating.mjs +4 -2
  50. package/fesm2022/nanahoshi-mona-ui-scroll-view.mjs +5 -3
  51. package/fesm2022/nanahoshi-mona-ui-sheet.mjs +2 -0
  52. package/fesm2022/nanahoshi-mona-ui-slider.mjs +2 -0
  53. package/fesm2022/nanahoshi-mona-ui-spinner.mjs +2 -0
  54. package/fesm2022/nanahoshi-mona-ui-split-button.mjs +2 -0
  55. package/fesm2022/nanahoshi-mona-ui-splitter.mjs +2 -0
  56. package/fesm2022/nanahoshi-mona-ui-stepper.mjs +2 -0
  57. package/fesm2022/nanahoshi-mona-ui-tabs.mjs +2 -0
  58. package/fesm2022/nanahoshi-mona-ui-text-box.mjs +2 -0
  59. package/fesm2022/nanahoshi-mona-ui-time-picker.mjs +2 -0
  60. package/fesm2022/nanahoshi-mona-ui-time-selector.mjs +2 -0
  61. package/fesm2022/nanahoshi-mona-ui-window.mjs +2 -0
  62. package/i18n/catalogs/en-US.messages.json +373 -0
  63. package/i18n/icu/package.json +4 -0
  64. package/locales/ar-sa/ar-sa.messages.json +373 -0
  65. package/locales/ar-sa/package.json +4 -0
  66. package/locales/de-de/de-de.messages.json +373 -0
  67. package/locales/de-de/package.json +4 -0
  68. package/locales/es-es/es-es.messages.json +373 -0
  69. package/locales/es-es/package.json +4 -0
  70. package/locales/fr-fr/fr-fr.messages.json +373 -0
  71. package/locales/fr-fr/package.json +4 -0
  72. package/locales/id-id/id-id.messages.json +373 -0
  73. package/locales/id-id/package.json +4 -0
  74. package/locales/it-it/it-it.messages.json +373 -0
  75. package/locales/it-it/package.json +4 -0
  76. package/locales/ja-jp/ja-jp.messages.json +373 -0
  77. package/locales/ja-jp/package.json +4 -0
  78. package/locales/ko-kr/ko-kr.messages.json +373 -0
  79. package/locales/ko-kr/package.json +4 -0
  80. package/locales/pt-br/package.json +4 -0
  81. package/locales/pt-br/pt-br.messages.json +373 -0
  82. package/locales/ru-ru/package.json +4 -0
  83. package/locales/ru-ru/ru-ru.messages.json +373 -0
  84. package/locales/tr-tr/package.json +4 -0
  85. package/locales/tr-tr/tr-tr.messages.json +373 -0
  86. package/locales/zh-cn/package.json +4 -0
  87. package/locales/zh-cn/zh-cn.messages.json +373 -0
  88. package/locales/zh-tw/package.json +4 -0
  89. package/locales/zh-tw/zh-tw.messages.json +373 -0
  90. package/package.json +111 -53
  91. package/types/nanahoshi-mona-ui-i18n-icu.d.ts +74 -0
  92. package/types/nanahoshi-mona-ui-i18n.d.ts +108 -6
  93. package/types/nanahoshi-mona-ui-locales-ar-sa.d.ts +379 -0
  94. package/types/nanahoshi-mona-ui-locales-de-de.d.ts +379 -0
  95. package/types/nanahoshi-mona-ui-locales-es-es.d.ts +379 -0
  96. package/types/nanahoshi-mona-ui-locales-fr-fr.d.ts +379 -0
  97. package/types/nanahoshi-mona-ui-locales-id-id.d.ts +379 -0
  98. package/types/nanahoshi-mona-ui-locales-it-it.d.ts +379 -0
  99. package/types/nanahoshi-mona-ui-locales-ja-jp.d.ts +379 -0
  100. package/types/nanahoshi-mona-ui-locales-ko-kr.d.ts +379 -0
  101. package/types/nanahoshi-mona-ui-locales-pt-br.d.ts +379 -0
  102. package/types/nanahoshi-mona-ui-locales-ru-ru.d.ts +379 -0
  103. package/types/nanahoshi-mona-ui-locales-tr-tr.d.ts +379 -0
  104. package/types/nanahoshi-mona-ui-locales-zh-cn.d.ts +379 -0
  105. package/types/nanahoshi-mona-ui-locales-zh-tw.d.ts +379 -0
  106. package/types/nanahoshi-mona-ui-locales.d.ts +30 -4915
  107. package/types/nanahoshi-mona-ui-numeric-text-box.d.ts +113 -37
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, signal, computed, Injectable, isDevMode, ElementRef, DestroyRef, makeEnvironmentProviders } from '@angular/core';
2
+ import { InjectionToken, inject, signal, computed, Injectable, isDevMode, ElementRef, DestroyRef, afterNextRender, Injector, makeEnvironmentProviders, provideAppInitializer } from '@angular/core';
3
3
  import { Directionality } from '@angular/cdk/bidi';
4
4
  import { DateTime } from 'luxon';
5
5
 
@@ -83,19 +83,36 @@ function mergeMessages(fallback, localeMessages, overrideMessages) {
83
83
 
84
84
  class MonaI18nService {
85
85
  #config;
86
+ #loadError;
87
+ #loadedLocales;
88
+ #loadingLocaleId;
86
89
  #locale;
87
90
  #localeOverrides;
88
91
  #overrides;
92
+ /** Incremented by every `load()` and `use()`, so only the latest request changes the locale. */
93
+ #localeRequest;
89
94
  constructor() {
90
95
  this.#config = inject(MONA_I18N_CONFIG);
96
+ this.#loadError = signal(null, /* @ts-ignore */
97
+ ...(ngDevMode ? [{ debugName: "#loadError" }] : /* istanbul ignore next */ []));
98
+ this.#loadedLocales = new Map();
99
+ this.#loadingLocaleId = signal(null, /* @ts-ignore */
100
+ ...(ngDevMode ? [{ debugName: "#loadingLocaleId" }] : /* istanbul ignore next */ []));
91
101
  this.#locale = signal(this.#config.locale ?? MONA_DEFAULT_LOCALE, /* @ts-ignore */
92
102
  ...(ngDevMode ? [{ debugName: "#locale" }] : /* istanbul ignore next */ []));
93
103
  this.#localeOverrides = signal(createLocaleOverrides(this.#config.localeMessages), /* @ts-ignore */
94
104
  ...(ngDevMode ? [{ debugName: "#localeOverrides" }] : /* istanbul ignore next */ []));
95
105
  this.#overrides = signal(this.#config.messages ?? {}, /* @ts-ignore */
96
106
  ...(ngDevMode ? [{ debugName: "#overrides" }] : /* istanbul ignore next */ []));
107
+ /** Incremented by every `load()` and `use()`, so only the latest request changes the locale. */
108
+ this.#localeRequest = 0;
97
109
  this.direction = computed(() => this.#locale().direction, /* @ts-ignore */
98
110
  ...(ngDevMode ? [{ debugName: "direction" }] : /* istanbul ignore next */ []));
111
+ /** The error of the latest `load()` when it failed, otherwise `null`. */
112
+ this.loadError = this.#loadError.asReadonly();
113
+ /** Whether a `load()` is in progress and has not been superseded. */
114
+ this.loading = computed(() => this.#loadingLocaleId() != null, /* @ts-ignore */
115
+ ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
99
116
  this.locale = this.#locale.asReadonly();
100
117
  /**
101
118
  * Canonical BCP 47 id of the active locale. POSIX ids such as `en_US` are converted to `en-US`,
@@ -131,6 +148,51 @@ class MonaI18nService {
131
148
  return mergeTwo(merged, localeOverrideMessages);
132
149
  });
133
150
  }
151
+ /**
152
+ * Loads a locale with the `loadLocale` function configured in `provideMonaI18n` and switches to it.
153
+ *
154
+ * - Ids are canonicalized, and each locale is loaded once: later calls reuse it, and concurrent calls for the
155
+ * same id share one request.
156
+ * - The latest `load()` or `use()` wins. When a newer one starts before this one finishes, this one resolves
157
+ * without switching.
158
+ * - When loading fails, the current locale stays, `loadError` is set and the promise rejects. The failed
159
+ * locale is not cached, so a later call retries it.
160
+ */
161
+ async load(localeId) {
162
+ const loadLocale = this.#config.loadLocale;
163
+ if (loadLocale == null) {
164
+ throw new Error("[mona-ui] MonaI18nService.load() needs a loader: provideMonaI18n({ loadLocale: loadMonaLocale }).");
165
+ }
166
+ const canonicalId = canonicalizeLocaleId(localeId);
167
+ const request = ++this.#localeRequest;
168
+ this.#loadingLocaleId.set(canonicalId);
169
+ this.#loadError.set(null);
170
+ let pending = this.#loadedLocales.get(canonicalId);
171
+ if (pending == null) {
172
+ pending = Promise.resolve().then(() => loadLocale(canonicalId));
173
+ this.#loadedLocales.set(canonicalId, pending);
174
+ }
175
+ try {
176
+ const locale = await pending;
177
+ if (request === this.#localeRequest) {
178
+ this.#applyLocale(locale);
179
+ }
180
+ }
181
+ catch (error) {
182
+ if (this.#loadedLocales.get(canonicalId) === pending) {
183
+ this.#loadedLocales.delete(canonicalId);
184
+ }
185
+ if (request === this.#localeRequest) {
186
+ this.#loadError.set(error);
187
+ }
188
+ throw error;
189
+ }
190
+ finally {
191
+ if (request === this.#localeRequest) {
192
+ this.#loadingLocaleId.set(null);
193
+ }
194
+ }
195
+ }
134
196
  /**
135
197
  * Merges overrides into the existing ones. With `options.locale`, they apply only while that locale is
136
198
  * active, so overrides patched for German do not show after switching to English.
@@ -154,7 +216,14 @@ class MonaI18nService {
154
216
  const localeId = canonicalizeLocaleId(options.locale);
155
217
  this.#localeOverrides.update(current => new Map(current).set(localeId, messages));
156
218
  }
219
+ /** Switches to a locale that is already loaded. It supersedes a `load()` still in progress. */
157
220
  use(locale) {
221
+ this.#localeRequest++;
222
+ this.#loadingLocaleId.set(null);
223
+ this.#loadError.set(null);
224
+ this.#applyLocale(locale);
225
+ }
226
+ #applyLocale(locale) {
158
227
  warnOnInvalidLocaleId(locale.id);
159
228
  this.#locale.set(locale);
160
229
  }
@@ -241,39 +310,94 @@ function resolveComponentDirection(hostElement, directionality) {
241
310
  }
242
311
  return "ltr";
243
312
  }
244
- const documentObservers = new WeakMap();
245
- function observeDocumentDirChanges(doc, callback) {
246
- let entry = documentObservers.get(doc);
247
- if (!entry) {
248
- const callbacks = new Set();
249
- const observer = new MutationObserver(() => {
250
- for (const cb of Array.from(callbacks)) {
251
- cb();
313
+ /**
314
+ * One `dir` attribute observer per observed root (the document element, a shadow root, or the top of a detached
315
+ * tree), shared by every direction-aware instance under it. A mutation only invalidates the instances it can affect,
316
+ * so a `dir` written on an overlay pane does not re-resolve every component on the page.
317
+ */
318
+ const directionRegistries = new WeakMap();
319
+ function isShadowRoot(node) {
320
+ return node.nodeType === 11 && "host" in node;
321
+ }
322
+ function getComposedParent(node) {
323
+ if (node.parentNode) {
324
+ return node.parentNode;
325
+ }
326
+ return isShadowRoot(node) ? node.host : null;
327
+ }
328
+ function composedContains(ancestor, node) {
329
+ for (let current = node; current; current = getComposedParent(current)) {
330
+ if (current === ancestor) {
331
+ return true;
332
+ }
333
+ }
334
+ return false;
335
+ }
336
+ /**
337
+ * A `dir` change affects an instance when it lands on the instance's element or one of its ancestors, or anywhere
338
+ * inside the `dir="auto"` root it resolves against (a nested explicit direction changes which text counts).
339
+ */
340
+ function isAffectedByDirChange(subscriber, targets) {
341
+ for (let current = subscriber.element; current; current = getComposedParent(current)) {
342
+ if (targets.has(current)) {
343
+ return true;
344
+ }
345
+ }
346
+ const autoRoot = subscriber.autoRoot;
347
+ if (autoRoot) {
348
+ for (const target of targets) {
349
+ if (composedContains(autoRoot, target)) {
350
+ return true;
252
351
  }
253
- });
254
- if (doc.documentElement) {
255
- observer.observe(doc.documentElement, {
256
- attributes: true,
257
- attributeFilter: ["dir"],
258
- subtree: true
259
- });
260
352
  }
261
- entry = { refCount: 0, observer, callbacks };
262
- documentObservers.set(doc, entry);
263
353
  }
264
- entry.refCount++;
265
- entry.callbacks.add(callback);
266
- let cleanedUp = false;
354
+ return false;
355
+ }
356
+ function getObservedRoots(element) {
357
+ const roots = [];
358
+ let root = element.getRootNode?.() ?? null;
359
+ while (root && isShadowRoot(root)) {
360
+ roots.push(root);
361
+ root = root.host.getRootNode?.() ?? null;
362
+ }
363
+ // The top of a detached tree, so a host that is not attached yet still sees its own subtree change.
364
+ if (root && root !== element && root.nodeType === 1) {
365
+ roots.push(root);
366
+ }
367
+ const documentElement = element.ownerDocument?.documentElement;
368
+ if (documentElement && !roots.includes(documentElement)) {
369
+ roots.push(documentElement);
370
+ }
371
+ return roots;
372
+ }
373
+ function registerDirectionSubscriber(observedRoot, subscriber) {
374
+ let registry = directionRegistries.get(observedRoot);
375
+ if (!registry) {
376
+ const subscribers = new Set();
377
+ const observer = new MutationObserver(records => {
378
+ const targets = new Set();
379
+ for (const record of records) {
380
+ targets.add(record.target);
381
+ }
382
+ for (const candidate of Array.from(subscribers)) {
383
+ if (subscribers.has(candidate) && isAffectedByDirChange(candidate, targets)) {
384
+ candidate.invalidate();
385
+ }
386
+ }
387
+ });
388
+ observer.observe(observedRoot, { attributes: true, attributeFilter: ["dir"], subtree: true });
389
+ registry = { observer, subscribers };
390
+ directionRegistries.set(observedRoot, registry);
391
+ }
392
+ const activeRegistry = registry;
393
+ activeRegistry.subscribers.add(subscriber);
267
394
  return () => {
268
- if (cleanedUp || !entry) {
269
- return;
270
- }
271
- cleanedUp = true;
272
- entry.callbacks.delete(callback);
273
- entry.refCount--;
274
- if (entry.refCount <= 0) {
275
- entry.observer.disconnect();
276
- documentObservers.delete(doc);
395
+ activeRegistry.subscribers.delete(subscriber);
396
+ if (activeRegistry.subscribers.size === 0) {
397
+ activeRegistry.observer.disconnect();
398
+ if (directionRegistries.get(observedRoot) === activeRegistry) {
399
+ directionRegistries.delete(observedRoot);
400
+ }
277
401
  }
278
402
  };
279
403
  }
@@ -320,46 +444,61 @@ function observeAutoDirectionChanges(autoRoot, callback) {
320
444
  }
321
445
  };
322
446
  }
447
+ /** The nearest `dir="auto"` ancestor-or-self, unless an explicit `ltr`/`rtl` comes first. */
448
+ function findAutoRoot(element) {
449
+ for (let current = element; current; current = current.parentElement) {
450
+ const rawDir = current.getAttribute?.("dir")?.trim().toLowerCase();
451
+ if (rawDir === "auto") {
452
+ return current;
453
+ }
454
+ if (rawDir === "rtl" || rawDir === "ltr") {
455
+ return null;
456
+ }
457
+ }
458
+ return null;
459
+ }
323
460
  function observeComponentDirection(hostElement, directionality, onChange) {
324
461
  const element = hostElement instanceof ElementRef ? hostElement.nativeElement : hostElement;
462
+ const canObserve = typeof MutationObserver !== "undefined" && !!element;
325
463
  let currentDir = resolveComponentDirection(element, directionality);
326
464
  let unobserveAuto = null;
327
- let observedAutoElement = null;
465
+ const rootSubscriptions = new Map();
466
+ const subscriber = element
467
+ ? { autoRoot: null, element, invalidate: () => check() }
468
+ : null;
328
469
  const updateAutoContentObserver = () => {
329
- if (typeof MutationObserver === "undefined" || !element) {
470
+ if (!canObserve || !subscriber) {
330
471
  return;
331
472
  }
332
- let autoRoot = null;
333
- let current = element;
334
- while (current) {
335
- const rawDir = current.getAttribute?.("dir")?.trim().toLowerCase();
336
- if (rawDir === "auto") {
337
- autoRoot = current;
338
- break;
339
- }
340
- if (rawDir === "rtl" || rawDir === "ltr") {
341
- break;
342
- }
343
- current = current.parentElement;
473
+ const autoRoot = findAutoRoot(subscriber.element);
474
+ if (autoRoot === subscriber.autoRoot) {
475
+ return;
344
476
  }
345
- if (autoRoot) {
346
- if (observedAutoElement !== autoRoot) {
347
- if (unobserveAuto) {
348
- unobserveAuto();
349
- }
350
- observedAutoElement = autoRoot;
351
- unobserveAuto = observeAutoDirectionChanges(autoRoot, () => check());
477
+ unobserveAuto?.();
478
+ unobserveAuto = autoRoot ? observeAutoDirectionChanges(autoRoot, () => check()) : null;
479
+ subscriber.autoRoot = autoRoot;
480
+ };
481
+ // Re-registers with the roots the host is under now: it may have been created in a detached view and inserted
482
+ // into the document or a shadow root later.
483
+ const syncObservedRoots = () => {
484
+ if (!canObserve || !subscriber) {
485
+ return;
486
+ }
487
+ const roots = getObservedRoots(subscriber.element);
488
+ for (const [root, unsubscribe] of rootSubscriptions) {
489
+ if (!roots.includes(root)) {
490
+ unsubscribe();
491
+ rootSubscriptions.delete(root);
352
492
  }
353
493
  }
354
- else {
355
- if (unobserveAuto) {
356
- unobserveAuto();
357
- unobserveAuto = null;
358
- observedAutoElement = null;
494
+ for (const root of roots) {
495
+ if (!rootSubscriptions.has(root)) {
496
+ rootSubscriptions.set(root, registerDirectionSubscriber(root, subscriber));
359
497
  }
360
498
  }
361
499
  };
362
500
  const check = () => {
501
+ syncObservedRoots();
363
502
  updateAutoContentObserver();
364
503
  const nextDir = resolveComponentDirection(element, directionality);
365
504
  if (nextDir !== currentDir) {
@@ -377,27 +516,24 @@ function observeComponentDirection(hostElement, directionality, onChange) {
377
516
  });
378
517
  cleanupFns.push(() => sub.unsubscribe());
379
518
  }
380
- if (typeof MutationObserver !== "undefined") {
381
- if (element) {
382
- const elObserver = new MutationObserver(() => check());
383
- elObserver.observe(element, { attributes: true, attributeFilter: ["dir"] });
384
- cleanupFns.push(() => elObserver.disconnect());
385
- const root = element.getRootNode?.();
386
- if (root && root !== element && root !== element.ownerDocument) {
387
- const rootObserver = new MutationObserver(() => check());
388
- rootObserver.observe(root, { attributes: true, attributeFilter: ["dir"], subtree: true });
389
- cleanupFns.push(() => rootObserver.disconnect());
390
- }
391
- if (element.ownerDocument) {
392
- cleanupFns.push(observeDocumentDirChanges(element.ownerDocument, check));
393
- }
394
- }
519
+ if (canObserve && element) {
520
+ // The host's own dir, which the shared root observers miss while the host is outside them (detached or moved).
521
+ const hostObserver = new MutationObserver(() => check());
522
+ hostObserver.observe(element, { attributes: true, attributeFilter: ["dir"] });
523
+ cleanupFns.push(() => hostObserver.disconnect());
395
524
  }
525
+ syncObservedRoots();
526
+ cleanupFns.push(() => {
527
+ for (const unsubscribe of rootSubscriptions.values()) {
528
+ unsubscribe();
529
+ }
530
+ rootSubscriptions.clear();
531
+ });
396
532
  cleanupFns.push(() => {
397
- if (unobserveAuto) {
398
- unobserveAuto();
399
- unobserveAuto = null;
400
- observedAutoElement = null;
533
+ unobserveAuto?.();
534
+ unobserveAuto = null;
535
+ if (subscriber) {
536
+ subscriber.autoRoot = null;
401
537
  }
402
538
  });
403
539
  const handle = Object.assign(() => {
@@ -416,17 +552,17 @@ function injectComponentDirection(hostElementRef) {
416
552
  const destroyRef = inject(DestroyRef, { optional: true });
417
553
  const domVersion = signal(0, /* @ts-ignore */
418
554
  ...(ngDevMode ? [{ debugName: "domVersion" }] : /* istanbul ignore next */ []));
419
- let updateAutoObserverFn = () => { };
420
555
  if (destroyRef) {
421
- const cleanup = observeComponentDirection(hostRef, directionality, () => {
556
+ const handle = observeComponentDirection(hostRef, directionality, () => {
422
557
  domVersion.update(v => v + 1);
423
558
  });
424
- updateAutoObserverFn = cleanup.updateAutoObserver;
425
- destroyRef.onDestroy(() => cleanup());
559
+ destroyRef.onDestroy(() => handle());
560
+ // A host can be moved under a different `dir="auto"` root after it is created; re-check once it has rendered.
561
+ afterNextRender(() => handle.check(), { injector: inject(Injector) });
426
562
  }
563
+ // Pure: observers are managed by the handle above, never during signal evaluation.
427
564
  return computed(() => {
428
565
  domVersion();
429
- updateAutoObserverFn();
430
566
  return resolveComponentDirection(hostRef, directionality);
431
567
  });
432
568
  }
@@ -452,11 +588,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
452
588
  }] });
453
589
 
454
590
  function provideMonaI18n(config = {}) {
591
+ const initialLocale = config.initialLocale;
455
592
  return makeEnvironmentProviders([
456
593
  {
457
594
  provide: MONA_I18N_CONFIG,
458
595
  useValue: config
459
- }
596
+ },
597
+ ...(initialLocale == null
598
+ ? []
599
+ : [
600
+ // A failed load must not stop the application from starting: it keeps the configured locale.
601
+ provideAppInitializer(() => inject(MonaI18nService)
602
+ .load(initialLocale)
603
+ .catch((error) => console.error(`[mona-ui] Could not load the initial locale "${initialLocale}".`, error)))
604
+ ])
460
605
  ]);
461
606
  }
462
607
 
@@ -601,7 +746,12 @@ function getNumberFormatter(locale, options) {
601
746
  }
602
747
  return formatter;
603
748
  }
749
+ /**
750
+ * Formats a number for the locale. A decimal string, such as one returned by {@link parseLocalizedDecimal}, is
751
+ * formatted exactly, without converting it to a `number` first.
752
+ */
604
753
  function formatNumber(value, locale, options) {
754
+ // Intl.NumberFormat formats decimal strings exactly (Intl.NumberFormat v3); the ES2022 lib types only declare numbers.
605
755
  return getNumberFormatter(locale, options).format(value);
606
756
  }
607
757
  /**
@@ -676,7 +826,52 @@ function getEditDecimalSeparators(symbols) {
676
826
  }
677
827
  return separators;
678
828
  }
829
+ const MAX_SAFE_INTEGER_DIGITS = String(Number.MAX_SAFE_INTEGER);
830
+ /** The most significant digits a double reproduces exactly when converted from decimal text and back. */
831
+ const MAX_EXACT_SIGNIFICANT_DIGITS = 15;
832
+ /** Whether ASCII integer and fraction digits survive conversion to a `number` unchanged. */
833
+ function isRepresentableAsNumber(intDigits, fracDigits) {
834
+ const integer = intDigits.replace(/^0+/, "");
835
+ const fraction = fracDigits.replace(/0+$/, "");
836
+ if (fraction === "") {
837
+ return (integer.length < MAX_SAFE_INTEGER_DIGITS.length ||
838
+ (integer.length === MAX_SAFE_INTEGER_DIGITS.length && integer <= MAX_SAFE_INTEGER_DIGITS));
839
+ }
840
+ return (integer + fraction).replace(/^0+/, "").length <= MAX_EXACT_SIGNIFICANT_DIGITS;
841
+ }
842
+ function partsToNumber(parts) {
843
+ if (!isRepresentableAsNumber(parts.integer, parts.fraction)) {
844
+ return "precision";
845
+ }
846
+ return Number((parts.negative ? "-" : "") + parts.integer + (parts.fraction ? "." + parts.fraction : ""));
847
+ }
848
+ /** Leading zeros, trailing fraction zeros and the sign of zero are dropped: `"-0012.500"` becomes `"-12.5"`. */
849
+ function partsToDecimal(parts) {
850
+ const integer = parts.integer.replace(/^0+(?=\d)/, "") || "0";
851
+ const fraction = parts.fraction.replace(/0+$/, "");
852
+ const digits = fraction ? `${integer}.${fraction}` : integer;
853
+ return parts.negative && /[1-9]/.test(digits) ? `-${digits}` : digits;
854
+ }
855
+ /**
856
+ * Parses localized text to a number. Returns `null` when the text is not a number in the locale, or when a
857
+ * `number` cannot hold it exactly (see {@link LocalizedNumberInvalidReason}); it never rounds silently.
858
+ * Use {@link parseLocalizedDecimal} for values that need more precision.
859
+ */
679
860
  function parseLocalizedNumber(text, localeId, options) {
861
+ const parts = parseLocalizedNumberParts(text, localeId, options);
862
+ const value = parts === null ? null : partsToNumber(parts);
863
+ return value === "precision" ? null : value;
864
+ }
865
+ /**
866
+ * Parses localized text with the same rules as {@link parseLocalizedNumber}, but returns a canonical decimal string
867
+ * (`"-1234.5"`: ASCII digits, `.` as the decimal separator, no grouping, exponent, leading zeros or trailing
868
+ * fraction zeros) and has no precision limit. Returns `null` when the text is not a number in the locale.
869
+ */
870
+ function parseLocalizedDecimal(text, localeId, options) {
871
+ const parts = parseLocalizedNumberParts(text, localeId, options);
872
+ return parts === null ? null : partsToDecimal(parts);
873
+ }
874
+ function parseLocalizedNumberParts(text, localeId, options) {
680
875
  if (text == null) {
681
876
  return null;
682
877
  }
@@ -819,9 +1014,7 @@ function parseLocalizedNumber(text, localeId, options) {
819
1014
  }
820
1015
  }
821
1016
  }
822
- const normalizedStr = sign + intPart + (fracPart && fracPart.length > 0 ? "." + fracPart : "");
823
- const num = Number(normalizedStr);
824
- return Number.isFinite(num) ? num : null;
1017
+ return { fraction: fracPart ?? "", integer: intPart, negative: sign === "-" };
825
1018
  }
826
1019
  else {
827
1020
  // mode === "edit": editor buffer grammar with exactly one decimal separator (locale primary or alternate)
@@ -875,11 +1068,7 @@ function parseLocalizedNumber(text, localeId, options) {
875
1068
  intPart = cleaned;
876
1069
  fracPart = "";
877
1070
  }
878
- const normalizedStr = sign +
879
- (intPart.length > 0 ? intPart : "0") +
880
- (fracPart.length > 0 ? "." + fracPart : cleaned.endsWith(foundSep ?? "") ? "" : "");
881
- const num = Number(normalizedStr);
882
- return Number.isFinite(num) ? num : null;
1071
+ return { fraction: fracPart, integer: intPart.length > 0 ? intPart : "0", negative: sign === "-" };
883
1072
  }
884
1073
  }
885
1074
  function validateLocalizedNumber(text, localeId, options) {
@@ -891,9 +1080,13 @@ function validateLocalizedNumber(text, localeId, options) {
891
1080
  return { value: null, valid: true, fractionDigits: 0 };
892
1081
  }
893
1082
  const mode = options?.mode ?? "locale";
894
- const parsed = parseLocalizedNumber(trimmed, localeId, { mode });
1083
+ const parts = parseLocalizedNumberParts(trimmed, localeId, { mode });
1084
+ const parsed = parts === null ? null : partsToNumber(parts);
1085
+ if (parsed === "precision") {
1086
+ return { value: null, valid: false, fractionDigits: 0, reason: "precision" };
1087
+ }
895
1088
  if (parsed === null || !Number.isFinite(parsed)) {
896
- return { value: null, valid: false, fractionDigits: 0 };
1089
+ return { value: null, valid: false, fractionDigits: 0, reason: "syntax" };
897
1090
  }
898
1091
  // Determine the number of fraction digits from the source string
899
1092
  const symbols = getNumberSymbols(localeId);
@@ -952,11 +1145,42 @@ function validateLocalizedNumber(text, localeId, options) {
952
1145
  }
953
1146
  }
954
1147
  if (options?.decimals !== undefined && fractionDigits > options.decimals) {
955
- return { value: parsed, valid: false, fractionDigits };
1148
+ return { value: parsed, valid: false, fractionDigits, reason: "decimals" };
956
1149
  }
957
1150
  return { value: parsed, valid: true, fractionDigits };
958
1151
  }
1152
+ /**
1153
+ * Validates localized text like {@link validateLocalizedNumber}, but returns a canonical decimal string and has no
1154
+ * precision limit, so `reason` is never `"precision"`.
1155
+ */
1156
+ function validateLocalizedDecimal(text, localeId, options) {
1157
+ const trimmed = text == null ? "" : String(text).trim();
1158
+ if (trimmed === "" || trimmed === "-" || trimmed === "+") {
1159
+ return { fractionDigits: 0, valid: true, value: null };
1160
+ }
1161
+ const parts = parseLocalizedNumberParts(trimmed, localeId, { mode: options?.mode ?? "locale" });
1162
+ if (parts === null) {
1163
+ return { fractionDigits: 0, reason: "syntax", valid: false, value: null };
1164
+ }
1165
+ const value = partsToDecimal(parts);
1166
+ const fractionDigits = parts.fraction.length;
1167
+ if (options?.decimals !== undefined && fractionDigits > options.decimals) {
1168
+ return { fractionDigits, reason: "decimals", valid: false, value };
1169
+ }
1170
+ return { fractionDigits, valid: true, value };
1171
+ }
1172
+ const EDIT_SYNTAX_ERROR = { complete: false, reason: "syntax", valid: false, value: null };
959
1173
  function validateLocalizedNumberEdit(text, localeId, options) {
1174
+ return validateEditInput(text, localeId, options, partsToNumber);
1175
+ }
1176
+ /**
1177
+ * Validates text while it is typed, like {@link validateLocalizedNumberEdit}, but returns a canonical decimal string
1178
+ * and has no precision limit.
1179
+ */
1180
+ function validateLocalizedDecimalEdit(text, localeId, options) {
1181
+ return validateEditInput(text, localeId, options, partsToDecimal);
1182
+ }
1183
+ function validateEditInput(text, localeId, options, convert) {
960
1184
  if (text == null) {
961
1185
  return { valid: true, complete: false, value: null };
962
1186
  }
@@ -966,7 +1190,7 @@ function validateLocalizedNumberEdit(text, localeId, options) {
966
1190
  }
967
1191
  // Reject control or whitespace characters in interactive typing
968
1192
  if (/[\s\u00A0\u202F]/.test(raw)) {
969
- return { valid: false, complete: false, value: null };
1193
+ return EDIT_SYNTAX_ERROR;
970
1194
  }
971
1195
  const symbols = getNumberSymbols(localeId);
972
1196
  // Normalize minus signs and bidi controls
@@ -977,12 +1201,12 @@ function validateLocalizedNumberEdit(text, localeId, options) {
977
1201
  const allowSign = options?.allowSign ?? true;
978
1202
  if (normalized.startsWith("-") || normalized.startsWith("+")) {
979
1203
  if (!allowSign) {
980
- return { valid: false, complete: false, value: null };
1204
+ return EDIT_SYNTAX_ERROR;
981
1205
  }
982
1206
  normalized = normalized.slice(1);
983
1207
  }
984
1208
  if (normalized.includes("-") || normalized.includes("+")) {
985
- return { valid: false, complete: false, value: null };
1209
+ return EDIT_SYNTAX_ERROR;
986
1210
  }
987
1211
  // If input was just "-" or "+"
988
1212
  if (normalized === "") {
@@ -997,7 +1221,7 @@ function validateLocalizedNumberEdit(text, localeId, options) {
997
1221
  if (normalized.startsWith(sep, i)) {
998
1222
  if (foundSep !== null) {
999
1223
  // More than one separator is rejected in edit mode
1000
- return { valid: false, complete: false, value: null };
1224
+ return EDIT_SYNTAX_ERROR;
1001
1225
  }
1002
1226
  foundSep = sep;
1003
1227
  sepIndex = i;
@@ -1010,23 +1234,28 @@ function validateLocalizedNumberEdit(text, localeId, options) {
1010
1234
  if (foundSep !== null) {
1011
1235
  if (decimals === 0) {
1012
1236
  // No decimal point permitted when decimals === 0
1013
- return { valid: false, complete: false, value: null };
1237
+ return { valid: false, complete: false, value: null, reason: "decimals" };
1014
1238
  }
1015
1239
  const intPart = normalized.slice(0, sepIndex);
1016
1240
  const fracPart = normalized.slice(sepIndex + foundSep.length);
1017
1241
  // Integer part before separator must consist purely of ASCII digits (or empty, e.g. ".5")
1018
1242
  if (intPart.length > 0 && !/^\d+$/.test(intPart)) {
1019
- return { valid: false, complete: false, value: null };
1243
+ return EDIT_SYNTAX_ERROR;
1020
1244
  }
1021
1245
  // Fractional part after separator must consist purely of ASCII digits (or empty, e.g. "12.")
1022
1246
  if (fracPart.length > 0 && !/^\d+$/.test(fracPart)) {
1023
- return { valid: false, complete: false, value: null };
1247
+ return EDIT_SYNTAX_ERROR;
1024
1248
  }
1025
1249
  // Count fraction digits using normalized ASCII characters (safe against UTF-16 astral splitting)
1026
1250
  if (fracPart.length > decimals) {
1027
- return { valid: false, complete: false, value: null };
1251
+ return { valid: false, complete: false, value: null, reason: "decimals" };
1252
+ }
1253
+ const parts = parseLocalizedNumberParts(raw, localeId, { mode: "edit" });
1254
+ const parsed = parts === null ? null : convert(parts);
1255
+ // Refuse the keystroke that would make the value too precise for the target type, instead of rounding it.
1256
+ if (parsed === "precision") {
1257
+ return { valid: false, complete: false, value: null, reason: "precision" };
1028
1258
  }
1029
- const parsed = parseLocalizedNumber(raw, localeId, { mode: "edit" });
1030
1259
  const complete = fracPart.length > 0 && (intPart.length > 0 || fracPart.length > 0);
1031
1260
  return {
1032
1261
  valid: true,
@@ -1036,9 +1265,13 @@ function validateLocalizedNumberEdit(text, localeId, options) {
1036
1265
  }
1037
1266
  // No separator found: must be purely ASCII digits
1038
1267
  if (!/^\d+$/.test(normalized)) {
1039
- return { valid: false, complete: false, value: null };
1268
+ return EDIT_SYNTAX_ERROR;
1269
+ }
1270
+ const parts = parseLocalizedNumberParts(raw, localeId, { mode: "edit" });
1271
+ const parsed = parts === null ? null : convert(parts);
1272
+ if (parsed === "precision") {
1273
+ return { valid: false, complete: false, value: null, reason: "precision" };
1040
1274
  }
1041
- const parsed = parseLocalizedNumber(raw, localeId, { mode: "edit" });
1042
1275
  return {
1043
1276
  valid: true,
1044
1277
  complete: true,
@@ -4979,5 +5212,5 @@ function generatePseudoLocale(catalogs, options) {
4979
5212
  * Generated bundle index. Do not edit.
4980
5213
  */
4981
5214
 
4982
- export { BIDI_CONTROL_REGEX, MONA_DEFAULT_LOCALE, MONA_I18N_CONFIG, MonaDirectionService, MonaI18nService, formatGregorianDateToLocaleString, formatNumber, generatePseudoLocale, getEditDecimalSeparators, getLocaleDateInputFormat, getLocaleDateTimeInputFormat, getLocaleFirstDayOfWeek, getLocaleTimeInputFormat, getNumberFormatter, getNumberGroupingPattern, getNumberSymbols, gregorianDateTime, gregorianDateTimeFromObject, injectComponentDirection, mergeMessages, mergeTwo, normalizeGregorianLocale, normalizeLocalizedDigits, normalizeLocalizedInput, normalizeLocalizedMinus, observeComponentDirection, parseGregorianDate, parseLocalizedNumber, provideMonaI18n, pseudoLocalize, pseudoLocalizeCatalog, resolveComponentDirection, sanitizeDateText, stripBidiControls, validateLocalizedNumber, validateLocalizedNumberEdit };
5215
+ export { BIDI_CONTROL_REGEX, MONA_DEFAULT_LOCALE, MONA_FALLBACK_LOCALE_ID, MONA_I18N_CONFIG, MonaDirectionService, MonaI18nService, canonicalizeLocaleId, formatGregorianDateToLocaleString, formatNumber, generatePseudoLocale, getEditDecimalSeparators, getLocaleDateInputFormat, getLocaleDateTimeInputFormat, getLocaleFirstDayOfWeek, getLocaleTimeInputFormat, getNumberFormatter, getNumberGroupingPattern, getNumberSymbols, gregorianDateTime, gregorianDateTimeFromObject, injectComponentDirection, mergeMessages, mergeTwo, normalizeGregorianLocale, normalizeLocalizedDigits, normalizeLocalizedInput, normalizeLocalizedMinus, observeComponentDirection, parseGregorianDate, parseLocalizedDecimal, parseLocalizedNumber, provideMonaI18n, pseudoLocalize, pseudoLocalizeCatalog, resolveComponentDirection, sanitizeDateText, stripBidiControls, tryCanonicalizeLocaleId, validateLocalizedDecimal, validateLocalizedDecimalEdit, validateLocalizedNumber, validateLocalizedNumberEdit };
4983
5216
  //# sourceMappingURL=nanahoshi-mona-ui-i18n.mjs.map
@@ -14,6 +14,8 @@ import { v4 } from 'uuid';
14
14
  import { cva } from 'class-variance-authority';
15
15
  import { CheckboxDirective } from '@nanahoshi/mona-ui/check-box';
16
16
 
17
+ // Generated from i18n/catalogs/en-US.messages.json by scripts/generate-locale-catalogs.ts. Do not edit.
18
+ // Edit the JSON file and run `npm run generate:locale-catalogs`.
17
19
  const LIST_DEFAULT_MESSAGES = {
18
20
  filterList: "Filter list",
19
21
  noData: "No data"
@@ -1352,6 +1352,8 @@ const treeNodeExpanderThemeVariants = cva(`
1352
1352
  cursor-pointer text-sm text-foreground
1353
1353
  `);
1354
1354
 
1355
+ // Generated from i18n/catalogs/en-US.messages.json by scripts/generate-locale-catalogs.ts. Do not edit.
1356
+ // Edit the JSON file and run `npm run generate:locale-catalogs`.
1355
1357
  const TREE_DEFAULT_MESSAGES = {
1356
1358
  collapse: "Collapse",
1357
1359
  expand: "Expand",