@astryxdesign/core 0.5.1 → 0.5.2-canary.1917581

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 (111) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  3. package/dist/BottomSheet/BottomSheet.js +1 -1
  4. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +2 -2
  5. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheetEdgeTint.js +7 -10
  7. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  8. package/dist/BottomSheet/BottomSheetSwitcher.js +1 -1
  9. package/dist/Button/Button.d.ts +2 -1
  10. package/dist/Button/Button.d.ts.map +1 -1
  11. package/dist/Button/Button.js +11 -4
  12. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  13. package/dist/Chat/ChatToolCalls.js +34 -17
  14. package/dist/Popover/Popover.d.ts +10 -6
  15. package/dist/Popover/Popover.d.ts.map +1 -1
  16. package/dist/Popover/Popover.js +141 -16
  17. package/dist/Popover/usePopover.d.ts +4 -0
  18. package/dist/Popover/usePopover.d.ts.map +1 -1
  19. package/dist/Popover/usePopover.js +52 -5
  20. package/dist/Toast/useToastGesture.d.ts.map +1 -1
  21. package/dist/Toast/useToastGesture.js +46 -9
  22. package/dist/astryx.css +10 -0
  23. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  24. package/dist/hooks/useFocusTrap.js +12 -0
  25. package/dist/i18n/InternationalizationContext.d.ts +2 -0
  26. package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
  27. package/dist/i18n/InternationalizationContext.js +3 -1
  28. package/dist/i18n/InternationalizationProvider.d.ts.map +1 -1
  29. package/dist/i18n/InternationalizationProvider.js +3 -1
  30. package/dist/i18n/resolve.d.ts +1 -1
  31. package/dist/i18n/resolve.d.ts.map +1 -1
  32. package/dist/i18n/resolve.js +37 -38
  33. package/dist/i18n/useTranslator.d.ts.map +1 -1
  34. package/dist/i18n/useTranslator.js +2 -3
  35. package/locales/af-ZA.json +0 -4
  36. package/locales/ar-SA.json +0 -4
  37. package/locales/ca-ES.json +0 -4
  38. package/locales/cs-CZ.json +0 -4
  39. package/locales/da-DK.json +0 -4
  40. package/locales/de-DE.json +0 -4
  41. package/locales/el-GR.json +0 -4
  42. package/locales/en.json +16 -0
  43. package/locales/es-ES.json +0 -4
  44. package/locales/fi-FI.json +0 -4
  45. package/locales/fr-FR.json +0 -4
  46. package/locales/he-IL.json +0 -4
  47. package/locales/hu-HU.json +0 -4
  48. package/locales/it-IT.json +0 -4
  49. package/locales/ja-JP.json +0 -4
  50. package/locales/ko-KR.json +0 -4
  51. package/locales/nl-NL.json +0 -4
  52. package/locales/no-NO.json +0 -4
  53. package/locales/pl-PL.json +0 -4
  54. package/locales/pseudo.json +12 -0
  55. package/locales/pt-BR.json +0 -4
  56. package/locales/pt-PT.json +0 -4
  57. package/locales/ro-RO.json +0 -4
  58. package/locales/ru-RU.json +0 -4
  59. package/locales/sr-SP.json +0 -4
  60. package/locales/sv-SE.json +0 -4
  61. package/locales/tr-TR.json +0 -4
  62. package/locales/uk-UA.json +0 -4
  63. package/locales/vi-VN.json +0 -4
  64. package/locales/zh-CN.json +0 -4
  65. package/locales/zh-TW.json +0 -4
  66. package/package.json +3 -3
  67. package/src/BottomSheet/BottomSheet.tsx +1 -2
  68. package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +4 -6
  69. package/src/BottomSheet/BottomSheetEdgeTint.tsx +7 -10
  70. package/src/BottomSheet/BottomSheetSwitcher.tsx +1 -2
  71. package/src/Button/Button.test.tsx +10 -0
  72. package/src/Button/Button.tsx +13 -5
  73. package/src/Calendar/Calendar.doc.mjs +1 -1
  74. package/src/Chat/ChatToolCalls.test.tsx +44 -0
  75. package/src/Chat/ChatToolCalls.tsx +36 -15
  76. package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
  77. package/src/CheckboxList/CheckboxList.doc.mjs +1 -1
  78. package/src/ComplexSelector/ComplexSelector.doc.mjs +2 -2
  79. package/src/DateInput/DateInput.doc.mjs +1 -1
  80. package/src/DateRangeInput/DateRangeInput.doc.mjs +1 -1
  81. package/src/DateTimeInput/DateTimeInput.doc.mjs +1 -1
  82. package/src/Field/Field.doc.mjs +1 -1
  83. package/src/FieldStatus/FieldStatus.doc.mjs +1 -1
  84. package/src/FileInput/FileInput.doc.mjs +1 -1
  85. package/src/Indicator/Indicator.doc.mjs +1 -1
  86. package/src/InputGroup/InputGroup.doc.mjs +1 -1
  87. package/src/MultiSelector/MultiSelector.doc.mjs +1 -1
  88. package/src/NumberInput/NumberInput.doc.mjs +1 -1
  89. package/src/Popover/Popover.doc.mjs +11 -11
  90. package/src/Popover/Popover.test.tsx +397 -2
  91. package/src/Popover/Popover.tsx +228 -21
  92. package/src/Popover/usePopover.tsx +92 -15
  93. package/src/PowerSearch/PowerSearch.doc.mjs +1 -1
  94. package/src/RadioList/RadioList.doc.mjs +1 -1
  95. package/src/Selector/Selector.doc.mjs +1 -1
  96. package/src/Slider/Slider.doc.mjs +2 -2
  97. package/src/Switch/Switch.doc.mjs +1 -1
  98. package/src/TextArea/TextArea.doc.mjs +1 -1
  99. package/src/TextInput/TextInput.doc.mjs +1 -1
  100. package/src/TimeInput/TimeInput.doc.mjs +1 -1
  101. package/src/Toast/ToastViewport.test.tsx +118 -4
  102. package/src/Toast/useToastGesture.ts +61 -8
  103. package/src/Tokenizer/Tokenizer.doc.mjs +1 -1
  104. package/src/Typeahead/Typeahead.doc.mjs +1 -1
  105. package/src/hooks/useFocusTrap.test.tsx +40 -0
  106. package/src/hooks/useFocusTrap.ts +13 -0
  107. package/src/i18n/InternationalizationContext.ts +3 -0
  108. package/src/i18n/InternationalizationProvider.tsx +8 -1
  109. package/src/i18n/__tests__/resolve.test.ts +23 -1
  110. package/src/i18n/resolve.ts +46 -49
  111. package/src/i18n/useTranslator.ts +2 -7
@@ -22,6 +22,7 @@ import { LayerDepthProvider } from "../Layer/LayerDepthContext.js";
22
22
  import "../theme/tokens.stylex.js";
23
23
  import { colorVars, spacingVars, radiusVars, shadowVars } from "../theme/tokens.stylex.js";
24
24
  import { Button } from "../Button/index.js";
25
+ import { FOCUSABLE_SELECTOR } from "../hooks/focusableSelector.js";
25
26
  import { rtlStyles } from "../utils/index.js";
26
27
  import { useTranslator } from "../i18n/index.js";
27
28
  import { useDevWarning } from "../hooks/useDevWarning.js";
@@ -29,6 +30,24 @@ import { mergeProps } from "../utils/mergeProps.js";
29
30
  import { themeProps } from "../utils/themeProps.js";
30
31
  import { stableClassName } from "../naming.js";
31
32
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
33
+ const FALLBACK_CLOSE_SELECTOR = '[data-astryx-popover-fallback-close]';
34
+ function attemptFocus(element) {
35
+ try {
36
+ element.focus();
37
+ } catch {
38
+ // Ignore non-focusable elements; the caller will try the next target.
39
+ }
40
+ return document.activeElement === element;
41
+ }
42
+ function focusFirstContentControl(container) {
43
+ const focusable = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter(element => element.closest(FALLBACK_CLOSE_SELECTOR) == null);
44
+ for (const element of focusable) {
45
+ if (attemptFocus(element)) {
46
+ return true;
47
+ }
48
+ }
49
+ return false;
50
+ }
32
51
  const styles = {
33
52
  surface: {
34
53
  kWkggS: "x1prclbq",
@@ -39,6 +58,7 @@ const styles = {
39
58
  },
40
59
  contentWrapper: {
41
60
  kVAEAm: "x1n2onr6",
61
+ kI3sdo: "x1a2a7pz",
42
62
  $$css: true
43
63
  },
44
64
  closeButtonWrapper: {
@@ -129,6 +149,7 @@ function usePopoverImplementation(options = {}) {
129
149
 
130
150
  // Track whether to skip auto-focus for the current open event
131
151
  const skipAutoFocusRef = useRef(false);
152
+ const focusTargetRef = useRef('first');
132
153
 
133
154
  // Core layer for popover positioning
134
155
  const layer = useLayerInternal({
@@ -148,20 +169,40 @@ function usePopoverImplementation(options = {}) {
148
169
  isActive: layer.isOpen,
149
170
  onEscape: hasEscapeDismiss || hasLightDismiss ? layer.hide : undefined
150
171
  });
172
+ const focusInitialTarget = useCallback(() => {
173
+ const container = contentRef.current;
174
+ if (!container) {
175
+ return;
176
+ }
177
+ if (focusFirstContentControl(container)) {
178
+ return;
179
+ }
180
+ if (role === 'dialog') {
181
+ attemptFocus(container);
182
+ return;
183
+ }
184
+ focusFirst();
185
+ }, [contentRef, focusFirst, role]);
151
186
 
152
187
  // Auto-focus first element when popover opens (unless skipped)
153
188
  useEffect(() => {
154
189
  if (layer.isOpen && hasAutoFocus && !skipAutoFocusRef.current) {
155
190
  // Use requestAnimationFrame to ensure DOM is ready
156
191
  requestAnimationFrame(() => {
157
- focusFirst();
192
+ const container = contentRef.current;
193
+ if (focusTargetRef.current === 'container' && role === 'dialog' && container) {
194
+ attemptFocus(container);
195
+ } else {
196
+ focusInitialTarget();
197
+ }
158
198
  });
159
199
  }
160
- // Reset the skip flag after the effect runs
200
+ // Reset per-open focus preferences after the popover closes.
161
201
  if (!layer.isOpen) {
162
202
  skipAutoFocusRef.current = false;
203
+ focusTargetRef.current = 'first';
163
204
  }
164
- }, [layer.isOpen, hasAutoFocus, focusFirst]);
205
+ }, [contentRef, layer.isOpen, hasAutoFocus, focusInitialTarget, role]);
165
206
 
166
207
  // Combined ref for trigger element (layer anchor + our ref)
167
208
  const triggerRef = useCallback(el => {
@@ -172,20 +213,22 @@ function usePopoverImplementation(options = {}) {
172
213
  // Show function with optional skipAutoFocus
173
214
  const show = useCallback(showOptions => {
174
215
  skipAutoFocusRef.current = showOptions?.skipAutoFocus ?? false;
216
+ focusTargetRef.current = showOptions?.focusTarget ?? 'first';
175
217
  layer.show();
176
218
  }, [layer]);
177
219
 
178
220
  // Toggle function
179
- const toggle = useCallback(() => {
221
+ const toggleWithOptions = useCallback(showOptions => {
180
222
  if (layer.wasJustDismissed()) {
181
223
  return;
182
224
  }
183
225
  if (layer.isOpen) {
184
226
  layer.hide();
185
227
  } else {
186
- show();
228
+ show(showOptions);
187
229
  }
188
230
  }, [layer, show]);
231
+ const toggle = useCallback(() => toggleWithOptions(), [toggleWithOptions]);
189
232
 
190
233
  // ARIA attributes for the trigger
191
234
  const triggerProps = {
@@ -210,11 +253,13 @@ function usePopoverImplementation(options = {}) {
210
253
  role: role === 'dialog' ? 'dialog' : undefined,
211
254
  "aria-modal": role === 'dialog' && isModal ? true : undefined,
212
255
  "aria-label": role === 'dialog' ? dialogLabel : undefined,
256
+ tabIndex: role === 'dialog' ? -1 : undefined,
213
257
  ...mergeProps({
214
258
  ...surfaceProps,
215
259
  className: surfaceClassName
216
260
  }, stylex.props(styles.contentWrapper, hasSurface && styles.surface, xstyle), className, style),
217
261
  children: [children, hasCloseButton && /*#__PURE__*/_jsx("div", {
262
+ "data-astryx-popover-fallback-close": "",
218
263
  ...stylex.props(styles.closeButtonWrapper, rtlStyles.centerInline('100%')),
219
264
  children: /*#__PURE__*/_jsx(Button, {
220
265
  variant: "secondary",
@@ -235,6 +280,7 @@ function usePopoverImplementation(options = {}) {
235
280
  show,
236
281
  hide: layer.hide,
237
282
  toggle,
283
+ toggleWithOptions,
238
284
  wasJustDismissed: layer.wasJustDismissed,
239
285
  isOpen: layer.isOpen,
240
286
  id: layer.id,
@@ -245,6 +291,7 @@ function usePopoverImplementation(options = {}) {
245
291
  export function usePopover(options = {}) {
246
292
  const {
247
293
  wasJustDismissed: _,
294
+ toggleWithOptions: __,
248
295
  ...popover
249
296
  } = usePopoverImplementation(options);
250
297
  return popover;
@@ -1 +1 @@
1
- {"version":3,"file":"useToastGesture.d.ts","sourceRoot":"","sources":["../../src/Toast/useToastGesture.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAC,YAAY,IAAI,iBAAiB,EAAE,SAAS,EAAC,MAAM,OAAO,CAAC;AAWxE,MAAM,MAAM,qBAAqB,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AAoB3C,UAAU,sBAAsB;IAC9B,SAAS,EAAE,qBAAqB,CAAC;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;IACvB,aAAa,EAAE,MAAM,OAAO,CAAC;IAC7B,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,kBAAkB,EAAE,CAClB,MAAM,EAAE,WAAW,GAAG,IAAI,EAC1B,IAAI,EAAE,WAAW,KACd,OAAO,CAAC;CACd;AAED,UAAU,oBAAoB;IAC5B,aAAa,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;IAClE,aAAa,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;IAClE,WAAW,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;IAChE,eAAe,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;IACpE,oBAAoB,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;CAC1E;AAED,UAAU,qBAAqB;IAC7B,OAAO,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC1C,QAAQ,EAAE,oBAAoB,CAAC;CAChC;AAcD,wBAAgB,eAAe,CAAC,EAC9B,SAAS,EACT,OAAO,EACP,aAAa,EACb,aAAa,EACb,UAAU,EACV,WAAW,EACX,OAAO,EACP,kBAAkB,GACnB,EAAE,sBAAsB,GAAG,qBAAqB,CA4QhD"}
1
+ {"version":3,"file":"useToastGesture.d.ts","sourceRoot":"","sources":["../../src/Toast/useToastGesture.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAC,YAAY,IAAI,iBAAiB,EAAE,SAAS,EAAC,MAAM,OAAO,CAAC;AAWxE,MAAM,MAAM,qBAAqB,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AAsB3C,UAAU,sBAAsB;IAC9B,SAAS,EAAE,qBAAqB,CAAC;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;IACvB,aAAa,EAAE,MAAM,OAAO,CAAC;IAC7B,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,kBAAkB,EAAE,CAClB,MAAM,EAAE,WAAW,GAAG,IAAI,EAC1B,IAAI,EAAE,WAAW,KACd,OAAO,CAAC;CACd;AAED,UAAU,oBAAoB;IAC5B,aAAa,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;IAClE,aAAa,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;IAClE,WAAW,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;IAChE,eAAe,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;IACpE,oBAAoB,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;CAC1E;AAED,UAAU,qBAAqB;IAC7B,OAAO,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC1C,QAAQ,EAAE,oBAAoB,CAAC;CAChC;AAcD,wBAAgB,eAAe,CAAC,EAC9B,SAAS,EACT,OAAO,EACP,aAAa,EACb,aAAa,EACb,UAAU,EACV,WAAW,EACX,OAAO,EACP,kBAAkB,GACnB,EAAE,sBAAsB,GAAG,qBAAqB,CA+ThD"}
@@ -62,6 +62,7 @@ export function useToastGesture({
62
62
  }
63
63
  const surfaceSize = Math.max(root.getBoundingClientRect().height, 1);
64
64
  gestureRef.current = {
65
+ source: point.source,
65
66
  pointerId: point.pointerId,
66
67
  startX: point.clientX,
67
68
  startY: point.clientY,
@@ -77,7 +78,7 @@ export function useToastGesture({
77
78
  const moveGesture = useCallback((point, preventDefault, releaseCapture) => {
78
79
  const state = gestureRef.current;
79
80
  const root = rootRef.current;
80
- if (!state || !root || point.pointerId !== state.pointerId) {
81
+ if (!state || !root || point.source !== state.source || point.pointerId !== state.pointerId) {
81
82
  return;
82
83
  }
83
84
  const deltaX = point.clientX - state.startX;
@@ -111,7 +112,7 @@ export function useToastGesture({
111
112
  const endGesture = useCallback(point => {
112
113
  const state = gestureRef.current;
113
114
  const root = rootRef.current;
114
- if (!state || !root || point.pointerId !== state.pointerId) {
115
+ if (!state || !root || point.source !== state.source || point.pointerId !== state.pointerId) {
115
116
  return;
116
117
  }
117
118
  const travel = Math.max(0, (point.clientY - state.startY) * state.direction);
@@ -120,6 +121,10 @@ export function useToastGesture({
120
121
  gestureRef.current = null;
121
122
  root.style.removeProperty('transition-duration');
122
123
  if (isDismissed) {
124
+ // The exit animation owns the final translation. Clear the live-drag
125
+ // transform/fade/scale first so a standalone Toast does not remain
126
+ // partially translated or faded after its dismiss callback fires.
127
+ clearTransientStyles(root);
123
128
  root.style.setProperty('--_toast-swipe-exit-y', state.direction === 1 ? SWIPE_EXIT_DISTANCE : `calc(-1 * ${SWIPE_EXIT_DISTANCE})`);
124
129
  dismiss();
125
130
  return;
@@ -130,7 +135,12 @@ export function useToastGesture({
130
135
  }
131
136
  }, [dismiss, resumeTimer]);
132
137
  const handlePointerDown = useCallback(event => {
133
- if (event.pointerType === 'pen' && (event.button == null || event.button === 0) && beginGesture(event, event.target)) {
138
+ if (event.pointerType === 'pen' && (event.button == null || event.button === 0) && beginGesture({
139
+ source: 'pen',
140
+ pointerId: event.pointerId,
141
+ clientX: event.clientX,
142
+ clientY: event.clientY
143
+ }, event.target)) {
134
144
  rootRef.current?.setPointerCapture?.(event.pointerId);
135
145
  }
136
146
  }, [beginGesture]);
@@ -138,17 +148,27 @@ export function useToastGesture({
138
148
  if (event.pointerType !== 'pen') {
139
149
  return;
140
150
  }
141
- moveGesture(event, () => event.preventDefault(), () => rootRef.current?.releasePointerCapture?.(event.pointerId));
151
+ moveGesture({
152
+ source: 'pen',
153
+ pointerId: event.pointerId,
154
+ clientX: event.clientX,
155
+ clientY: event.clientY
156
+ }, () => event.preventDefault(), () => rootRef.current?.releasePointerCapture?.(event.pointerId));
142
157
  }, [moveGesture]);
143
158
  const handlePointerUp = useCallback(event => {
144
159
  if (event.pointerType !== 'pen') {
145
160
  return;
146
161
  }
147
162
  rootRef.current?.releasePointerCapture?.(event.pointerId);
148
- endGesture(event);
163
+ endGesture({
164
+ source: 'pen',
165
+ pointerId: event.pointerId,
166
+ clientX: event.clientX,
167
+ clientY: event.clientY
168
+ });
149
169
  }, [endGesture]);
150
170
  const handlePointerCancel = useCallback(event => {
151
- if (event.pointerType === 'pen' && gestureRef.current?.pointerId === event.pointerId) {
171
+ if (event.pointerType === 'pen' && gestureRef.current?.source === 'pen' && gestureRef.current?.pointerId === event.pointerId) {
152
172
  resetGesture(true);
153
173
  }
154
174
  }, [resetGesture]);
@@ -158,21 +178,34 @@ export function useToastGesture({
158
178
  return;
159
179
  }
160
180
  const point = touch => ({
181
+ source: 'touch',
161
182
  pointerId: touch.identifier,
162
183
  clientX: touch.clientX,
163
184
  clientY: touch.clientY
164
185
  });
165
186
  const changedTouch = event => {
166
- const pointerId = gestureRef.current?.pointerId;
187
+ const state = gestureRef.current;
188
+ const pointerId = state?.source === 'touch' ? state.pointerId : null;
167
189
  return pointerId == null ? undefined : [...event.changedTouches].find(touch => touch.identifier === pointerId);
168
190
  };
169
191
  const handleTouchStart = event => {
170
- const touch = event.touches.length === 1 ? event.changedTouches[0] : null;
171
- if (touch) {
192
+ if (event.touches?.length !== 1) {
193
+ // A second contact means the browser may be handling pinch zoom or
194
+ // two-finger scrolling. Abandon any accepted one-finger swipe before
195
+ // touchmove can suppress that native gesture.
196
+ resetGesture(true);
197
+ return;
198
+ }
199
+ const touch = event.changedTouches[0];
200
+ if (touch != null) {
172
201
  beginGesture(point(touch), event.target);
173
202
  }
174
203
  };
175
204
  const handleTouchMove = event => {
205
+ if (event.touches?.length !== 1) {
206
+ resetGesture(true);
207
+ return;
208
+ }
176
209
  const touch = changedTouch(event);
177
210
  if (touch) {
178
211
  moveGesture(point(touch), () => {
@@ -183,6 +216,10 @@ export function useToastGesture({
183
216
  }
184
217
  };
185
218
  const handleTouchEnd = event => {
219
+ if (event.touches.length > 0) {
220
+ resetGesture(true);
221
+ return;
222
+ }
186
223
  const touch = changedTouch(event);
187
224
  if (touch) {
188
225
  endGesture(point(touch));
package/dist/astryx.css CHANGED
@@ -1012,6 +1012,7 @@
1012
1012
  .xkd40ry:not(#\#):not(#\#):not(#\#){margin-inline-end:calc(-1 * var(--spacing-1))}
1013
1013
  .x3kzqx6:not(#\#):not(#\#):not(#\#){margin-inline-end:calc(-1 * var(--spacing-2))}
1014
1014
  .xmequkd:not(#\#):not(#\#):not(#\#){margin-inline-end:calc(var(--spacing-1) * -1)}
1015
+ .xxeicf8:not(#\#):not(#\#):not(#\#){margin-inline-end:max(var(--spacing-4),env(safe-area-inset-left,0px),env(safe-area-inset-right,0px))}
1015
1016
  .xnur1sd:not(#\#):not(#\#):not(#\#){margin-inline-end:var(--spacing-1)}
1016
1017
  .x1c9tiao:not(#\#):not(#\#):not(#\#){margin-inline-end:var(--x-marginInlineEnd)}
1017
1018
  .x1qfufaz:not(#\#):not(#\#):not(#\#){margin-inline-start:-8px}
@@ -1022,6 +1023,7 @@
1022
1023
  .x1wim8z0:not(#\#):not(#\#):not(#\#){margin-inline-start:calc(-1 * var(--layout-padding-inner-x,var(--spacing-4)))}
1023
1024
  .x1s1akpx:not(#\#):not(#\#):not(#\#){margin-inline-start:calc(-1 * var(--spacing-2))}
1024
1025
  .x1xrm450:not(#\#):not(#\#):not(#\#){margin-inline-start:calc(var(--spacing-2) - var(--spacing-1) + 1px)}
1026
+ .x47fqr9:not(#\#):not(#\#):not(#\#){margin-inline-start:max(var(--spacing-4),env(safe-area-inset-left,0px),env(safe-area-inset-right,0px))}
1025
1027
  .x12vn87o:not(#\#):not(#\#):not(#\#){margin-inline-start:var(--_tree-indent,0px)}
1026
1028
  .xladpz9:not(#\#):not(#\#):not(#\#){margin-inline-start:var(--spacing-0-5)}
1027
1029
  .x11g1kdw:not(#\#):not(#\#):not(#\#){margin-inline-start:var(--spacing-1)}
@@ -1331,6 +1333,7 @@
1331
1333
  .xbt4iw:focus-within:not(#\#):not(#\#):not(#\#){pointer-events:auto}
1332
1334
  .xo74pv5:focus-within:not(#\#):not(#\#):not(#\#){position:static}
1333
1335
  .x1mqgsch:is([data-astryx-scrolling]):not(#\#):not(#\#):not(#\#){scrollbar-width:none}
1336
+ .x8ppqj.x8ppqj:active:where(:not(:disabled,[aria-disabled="true"])):not(#\#):not(#\#):not(#\#){transform:scale(.98)}
1334
1337
  .xaafvuh:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){transform:scaleX(-1) rotate(0deg)}
1335
1338
  .x1nxqau:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){transform:scaleX(-1) rotate(90deg)}
1336
1339
  .xgtlewx:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){transform:scaleX(-1)}
@@ -1555,6 +1558,7 @@
1555
1558
  .x6h7pi7:not(#\#):not(#\#):not(#\#):not(#\#){margin-bottom:calc(-1 * var(--container-padding-block-end,0px))}
1556
1559
  .x1cdrix3:not(#\#):not(#\#):not(#\#):not(#\#){margin-bottom:calc(-1 * var(--radius-chat))}
1557
1560
  .xdrfirk:not(#\#):not(#\#):not(#\#):not(#\#){margin-bottom:calc(-1 * var(--spacing-1-5))}
1561
+ .x1kps3mo:not(#\#):not(#\#):not(#\#):not(#\#){margin-bottom:max(var(--spacing-4),env(safe-area-inset-bottom,0px))}
1558
1562
  .xlstkdb:not(#\#):not(#\#):not(#\#):not(#\#){margin-bottom:var(--spacing-0-5)}
1559
1563
  .x14cgwvg:not(#\#):not(#\#):not(#\#):not(#\#){margin-bottom:var(--spacing-1)}
1560
1564
  .x1p37lm5:not(#\#):not(#\#):not(#\#):not(#\#){margin-bottom:var(--spacing-2)}
@@ -1572,6 +1576,7 @@
1572
1576
  .x1c40v9y:not(#\#):not(#\#):not(#\#):not(#\#){margin-top:calc(-1 * var(--spacing-1-5))}
1573
1577
  .x1ajqlyv:not(#\#):not(#\#):not(#\#):not(#\#){margin-top:calc(-1 * var(--spacing-2))}
1574
1578
  .x1233pnv:not(#\#):not(#\#):not(#\#):not(#\#){margin-top:calc((1em * var(--text-body-leading) - 6px) / 2)}
1579
+ .xsoubt3:not(#\#):not(#\#):not(#\#):not(#\#){margin-top:max(var(--spacing-4),env(safe-area-inset-top,0px))}
1575
1580
  .xjc2qm6:not(#\#):not(#\#):not(#\#):not(#\#){margin-top:var(--spacing-0-5)}
1576
1581
  .xcsaf9d:not(#\#):not(#\#):not(#\#):not(#\#){margin-top:var(--spacing-1)}
1577
1582
  .xtbrsbv:not(#\#):not(#\#):not(#\#):not(#\#){margin-top:var(--spacing-2)}
@@ -1585,6 +1590,7 @@
1585
1590
  .x1xiyphd:not(#\#):not(#\#):not(#\#):not(#\#){max-height:240px}
1586
1591
  .xuyqlj2:not(#\#):not(#\#):not(#\#):not(#\#){max-height:300px}
1587
1592
  .xbhvqqn:not(#\#):not(#\#):not(#\#):not(#\#){max-height:calc(100% - var(--spacing-3))}
1593
+ .xtm2925:not(#\#):not(#\#):not(#\#):not(#\#){max-height:calc(100vh - var(--spacing-4) - var(--spacing-4));max-height:calc(100dvb - max(var(--spacing-4),env(safe-area-inset-top,0px)) - max(var(--spacing-4),env(safe-area-inset-bottom,0px)))}
1588
1594
  .x14bu9tk:not(#\#):not(#\#):not(#\#):not(#\#){max-height:calc(92dvh + 48px)}
1589
1595
  .xpnbb6b:not(#\#):not(#\#):not(#\#):not(#\#){max-height:min(480px,calc(100vh - 32px))}
1590
1596
  .x7ab17h:not(#\#):not(#\#):not(#\#):not(#\#){max-height:none}
@@ -1604,6 +1610,8 @@
1604
1610
  .x11gisft:not(#\#):not(#\#):not(#\#):not(#\#){max-width:640px}
1605
1611
  .x1ap80js:not(#\#):not(#\#):not(#\#):not(#\#){max-width:800px}
1606
1612
  .xq8v1hd:not(#\#):not(#\#):not(#\#):not(#\#){max-width:90%}
1613
+ .xbd96yr:not(#\#):not(#\#):not(#\#):not(#\#){max-width:calc(100% - var(--spacing-4));max-width:calc(100% - max(var(--spacing-4),env(safe-area-inset-left,0px),env(safe-area-inset-right,0px)))}
1614
+ .xs2v1xk:not(#\#):not(#\#):not(#\#):not(#\#){max-width:calc(100vw - var(--spacing-4) - var(--spacing-4));max-width:calc(100vi - max(var(--spacing-4),env(safe-area-inset-left,0px)) - max(var(--spacing-4),env(safe-area-inset-right,0px)))}
1607
1615
  .xl6temz:not(#\#):not(#\#):not(#\#):not(#\#){max-width:max(80%,280px)}
1608
1616
  .x1m1aaou:not(#\#):not(#\#):not(#\#):not(#\#){max-width:min(960px,calc(100dvw - var(--spacing-4)))}
1609
1617
  .x1x1rfll:not(#\#):not(#\#):not(#\#):not(#\#){max-width:none}
@@ -1641,6 +1649,8 @@
1641
1649
  .x900493:not(#\#):not(#\#):not(#\#):not(#\#){min-width:48px}
1642
1650
  .x13n9wmd:not(#\#):not(#\#):not(#\#):not(#\#){min-width:4ch}
1643
1651
  .xfvyar9:not(#\#):not(#\#):not(#\#):not(#\#){min-width:60px}
1652
+ .x1ks9poc:not(#\#):not(#\#):not(#\#):not(#\#){min-width:anchor-size(width);min-width:min(anchor-size(width),calc(100% - var(--spacing-4)));min-width:min(anchor-size(width),calc(100% - max(var(--spacing-4),env(safe-area-inset-left,0px),env(safe-area-inset-right,0px))))}
1653
+ .xagy28z:not(#\#):not(#\#):not(#\#):not(#\#){min-width:anchor-size(width);min-width:min(anchor-size(width),calc(100vw - var(--spacing-4) - var(--spacing-4)));min-width:min(anchor-size(width),calc(100vi - max(var(--spacing-4),env(safe-area-inset-left,0px)) - max(var(--spacing-4),env(safe-area-inset-right,0px))))}
1644
1654
  .xrzjruh:not(#\#):not(#\#):not(#\#):not(#\#){min-width:anchor-size(width)}
1645
1655
  .x8wyhu6:not(#\#):not(#\#):not(#\#):not(#\#){min-width:calc(anchor-size(width) + 16px)}
1646
1656
  .x1pshirs:not(#\#):not(#\#):not(#\#):not(#\#){min-width:fit-content}
@@ -1 +1 @@
1
- {"version":3,"file":"useFocusTrap.d.ts","sourceRoot":"","sources":["../../src/hooks/useFocusTrap.ts"],"names":[],"mappings":"AA8BA;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,IAAI,OAAO,CAElD;AAgFD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAC;IAElB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW;IACrE;;OAEG;IACH,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAExC;;OAEG;IACH,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAC9D,OAAO,EAAE,mBAAmB,GAC3B,kBAAkB,CAAC,CAAC,CAAC,CA6NvB"}
1
+ {"version":3,"file":"useFocusTrap.d.ts","sourceRoot":"","sources":["../../src/hooks/useFocusTrap.ts"],"names":[],"mappings":"AA8BA;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,IAAI,OAAO,CAElD;AAgFD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAC;IAElB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW;IACrE;;OAEG;IACH,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAExC;;OAEG;IACH,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAC9D,OAAO,EAAE,mBAAmB,GAC3B,kBAAkB,CAAC,CAAC,CAAC,CA0OvB"}
@@ -321,6 +321,18 @@ export function useFocusTrap(options) {
321
321
  }
322
322
  const first = focusable[0];
323
323
  const last = focusable[focusable.length - 1];
324
+ if (document.activeElement === container) {
325
+ // Programmatic initial focus can sit on the trap container itself
326
+ // (for example a dialog panel with no content controls). Move into
327
+ // the tabbable cycle instead of letting Tab escape to the page.
328
+ event.preventDefault();
329
+ if (event.shiftKey) {
330
+ last.focus();
331
+ } else {
332
+ first.focus();
333
+ }
334
+ return;
335
+ }
324
336
  if (event.shiftKey) {
325
337
  // Shift+Tab: if on first element, wrap to last
326
338
  if (document.activeElement === first) {
@@ -1,9 +1,11 @@
1
+ import { getResolve } from './resolve';
1
2
  import type { Locale, MessagesByLocale, Overrides } from './types';
2
3
  export interface InternationalizationContextValue {
3
4
  locale: Locale;
4
5
  direction: 'ltr' | 'rtl';
5
6
  messages: MessagesByLocale;
6
7
  overrides?: Overrides;
8
+ translate: ReturnType<typeof getResolve>;
7
9
  }
8
10
  /**
9
11
  * Default value falls through to the shipped en catalog in resolve().
@@ -1 +1 @@
1
- {"version":3,"file":"InternationalizationContext.d.ts","sourceRoot":"","sources":["../../src/i18n/InternationalizationContext.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAC,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAEjE,MAAM,WAAW,gCAAgC;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;IACzB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED;;;GAGG;AACH,eAAO,MAAM,2BAA2B,2DAKpC,CAAC"}
1
+ {"version":3,"file":"InternationalizationContext.d.ts","sourceRoot":"","sources":["../../src/i18n/InternationalizationContext.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAC,UAAU,EAAC,MAAM,WAAW,CAAC;AACrC,OAAO,KAAK,EAAC,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAEjE,MAAM,WAAW,gCAAgC;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;IACzB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,SAAS,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;CAC1C;AAED;;;GAGG;AACH,eAAO,MAAM,2BAA2B,2DAMpC,CAAC"}
@@ -22,6 +22,7 @@
22
22
  * - /packages/core/src/i18n/index.ts
23
23
  */
24
24
  import { createContext } from 'react';
25
+ import { getResolve } from "./resolve.js";
25
26
  /**
26
27
  * Default value falls through to the shipped en catalog in resolve().
27
28
  * A consumer that doesn't render a provider still gets English defaults.
@@ -29,6 +30,7 @@ import { createContext } from 'react';
29
30
  export const InternationalizationContext = /*#__PURE__*/createContext({
30
31
  locale: 'en',
31
32
  direction: 'ltr',
32
- messages: {}
33
+ messages: {},
34
+ translate: getResolve('en', {})
33
35
  });
34
36
  InternationalizationContext.displayName = 'InternationalizationContext';
@@ -1 +1 @@
1
- {"version":3,"file":"InternationalizationProvider.d.ts","sourceRoot":"","sources":["../../src/i18n/InternationalizationProvider.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAU,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAG9C,OAAO,KAAK,EAAC,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAEjE,MAAM,WAAW,iCAAiC;IAChD;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;;;;;;;;OAWG;IACH,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,EAC3C,MAAM,EACN,QAAQ,EACR,SAAS,EACT,GAAG,EACH,QAAQ,GACT,EAAE,iCAAiC,+BAWnC;yBAjBe,4BAA4B"}
1
+ {"version":3,"file":"InternationalizationProvider.d.ts","sourceRoot":"","sources":["../../src/i18n/InternationalizationProvider.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAU,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAG9C,OAAO,KAAK,EAAC,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAGjE,MAAM,WAAW,iCAAiC;IAChD;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;;;;;;;;OAWG;IACH,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,EAC3C,MAAM,EACN,QAAQ,EACR,SAAS,EACT,GAAG,EACH,QAAQ,GACT,EAAE,iCAAiC,+BAiBnC;yBAvBe,4BAA4B"}
@@ -21,6 +21,7 @@
21
21
  import { useMemo } from 'react';
22
22
  import { InternationalizationContext } from "./InternationalizationContext.js";
23
23
  import { getLocaleDirection } from "./getLocaleDirection.js";
24
+ import { getResolve } from "./resolve.js";
24
25
  import { jsx as _jsx } from "react/jsx-runtime";
25
26
  /**
26
27
  * Provides locale + additional messages + overrides to all astryx
@@ -38,7 +39,8 @@ export function InternationalizationProvider({
38
39
  locale,
39
40
  direction,
40
41
  messages: messages ?? {},
41
- overrides
42
+ overrides,
43
+ translate: getResolve(locale, messages ?? {}, overrides)
42
44
  }), [locale, direction, messages, overrides]);
43
45
  return /*#__PURE__*/_jsx(InternationalizationContext, {
44
46
  value: value,
@@ -13,7 +13,7 @@ import type { Locale, MessagesByLocale, Overrides } from './types';
13
13
  * shipped en catalog separately as the final source-of-truth.
14
14
  */
15
15
  export declare function resolveLocaleChain(locale: Locale): Locale[];
16
- export declare function resolve(key: string, values: Record<string, unknown> | undefined, locale: Locale, messages: MessagesByLocale, overrides: Overrides | undefined): string;
16
+ export declare function getResolve(locale: Locale, messages: MessagesByLocale, overrides?: Overrides): (key: string, values: Record<string, unknown> | undefined) => string;
17
17
  /**
18
18
  * Reset internal caches. Test-only.
19
19
  * @internal
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/i18n/resolve.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAU,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAwB1E;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAc3D;AAsCD,wBAAgB,OAAO,CACrB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC3C,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,gBAAgB,EAC1B,SAAS,EAAE,SAAS,GAAG,SAAS,GAC/B,MAAM,CA0BR;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAGtC"}
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/i18n/resolve.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAU,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAwB1E;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAc3D;AAkCD,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,gBAAgB,EAC1B,SAAS,CAAC,EAAE,SAAS,IAIb,KAAK,MAAM,EAAE,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,YA0BjE;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAGtC"}
@@ -69,55 +69,54 @@ export function resolveLocaleChain(locale) {
69
69
  }
70
70
  return chain;
71
71
  }
72
- function lookup(key, locale, messages, overrides) {
72
+ function getLookup(locale, messages, overrides) {
73
+ const lookup = {};
73
74
  const chain = resolveLocaleChain(locale);
74
-
75
75
  // 1 + 2. Overrides (most specific to least specific in the chain)
76
76
  if (overrides !== undefined) {
77
77
  for (const tag of chain) {
78
- const value = overrides[tag]?.[key];
79
- if (value !== undefined) {
80
- return value;
78
+ if (overrides[tag]) {
79
+ for (const [key, value] of Object.entries(overrides[tag])) {
80
+ if (lookup[key] === undefined && value !== null) {
81
+ lookup[key] = value;
82
+ }
83
+ }
81
84
  }
82
85
  }
83
86
  }
84
-
85
- // 3 + 4. Shipped catalogs (most specific to least specific)
86
87
  for (const tag of chain) {
87
- const entry = messages[tag]?.[key];
88
- if (entry !== undefined) {
89
- return entry.defaultMessage;
88
+ if (messages[tag]) {
89
+ for (const [key, value] of Object.entries(messages[tag])) {
90
+ if (lookup[key] === undefined && value?.defaultMessage !== null) {
91
+ lookup[key] = value?.defaultMessage;
92
+ }
93
+ }
90
94
  }
91
95
  }
92
-
93
- // 5. Shipped en catalog — always present
94
- const enEntry = EN_CATALOG[key];
95
- if (enEntry !== undefined) {
96
- return enEntry.defaultMessage;
97
- }
98
-
99
- // 6. Nothing found
100
- return null;
96
+ return lookup;
101
97
  }
102
- export function resolve(key, values, locale, messages, overrides) {
103
- const result = lookup(key, locale, messages, overrides);
104
- if (result === null) {
105
- // Fires ONLY when a key is missing from every source including the
106
- // shipped `en` catalog — a real bug (typo, stale catalog, deleted key).
107
- // Fallback to `en` from a non-en locale is expected and stays silent,
108
- // matching the FormatJS / i18next default.
109
- warnOnce(`astryx-i18n:${locale}::${key}`, 'astryx-i18n', `missing key: ${key} (locale: ${locale})`);
110
- return key;
111
- }
112
- if (values === undefined) {
113
- // Static string — skip the parser entirely for the common case
114
- return result;
115
- }
116
- const formatted = getFormatter(result, locale).format(values);
117
- // IntlMessageFormat.format returns string | (string | React elements) — we
118
- // only ever pass string values so it will be a string; assert for the type
119
- // system.
120
- return formatted;
98
+ export function getResolve(locale, messages, overrides) {
99
+ const lookup = getLookup(locale, messages, overrides);
100
+ return (key, values) => {
101
+ const result = lookup[key] ?? EN_CATALOG[key]?.defaultMessage;
102
+ if (result === undefined) {
103
+ // Fires ONLY when a key is missing from every source including the
104
+ // shipped `en` catalog — a real bug (typo, stale catalog, deleted key).
105
+ // Fallback to `en` from a non-en locale is expected and stays silent,
106
+ // matching the FormatJS / i18next default.
107
+ warnOnce(`astryx-i18n:${locale}::${key}`, 'astryx-i18n', `missing key: ${key} (locale: ${locale})`);
108
+ return key;
109
+ }
110
+ if (values === undefined) {
111
+ // Static string — skip the parser entirely for the common case
112
+ return result;
113
+ }
114
+ const formatted = getFormatter(result, locale).format(values);
115
+ // IntlMessageFormat.format returns string | (string | React elements) — we
116
+ // only ever pass string values so it will be a string; assert for the type
117
+ // system.
118
+ return formatted;
119
+ };
121
120
  }
122
121
 
123
122
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"useTranslator.d.ts","sourceRoot":"","sources":["../../src/i18n/useTranslator.ts"],"names":[],"mappings":"AAyBA,MAAM,MAAM,YAAY,GAAG,CACzB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC7B,MAAM,CAAC;AAEZ;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,IAAI,YAAY,CAO5C"}
1
+ {"version":3,"file":"useTranslator.d.ts","sourceRoot":"","sources":["../../src/i18n/useTranslator.ts"],"names":[],"mappings":"AAwBA,MAAM,MAAM,YAAY,GAAG,CACzB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC7B,MAAM,CAAC;AAEZ;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,IAAI,YAAY,CAG5C"}
@@ -18,9 +18,8 @@
18
18
  * - /packages/core/src/i18n/index.ts
19
19
  * - /packages/core/src/i18n/t.client.ts
20
20
  */
21
- import { use, useCallback } from 'react';
21
+ import { use } from 'react';
22
22
  import { InternationalizationContext } from "./InternationalizationContext.js";
23
- import { resolve } from "./resolve.js";
24
23
  /**
25
24
  * Returns a translator function bound to the current provider's locale.
26
25
  * Safe to call from event handlers and effects.
@@ -35,5 +34,5 @@ import { resolve } from "./resolve.js";
35
34
  */
36
35
  export function useTranslator() {
37
36
  const ctx = use(InternationalizationContext);
38
- return useCallback((key, values) => resolve(key, values, ctx.locale, ctx.messages, ctx.overrides), [ctx.locale, ctx.messages, ctx.overrides]);
37
+ return ctx.translate;
39
38
  }
@@ -623,10 +623,6 @@
623
623
  "defaultMessage": "Kies 'n datum",
624
624
  "description": "Grey placeholder inside an empty DateTimeInput field. Note: the English says only \"date\" though the field is date+time — do not add \"and time\" unless natural."
625
625
  },
626
- "@astryx.dateTimeInput.dialogLabel": {
627
- "defaultMessage": "Kies datum",
628
- "description": "Screen-reader-only accessible name for the DateTimeInput picker dialog. Same English-only \"date\" phrasing as the placeholder — don't add \"and time\" unless natural."
629
- },
630
626
  "@astryx.link.newTab": {
631
627
  "defaultMessage": "(maak in nuwe oortjie oop)",
632
628
  "description": "Visually-hidden (screen-reader-only) suffix appended to a Link's accessible name when it opens in a new tab. Example: `Documentation (opens in new tab)`."
@@ -651,10 +651,6 @@
651
651
  "defaultMessage": "حدّد تاريخًا",
652
652
  "description": "Grey placeholder inside an empty DateTimeInput field. Note: the English says only \"date\" though the field is date+time — do not add \"and time\" unless natural."
653
653
  },
654
- "@astryx.dateTimeInput.dialogLabel": {
655
- "defaultMessage": "اختر التاريخ",
656
- "description": "Screen-reader-only accessible name for the DateTimeInput picker dialog. Same English-only \"date\" phrasing as the placeholder — don't add \"and time\" unless natural."
657
- },
658
654
  "@astryx.link.newTab": {
659
655
  "defaultMessage": "(يُفتح في علامة تبويب جديدة)",
660
656
  "description": "Visually-hidden (screen-reader-only) suffix appended to a Link's accessible name when it opens in a new tab. Example: `Documentation (opens in new tab)`."
@@ -643,10 +643,6 @@
643
643
  "defaultMessage": "Selecciona una data",
644
644
  "description": "Grey placeholder inside an empty DateTimeInput field. Note: the English says only \"date\" though the field is date+time — do not add \"and time\" unless natural."
645
645
  },
646
- "@astryx.dateTimeInput.dialogLabel": {
647
- "defaultMessage": "Tria una data",
648
- "description": "Screen-reader-only accessible name for the DateTimeInput picker dialog. Same English-only \"date\" phrasing as the placeholder — don't add \"and time\" unless natural."
649
- },
650
646
  "@astryx.link.newTab": {
651
647
  "defaultMessage": "(s’obre en una pestanya nova)",
652
648
  "description": "Visually-hidden (screen-reader-only) suffix appended to a Link's accessible name when it opens in a new tab. Example: `Documentation (opens in new tab)`."
@@ -647,10 +647,6 @@
647
647
  "defaultMessage": "Vyberte datum",
648
648
  "description": "Grey placeholder inside an empty DateTimeInput field. Note: the English says only \"date\" though the field is date+time — do not add \"and time\" unless natural."
649
649
  },
650
- "@astryx.dateTimeInput.dialogLabel": {
651
- "defaultMessage": "Zvolit datum",
652
- "description": "Screen-reader-only accessible name for the DateTimeInput picker dialog. Same English-only \"date\" phrasing as the placeholder — don't add \"and time\" unless natural."
653
- },
654
650
  "@astryx.link.newTab": {
655
651
  "defaultMessage": "(otevře se na nové kartě)",
656
652
  "description": "Visually-hidden (screen-reader-only) suffix appended to a Link's accessible name when it opens in a new tab. Example: `Documentation (opens in new tab)`."
@@ -639,10 +639,6 @@
639
639
  "defaultMessage": "Vælg en dato",
640
640
  "description": "Grey placeholder inside an empty DateTimeInput field. Note: the English says only \"date\" though the field is date+time — do not add \"and time\" unless natural."
641
641
  },
642
- "@astryx.dateTimeInput.dialogLabel": {
643
- "defaultMessage": "Vælg dato",
644
- "description": "Screen-reader-only accessible name for the DateTimeInput picker dialog. Same English-only \"date\" phrasing as the placeholder — don't add \"and time\" unless natural."
645
- },
646
642
  "@astryx.link.newTab": {
647
643
  "defaultMessage": "(åbner i ny fane)",
648
644
  "description": "Visually-hidden (screen-reader-only) suffix appended to a Link's accessible name when it opens in a new tab. Example: `Documentation (opens in new tab)`."