panelui-native 0.62.0 → 0.65.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/README.md +5 -0
- package/lib/module/components/area-chart/index.js +14 -4
- package/lib/module/components/area-chart/index.js.map +1 -1
- package/lib/module/components/badge/index.js +1 -1
- package/lib/module/components/badge/index.js.map +1 -1
- package/lib/module/components/bar-chart/index.js +53 -6
- package/lib/module/components/bar-chart/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js +2 -2
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button/index.js +19 -9
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/candlestick-chart/index.js +14 -4
- package/lib/module/components/candlestick-chart/index.js.map +1 -1
- package/lib/module/components/card/index.js +26 -1
- package/lib/module/components/card/index.js.map +1 -1
- package/lib/module/components/carousel/index.js +28 -14
- package/lib/module/components/carousel/index.js.map +1 -1
- package/lib/module/components/checkbox/index.js +2 -3
- package/lib/module/components/checkbox/index.js.map +1 -1
- package/lib/module/components/dialog/index.js +51 -5
- package/lib/module/components/dialog/index.js.map +1 -1
- package/lib/module/components/drawer/index.js +2 -2
- package/lib/module/components/drawer/index.js.map +1 -1
- package/lib/module/components/fab/index.js +4 -3
- package/lib/module/components/fab/index.js.map +1 -1
- package/lib/module/components/field/index.js +41 -6
- package/lib/module/components/field/index.js.map +1 -1
- package/lib/module/components/flow/flow-accessibility.js +84 -0
- package/lib/module/components/flow/flow-accessibility.js.map +1 -0
- package/lib/module/components/flow/index.js +100 -39
- package/lib/module/components/flow/index.js.map +1 -1
- package/lib/module/components/form/use-form.js +93 -66
- package/lib/module/components/form/use-form.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +2 -2
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/hex-chart/index.js +4 -4
- package/lib/module/components/hex-chart/index.js.map +1 -1
- package/lib/module/components/kpi/index.js +3 -1
- package/lib/module/components/kpi/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +17 -7
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/map/index.js +71 -8
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/map/map-accessibility.js +20 -0
- package/lib/module/components/map/map-accessibility.js.map +1 -0
- package/lib/module/components/map/maplibre.js.map +1 -1
- package/lib/module/components/menu/index.js +4 -1
- package/lib/module/components/menu/index.js.map +1 -1
- package/lib/module/components/number-input/index.js +1 -23
- package/lib/module/components/number-input/index.js.map +1 -1
- package/lib/module/components/number-input/number-input-math.js +44 -0
- package/lib/module/components/number-input/number-input-math.js.map +1 -0
- package/lib/module/components/pie-chart/index.js +2 -2
- package/lib/module/components/pie-chart/index.js.map +1 -1
- package/lib/module/components/plot/index.js +1329 -0
- package/lib/module/components/plot/index.js.map +1 -0
- package/lib/module/components/polar-area-chart/index.js +2 -2
- package/lib/module/components/polar-area-chart/index.js.map +1 -1
- package/lib/module/components/radar-chart/index.js +14 -4
- package/lib/module/components/radar-chart/index.js.map +1 -1
- package/lib/module/components/radio-group/index.js +1 -2
- package/lib/module/components/radio-group/index.js.map +1 -1
- package/lib/module/components/ring-chart/index.js +2 -2
- package/lib/module/components/ring-chart/index.js.map +1 -1
- package/lib/module/components/section-rail/index.js +120 -15
- package/lib/module/components/section-rail/index.js.map +1 -1
- package/lib/module/components/selection-mode/index.js +129 -18
- package/lib/module/components/selection-mode/index.js.map +1 -1
- package/lib/module/components/sortable/index.js +20 -60
- package/lib/module/components/sortable/index.js.map +1 -1
- package/lib/module/components/sortable/reorder.js +63 -0
- package/lib/module/components/sortable/reorder.js.map +1 -0
- package/lib/module/components/swipe/index.js +4 -4
- package/lib/module/components/swipe/index.js.map +1 -1
- package/lib/module/components/switch/index.js +36 -3
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/time-picker/accessibility.js +21 -0
- package/lib/module/components/time-picker/accessibility.js.map +1 -0
- package/lib/module/components/time-picker/index.js +34 -8
- package/lib/module/components/time-picker/index.js.map +1 -1
- package/lib/module/components/waterfall-chart/index.js +1325 -0
- package/lib/module/components/waterfall-chart/index.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/modal-isolation-store.js +29 -0
- package/lib/module/primitives/modal-isolation-store.js.map +1 -0
- package/lib/module/primitives/portal.js +36 -4
- package/lib/module/primitives/portal.js.map +1 -1
- package/lib/module/providers/panel-ui-provider.js +27 -6
- package/lib/module/providers/panel-ui-provider.js.map +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/badge/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/candlestick-chart/index.d.ts +6 -3
- package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/card/index.d.ts +48 -1
- package/lib/typescript/src/components/card/index.d.ts.map +1 -1
- package/lib/typescript/src/components/carousel/index.d.ts +1 -1
- package/lib/typescript/src/components/carousel/index.d.ts.map +1 -1
- package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
- package/lib/typescript/src/components/dialog/index.d.ts +47 -2
- package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
- package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
- package/lib/typescript/src/components/field/index.d.ts +8 -1
- package/lib/typescript/src/components/field/index.d.ts.map +1 -1
- package/lib/typescript/src/components/flow/flow-accessibility.d.ts +49 -0
- package/lib/typescript/src/components/flow/flow-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/flow/index.d.ts +14 -2
- package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
- package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/kpi/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/index.d.ts +13 -5
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/map-accessibility.d.ts +19 -0
- package/lib/typescript/src/components/map/map-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/map/maplibre.d.ts +2 -0
- package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -1
- package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
- package/lib/typescript/src/components/number-input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/number-input/number-input-math.d.ts +9 -0
- package/lib/typescript/src/components/number-input/number-input-math.d.ts.map +1 -0
- package/lib/typescript/src/components/plot/index.d.ts +471 -0
- package/lib/typescript/src/components/plot/index.d.ts.map +1 -0
- package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/section-rail/index.d.ts +8 -1
- package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -1
- package/lib/typescript/src/components/selection-mode/index.d.ts +82 -6
- package/lib/typescript/src/components/selection-mode/index.d.ts.map +1 -1
- package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
- package/lib/typescript/src/components/sortable/reorder.d.ts +14 -0
- package/lib/typescript/src/components/sortable/reorder.d.ts.map +1 -0
- package/lib/typescript/src/components/swipe/index.d.ts.map +1 -1
- package/lib/typescript/src/components/switch/index.d.ts +3 -3
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/accessibility.d.ts +10 -0
- package/lib/typescript/src/components/time-picker/accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/waterfall-chart/index.d.ts +379 -0
- package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/modal-isolation-store.d.ts +10 -0
- package/lib/typescript/src/primitives/modal-isolation-store.d.ts.map +1 -0
- package/lib/typescript/src/primitives/portal.d.ts +6 -0
- package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
- package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/components/area-chart/index.tsx +14 -4
- package/src/components/badge/index.tsx +4 -1
- package/src/components/bar-chart/index.tsx +54 -7
- package/src/components/bottom-sheet/index.tsx +3 -3
- package/src/components/button/index.tsx +15 -21
- package/src/components/candlestick-chart/index.tsx +20 -7
- package/src/components/card/index.tsx +32 -7
- package/src/components/carousel/index.tsx +42 -20
- package/src/components/checkbox/index.tsx +1 -2
- package/src/components/dialog/index.tsx +58 -5
- package/src/components/drawer/index.tsx +3 -3
- package/src/components/fab/index.tsx +8 -12
- package/src/components/field/index.tsx +52 -10
- package/src/components/flow/flow-accessibility.ts +109 -0
- package/src/components/flow/index.tsx +162 -50
- package/src/components/form/use-form.ts +101 -65
- package/src/components/heatmap-chart/index.tsx +2 -3
- package/src/components/hex-chart/index.tsx +4 -5
- package/src/components/kpi/index.tsx +3 -1
- package/src/components/line-chart/index.tsx +17 -6
- package/src/components/map/index.tsx +110 -7
- package/src/components/map/map-accessibility.ts +41 -0
- package/src/components/map/maplibre.ts +2 -0
- package/src/components/menu/index.tsx +4 -1
- package/src/components/number-input/index.tsx +1 -26
- package/src/components/number-input/number-input-math.ts +50 -0
- package/src/components/pie-chart/index.tsx +2 -2
- package/src/components/plot/index.tsx +1565 -0
- package/src/components/polar-area-chart/index.tsx +2 -2
- package/src/components/radar-chart/index.tsx +14 -4
- package/src/components/radio-group/index.tsx +1 -2
- package/src/components/ring-chart/index.tsx +2 -2
- package/src/components/section-rail/index.tsx +133 -14
- package/src/components/selection-mode/index.tsx +163 -26
- package/src/components/sortable/index.tsx +27 -69
- package/src/components/sortable/reorder.ts +88 -0
- package/src/components/swipe/index.tsx +7 -4
- package/src/components/switch/index.tsx +42 -6
- package/src/components/time-picker/accessibility.ts +24 -0
- package/src/components/time-picker/index.tsx +48 -2
- package/src/components/waterfall-chart/index.tsx +1589 -0
- package/src/index.ts +69 -0
- package/src/primitives/modal-isolation-store.ts +31 -0
- package/src/primitives/portal.tsx +34 -4
- package/src/providers/panel-ui-provider.tsx +41 -9
- package/theme.css +54 -3
|
@@ -174,6 +174,8 @@ export function useForm<T extends Record<string, any>>({
|
|
|
174
174
|
const validatorsRef = useRef<
|
|
175
175
|
Partial<Record<keyof T, (value: any, values: T) => string | undefined | Promise<string | undefined>>>
|
|
176
176
|
>({});
|
|
177
|
+
const validationOwnersRef = useRef<Partial<Record<keyof T, symbol>>>({});
|
|
178
|
+
const submissionLockedRef = useRef(false);
|
|
177
179
|
|
|
178
180
|
const [state, dispatch] = useReducer(reducer<T>, {
|
|
179
181
|
values: defaultValues,
|
|
@@ -196,31 +198,46 @@ export function useForm<T extends Record<string, any>>({
|
|
|
196
198
|
const valuesRef = useRef(state.values);
|
|
197
199
|
valuesRef.current = state.values;
|
|
198
200
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
201
|
+
/* Only the latest owner of a field may commit validation state. A value,
|
|
202
|
+
* validator or manual-error change claims ownership too, invalidating any
|
|
203
|
+
* result that was calculated from the state before that change. */
|
|
204
|
+
const claimFieldValidation = useCallback(<K extends keyof T>(name: K) => {
|
|
205
|
+
const owner = Symbol();
|
|
206
|
+
validationOwnersRef.current[name] = owner;
|
|
207
|
+
return owner;
|
|
202
208
|
}, []);
|
|
203
209
|
|
|
210
|
+
const setFieldValue = useCallback(
|
|
211
|
+
<K extends keyof T>(name: K, value: T[K]) => {
|
|
212
|
+
claimFieldValidation(name);
|
|
213
|
+
valuesRef.current = { ...valuesRef.current, [name]: value } as T;
|
|
214
|
+
dispatch({ type: 'SET_VALUE', name, value });
|
|
215
|
+
},
|
|
216
|
+
[claimFieldValidation]
|
|
217
|
+
);
|
|
218
|
+
|
|
204
219
|
const setFieldTouched = useCallback(<K extends keyof T>(name: K, touched = true) => {
|
|
205
220
|
dispatch({ type: 'SET_TOUCHED', name, touched });
|
|
206
221
|
}, []);
|
|
207
222
|
|
|
208
223
|
const setFieldError = useCallback(
|
|
209
224
|
<K extends keyof T>(name: K, error: string | undefined) => {
|
|
225
|
+
claimFieldValidation(name);
|
|
210
226
|
dispatch({ type: 'SET_ERROR', name, error });
|
|
211
227
|
},
|
|
212
|
-
[]
|
|
228
|
+
[claimFieldValidation]
|
|
213
229
|
);
|
|
214
230
|
|
|
215
231
|
const registerValidator = useCallback(
|
|
216
232
|
<K extends keyof T>(name: K, validator?: Validator<T, K>) => {
|
|
233
|
+
claimFieldValidation(name);
|
|
217
234
|
if (validator) {
|
|
218
235
|
validatorsRef.current[name] = validator;
|
|
219
236
|
} else {
|
|
220
237
|
delete validatorsRef.current[name];
|
|
221
238
|
}
|
|
222
239
|
},
|
|
223
|
-
[]
|
|
240
|
+
[claimFieldValidation]
|
|
224
241
|
);
|
|
225
242
|
|
|
226
243
|
const getFieldState = useCallback(
|
|
@@ -235,81 +252,100 @@ export function useForm<T extends Record<string, any>>({
|
|
|
235
252
|
const validateField = useCallback(async <K extends keyof T>(name: K) => {
|
|
236
253
|
const validator = validatorsRef.current[name];
|
|
237
254
|
if (!validator) return true;
|
|
255
|
+
const owner = claimFieldValidation(name);
|
|
238
256
|
const values = valuesRef.current;
|
|
239
257
|
const error = await runValidator(name, validator, values);
|
|
240
|
-
|
|
258
|
+
if (validationOwnersRef.current[name] === owner) {
|
|
259
|
+
dispatch({ type: 'SET_ERROR', name, error });
|
|
260
|
+
}
|
|
241
261
|
return !error;
|
|
242
|
-
}, []);
|
|
262
|
+
}, [claimFieldValidation]);
|
|
243
263
|
|
|
244
264
|
const handleSubmit = useCallback(async () => {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
265
|
+
// Reducer state cannot guard two calls made before React renders again.
|
|
266
|
+
if (submissionLockedRef.current) return;
|
|
267
|
+
submissionLockedRef.current = true;
|
|
268
|
+
try {
|
|
269
|
+
const values = valuesRef.current;
|
|
270
|
+
/*
|
|
271
|
+
* Every field the form knows of, which is not the same as every key of
|
|
272
|
+
* `defaultValues`: a field can register a validator under a name that was
|
|
273
|
+
* never declared there. Submitting only the declared ones meant such a
|
|
274
|
+
* field was never validated and never blocked anything, so a form of
|
|
275
|
+
* required-but-undeclared fields submitted itself while still empty.
|
|
276
|
+
*
|
|
277
|
+
* It is still a mistake to name a field that `defaultValues` does not
|
|
278
|
+
* declare — its value stays undefined and never reaches `onSubmit` —
|
|
279
|
+
* which is why `useField` says so. Validating it anyway is what turns
|
|
280
|
+
* that mistake into a visible error instead of a silent submit.
|
|
281
|
+
*/
|
|
282
|
+
const names = Array.from(
|
|
283
|
+
new Set([...Object.keys(values), ...Object.keys(validatorsRef.current)])
|
|
284
|
+
) as (keyof T)[];
|
|
285
|
+
const validationOwners = new Map(
|
|
286
|
+
names.map((name) => [name, claimFieldValidation(name)] as const)
|
|
287
|
+
);
|
|
288
|
+
dispatch({ type: 'TOUCH_ALL', names });
|
|
262
289
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
290
|
+
const fieldErrorEntries = await Promise.all(
|
|
291
|
+
names.map(async (name) => {
|
|
292
|
+
const validator = validatorsRef.current[name];
|
|
293
|
+
const error = validator
|
|
294
|
+
? await runValidator(name, validator, values)
|
|
295
|
+
: undefined;
|
|
296
|
+
return [name, error] as const;
|
|
297
|
+
})
|
|
298
|
+
);
|
|
272
299
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
300
|
+
/*
|
|
301
|
+
* Same reasoning as a field's rule, one level up: a cross-field check that
|
|
302
|
+
* throws must not take the screen, and must not let the submit through.
|
|
303
|
+
* It belongs to no single field, so it blocks without marking one — there
|
|
304
|
+
* is no field whose error line would be the honest place to put it.
|
|
305
|
+
*/
|
|
306
|
+
let formErrors: FieldErrors<T> = {};
|
|
307
|
+
let formValidateThrew = false;
|
|
308
|
+
if (validate) {
|
|
309
|
+
try {
|
|
310
|
+
formErrors = await validate(values);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error(
|
|
313
|
+
'[PanelUI] The form-level validate() threw, so the form is being ' +
|
|
314
|
+
'treated as invalid and the submit was not run.',
|
|
315
|
+
error
|
|
316
|
+
);
|
|
317
|
+
formValidateThrew = true;
|
|
318
|
+
}
|
|
291
319
|
}
|
|
292
|
-
}
|
|
293
320
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
321
|
+
const cleared = Object.fromEntries(names.map((name) => [name, undefined]));
|
|
322
|
+
const nextErrors: FieldErrors<T> = {
|
|
323
|
+
...cleared,
|
|
324
|
+
...Object.fromEntries(fieldErrorEntries.filter(([, error]) => error !== undefined)),
|
|
325
|
+
...formErrors,
|
|
326
|
+
};
|
|
327
|
+
const ownedErrors = Object.fromEntries(
|
|
328
|
+
names
|
|
329
|
+
.filter((name) => validationOwnersRef.current[name] === validationOwners.get(name))
|
|
330
|
+
.map((name) => [name, nextErrors[name]])
|
|
331
|
+
) as FieldErrors<T>;
|
|
332
|
+
dispatch({ type: 'SET_ERRORS', errors: ownedErrors });
|
|
301
333
|
|
|
302
|
-
|
|
334
|
+
if (formValidateThrew || Object.values(nextErrors).some(Boolean)) return;
|
|
303
335
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
336
|
+
dispatch({ type: 'SUBMIT_START' });
|
|
337
|
+
try {
|
|
338
|
+
await onSubmit(values);
|
|
339
|
+
} finally {
|
|
340
|
+
dispatch({ type: 'SUBMIT_END' });
|
|
341
|
+
}
|
|
307
342
|
} finally {
|
|
308
|
-
|
|
343
|
+
submissionLockedRef.current = false;
|
|
309
344
|
}
|
|
310
|
-
}, [validate, onSubmit]);
|
|
345
|
+
}, [claimFieldValidation, validate, onSubmit]);
|
|
311
346
|
|
|
312
347
|
const reset = useCallback((values: T = defaultsRef.current) => {
|
|
348
|
+
validationOwnersRef.current = {};
|
|
313
349
|
valuesRef.current = values;
|
|
314
350
|
dispatch({ type: 'RESET', values });
|
|
315
351
|
}, []);
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
isValidElement,
|
|
43
43
|
useContext,
|
|
44
44
|
useEffect,
|
|
45
|
+
useId,
|
|
45
46
|
useMemo,
|
|
46
47
|
useRef,
|
|
47
48
|
useState,
|
|
@@ -402,9 +403,7 @@ const HeatmapChartRoot = forwardRef<View, HeatmapChartProps>(function HeatmapCha
|
|
|
402
403
|
) {
|
|
403
404
|
const [width, setWidth] = useState(0);
|
|
404
405
|
const [activeCell, setActiveCellState] = useState<HeatmapCell | null>(null);
|
|
405
|
-
const clipId =
|
|
406
|
-
`panelui-heat-${Math.random().toString(36).slice(2, 9)}`
|
|
407
|
-
).current;
|
|
406
|
+
const clipId = `panelui-heat-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
408
407
|
|
|
409
408
|
const reveal = useSharedValue(0);
|
|
410
409
|
const reducedMotion = useReducedMotion();
|
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
isValidElement,
|
|
74
74
|
useContext,
|
|
75
75
|
useEffect,
|
|
76
|
+
useId,
|
|
76
77
|
useImperativeHandle,
|
|
77
78
|
useMemo,
|
|
78
79
|
useRef,
|
|
@@ -269,7 +270,7 @@ const HexChartRoot = forwardRef<HexChartHandle, HexChartProps>(function HexChart
|
|
|
269
270
|
density = 0.55,
|
|
270
271
|
shape = 'blob',
|
|
271
272
|
cellGap = 0.14,
|
|
272
|
-
animationDuration =
|
|
273
|
+
animationDuration = 620,
|
|
273
274
|
status = 'ready',
|
|
274
275
|
activeIndex: activeIndexProp,
|
|
275
276
|
onActiveIndexChange,
|
|
@@ -282,9 +283,7 @@ const HexChartRoot = forwardRef<HexChartHandle, HexChartProps>(function HexChart
|
|
|
282
283
|
const [internalActive, setInternalActive] = useState(-1);
|
|
283
284
|
const reveal = useSharedValue(0);
|
|
284
285
|
const reducedMotion = useReducedMotion();
|
|
285
|
-
const clipId =
|
|
286
|
-
`panelui-hex-${Math.random().toString(36).slice(2, 9)}`
|
|
287
|
-
).current;
|
|
286
|
+
const clipId = `panelui-hex-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
288
287
|
|
|
289
288
|
const controlled = activeIndexProp !== undefined;
|
|
290
289
|
const activeIndex = controlled ? activeIndexProp : internalActive;
|
|
@@ -413,7 +412,7 @@ const HexChartRoot = forwardRef<HexChartHandle, HexChartProps>(function HexChart
|
|
|
413
412
|
reveal.value = 0;
|
|
414
413
|
reveal.value = withTiming(1, {
|
|
415
414
|
duration: animationDuration,
|
|
416
|
-
easing: Easing.
|
|
415
|
+
easing: Easing.out(Easing.cubic),
|
|
417
416
|
});
|
|
418
417
|
},
|
|
419
418
|
[reducedMotion, animationDuration, reveal]
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { createContext, forwardRef, useContext, useMemo, type ReactNode } from 'react';
|
|
30
30
|
import { View, type ViewProps } from 'react-native';
|
|
31
|
-
import
|
|
31
|
+
import ArrowDown from 'lucide-react-native/icons/arrow-down';
|
|
32
|
+
import ArrowUp from 'lucide-react-native/icons/arrow-up';
|
|
33
|
+
import Minus from 'lucide-react-native/icons/minus';
|
|
32
34
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
33
35
|
import { useCSSVariable } from 'uniwind';
|
|
34
36
|
import { Text } from '../../primitives/text';
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
isValidElement,
|
|
49
49
|
useContext,
|
|
50
50
|
useEffect,
|
|
51
|
+
useId,
|
|
51
52
|
useImperativeHandle,
|
|
52
53
|
useMemo,
|
|
53
54
|
useRef,
|
|
@@ -221,7 +222,7 @@ const LineChartRoot = forwardRef<LineChartHandle, LineChartProps>(function LineC
|
|
|
221
222
|
xDataKey = 'date',
|
|
222
223
|
status = 'ready',
|
|
223
224
|
aspectRatio = 2,
|
|
224
|
-
animationDuration =
|
|
225
|
+
animationDuration = 700,
|
|
225
226
|
domainDuration = 500,
|
|
226
227
|
yDomain,
|
|
227
228
|
curve = 'monotone',
|
|
@@ -235,7 +236,7 @@ const LineChartRoot = forwardRef<LineChartHandle, LineChartProps>(function LineC
|
|
|
235
236
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
236
237
|
const [series, setSeries] = useState<[string, string][]>([]);
|
|
237
238
|
const [activeIndexJS, setActiveIndexJS] = useState(-1);
|
|
238
|
-
const clipId =
|
|
239
|
+
const clipId = `panelui-clip-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
239
240
|
|
|
240
241
|
const reveal = useSharedValue(0);
|
|
241
242
|
const domainMin = useSharedValue(0);
|
|
@@ -341,17 +342,27 @@ const LineChartRoot = forwardRef<LineChartHandle, LineChartProps>(function LineC
|
|
|
341
342
|
reveal.value = 0;
|
|
342
343
|
reveal.value = withTiming(1, {
|
|
343
344
|
duration: animationDuration,
|
|
344
|
-
easing: Easing.
|
|
345
|
+
easing: Easing.out(Easing.cubic),
|
|
345
346
|
});
|
|
346
347
|
},
|
|
347
348
|
[reducedMotion, animationDuration, reveal]
|
|
348
349
|
);
|
|
349
350
|
|
|
350
351
|
useEffect(() => {
|
|
351
|
-
|
|
352
|
+
/*
|
|
353
|
+
* Going back to `loading` arms the reveal again. Without this a chart that
|
|
354
|
+
* is refetched comes back fully drawn on the frame the data lands, which
|
|
355
|
+
* reads as the loading state having been for nothing.
|
|
356
|
+
*/
|
|
357
|
+
if (loading) {
|
|
358
|
+
revealed.current = false;
|
|
359
|
+
reveal.value = 0;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (revealed.current || plot.width <= 0 || !data.length) return;
|
|
352
363
|
revealed.current = true;
|
|
353
364
|
playReveal();
|
|
354
|
-
}, [loading, plot.width, data.length, playReveal]);
|
|
365
|
+
}, [loading, plot.width, data.length, playReveal, reveal]);
|
|
355
366
|
|
|
356
367
|
// `replay()` re-runs the reveal — for a control the caller wires up.
|
|
357
368
|
useImperativeHandle(ref, () => ({ replay: playReveal }), [playReveal]);
|
|
@@ -647,7 +658,7 @@ export interface LineChartAreaProps {
|
|
|
647
658
|
function LineChartArea({ dataKey, color, colorIndex = 1, opacity = 0.18 }: LineChartAreaProps) {
|
|
648
659
|
const { data, plot, domainMin, domainMax, curve, status, clipId } = useChart('LineChart.Area');
|
|
649
660
|
const fill = useSeriesColor(color, colorIndex);
|
|
650
|
-
const gradientId =
|
|
661
|
+
const gradientId = `panelui-area-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
651
662
|
|
|
652
663
|
const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
|
|
653
664
|
const loading = status === 'loading';
|
|
@@ -36,13 +36,14 @@ import {
|
|
|
36
36
|
isValidElement,
|
|
37
37
|
useCallback,
|
|
38
38
|
useContext,
|
|
39
|
+
useEffect,
|
|
39
40
|
useImperativeHandle,
|
|
40
41
|
useMemo,
|
|
41
42
|
useRef,
|
|
42
43
|
useState,
|
|
43
44
|
type ReactNode,
|
|
44
45
|
} from 'react';
|
|
45
|
-
import { Pressable, View, type ViewProps } from 'react-native';
|
|
46
|
+
import { Pressable, View, type PressableProps, type ViewProps } from 'react-native';
|
|
46
47
|
import { useCSSVariable } from 'uniwind';
|
|
47
48
|
import { tv } from 'tailwind-variants';
|
|
48
49
|
import { CompassIcon, CrosshairIcon, MinusIcon, PlusIcon } from '../../icons';
|
|
@@ -64,9 +65,15 @@ import {
|
|
|
64
65
|
type StyleSpecification,
|
|
65
66
|
type ViewState,
|
|
66
67
|
} from './maplibre';
|
|
68
|
+
import {
|
|
69
|
+
describeMapFeatures,
|
|
70
|
+
type MapFeatureAccessibility,
|
|
71
|
+
type MapFeatureAccessibilityDescription,
|
|
72
|
+
} from './map-accessibility';
|
|
67
73
|
|
|
68
74
|
export { hasMapLibre, CARTO_SOURCE };
|
|
69
75
|
export type { BasemapSource, BasemapTokens, LngLat, LngLatBounds, ViewState };
|
|
76
|
+
export type { MapFeatureAccessibility } from './map-accessibility';
|
|
70
77
|
|
|
71
78
|
const mapVariants = tv({
|
|
72
79
|
slots: {
|
|
@@ -172,7 +179,16 @@ interface MapContextValue {
|
|
|
172
179
|
ready: boolean;
|
|
173
180
|
}
|
|
174
181
|
|
|
175
|
-
|
|
182
|
+
interface AccessibleFeatureGroup {
|
|
183
|
+
features: MapFeatureAccessibilityDescription[];
|
|
184
|
+
onPress?: (feature: unknown) => void;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
interface InternalMapContextValue extends MapContextValue {
|
|
188
|
+
registerAccessibleFeatures(id: string, group: AccessibleFeatureGroup | null): void;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const MapContext = createContext<InternalMapContextValue | null>(null);
|
|
176
192
|
|
|
177
193
|
/**
|
|
178
194
|
* The map's camera and view state, from inside it.
|
|
@@ -293,6 +309,21 @@ const MapRoot = forwardRef<MapHandle, MapProps>(function MapRoot(
|
|
|
293
309
|
const mapRef = useRef<MapRef | null>(null);
|
|
294
310
|
const cameraRef = useRef<CameraRef | null>(null);
|
|
295
311
|
const [ready, setReady] = useState(false);
|
|
312
|
+
const [accessibleFeatureGroups, setAccessibleFeatureGroups] = useState(
|
|
313
|
+
() => new globalThis.Map<string, AccessibleFeatureGroup>()
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const registerAccessibleFeatures = useCallback(
|
|
317
|
+
(id: string, group: AccessibleFeatureGroup | null) => {
|
|
318
|
+
setAccessibleFeatureGroups((current) => {
|
|
319
|
+
const next = new globalThis.Map(current);
|
|
320
|
+
if (group?.features.length) next.set(id, group);
|
|
321
|
+
else next.delete(id);
|
|
322
|
+
return next;
|
|
323
|
+
});
|
|
324
|
+
},
|
|
325
|
+
[]
|
|
326
|
+
);
|
|
296
327
|
|
|
297
328
|
// Every colour the style needs, resolved from the active theme. Hooks run
|
|
298
329
|
// unconditionally, so they sit above the renderer check rather than inside it.
|
|
@@ -344,7 +375,10 @@ const MapRoot = forwardRef<MapHandle, MapProps>(function MapRoot(
|
|
|
344
375
|
[]
|
|
345
376
|
);
|
|
346
377
|
|
|
347
|
-
const context = useMemo(
|
|
378
|
+
const context = useMemo(
|
|
379
|
+
() => ({ mapRef, cameraRef, ready, registerAccessibleFeatures }),
|
|
380
|
+
[ready, registerAccessibleFeatures]
|
|
381
|
+
);
|
|
348
382
|
|
|
349
383
|
if (!hasMapLibre || !MapLibre) {
|
|
350
384
|
return <MapUnavailable className={className} {...props} />;
|
|
@@ -370,6 +404,8 @@ const MapRoot = forwardRef<MapHandle, MapProps>(function MapRoot(
|
|
|
370
404
|
touchZoom={interactive}
|
|
371
405
|
touchRotate={rotatable}
|
|
372
406
|
touchPitch={rotatable}
|
|
407
|
+
accessible={false}
|
|
408
|
+
importantForAccessibility="no"
|
|
373
409
|
onDidFinishLoadingMap={() => {
|
|
374
410
|
setReady(true);
|
|
375
411
|
onReady?.();
|
|
@@ -400,12 +436,41 @@ const MapRoot = forwardRef<MapHandle, MapProps>(function MapRoot(
|
|
|
400
436
|
{textChildren(children)}
|
|
401
437
|
</MapLibreMap>
|
|
402
438
|
</MapContext.Provider>
|
|
439
|
+
<View
|
|
440
|
+
accessibilityRole="list"
|
|
441
|
+
style={{ position: 'absolute', width: 1, height: 1, overflow: 'hidden' }}
|
|
442
|
+
>
|
|
443
|
+
{[...accessibleFeatureGroups].flatMap(([groupId, group]) =>
|
|
444
|
+
group.features.map(({ feature, label, hint, state }, index) =>
|
|
445
|
+
group.onPress ? (
|
|
446
|
+
<Pressable
|
|
447
|
+
key={`${groupId}-${index}`}
|
|
448
|
+
accessibilityRole="button"
|
|
449
|
+
accessibilityLabel={label}
|
|
450
|
+
accessibilityHint={hint}
|
|
451
|
+
accessibilityState={state}
|
|
452
|
+
onPress={() => group.onPress?.(feature)}
|
|
453
|
+
/>
|
|
454
|
+
) : (
|
|
455
|
+
<View
|
|
456
|
+
key={`${groupId}-${index}`}
|
|
457
|
+
accessible
|
|
458
|
+
accessibilityRole="text"
|
|
459
|
+
accessibilityLabel={label}
|
|
460
|
+
accessibilityHint={hint}
|
|
461
|
+
accessibilityState={state}
|
|
462
|
+
/>
|
|
463
|
+
)
|
|
464
|
+
)
|
|
465
|
+
)}
|
|
466
|
+
</View>
|
|
403
467
|
</View>
|
|
404
468
|
);
|
|
405
469
|
});
|
|
406
470
|
MapRoot.displayName = 'Map';
|
|
407
471
|
|
|
408
|
-
export interface MapMarkerProps
|
|
472
|
+
export interface MapMarkerProps
|
|
473
|
+
extends Omit<PressableProps, 'children' | 'onPress' | 'style'> {
|
|
409
474
|
/** Where the marker sits, `[longitude, latitude]`. */
|
|
410
475
|
lngLat: LngLat;
|
|
411
476
|
/**
|
|
@@ -415,6 +480,8 @@ export interface MapMarkerProps {
|
|
|
415
480
|
anchor?: 'center' | 'top' | 'bottom' | 'left' | 'right';
|
|
416
481
|
/** Pressing the marker. Adds a button role when given. */
|
|
417
482
|
onPress?: () => void;
|
|
483
|
+
/** Explicit spoken name. Other React Native accessibility props pass through too. */
|
|
484
|
+
accessibilityLabel?: string;
|
|
418
485
|
className?: string;
|
|
419
486
|
/** Defaults to a dot. Anything else replaces it. */
|
|
420
487
|
children?: ReactNode;
|
|
@@ -432,6 +499,8 @@ function MapMarker({
|
|
|
432
499
|
onPress,
|
|
433
500
|
className,
|
|
434
501
|
children,
|
|
502
|
+
accessibilityState,
|
|
503
|
+
...props
|
|
435
504
|
}: MapMarkerProps) {
|
|
436
505
|
const slots = mapVariants();
|
|
437
506
|
const [open, setOpen] = useState(false);
|
|
@@ -471,21 +540,28 @@ function MapMarker({
|
|
|
471
540
|
* the default pin keeps the box real, which is also the right drawing: a
|
|
472
541
|
* label with nothing to label is a caption floating on its own.
|
|
473
542
|
*/
|
|
543
|
+
const pressable = onPress || popups.length > 0;
|
|
474
544
|
const body = (
|
|
475
|
-
<View
|
|
545
|
+
<View
|
|
546
|
+
{...(!pressable ? props : {})}
|
|
547
|
+
accessibilityState={!pressable ? accessibilityState : undefined}
|
|
548
|
+
className={cn('items-center', className)}
|
|
549
|
+
>
|
|
476
550
|
{content.length > 0 ? content : <View className={slots.pin()} />}
|
|
477
551
|
{labels}
|
|
478
552
|
</View>
|
|
479
553
|
);
|
|
480
554
|
|
|
481
|
-
const pressable = onPress || popups.length > 0;
|
|
482
|
-
|
|
483
555
|
return (
|
|
484
556
|
<MarkerContext.Provider value={lngLat}>
|
|
485
557
|
<Marker lngLat={lngLat} anchor={anchor}>
|
|
486
558
|
{pressable ? (
|
|
487
559
|
<Pressable
|
|
560
|
+
{...props}
|
|
488
561
|
accessibilityRole="button"
|
|
562
|
+
accessibilityState={
|
|
563
|
+
popups.length > 0 ? { ...accessibilityState, expanded: open } : accessibilityState
|
|
564
|
+
}
|
|
489
565
|
onPress={() => {
|
|
490
566
|
if (popups.length > 0) setOpen((was) => !was);
|
|
491
567
|
onPress?.();
|
|
@@ -906,6 +982,8 @@ export interface MapGeoJSONProps {
|
|
|
906
982
|
fillOpacity?: number;
|
|
907
983
|
/** Fires with the pressed feature. */
|
|
908
984
|
onPress?: (feature: unknown) => void;
|
|
985
|
+
/** Describes each inline GeoJSON feature for the synchronized nonvisual list. */
|
|
986
|
+
accessibility?: (feature: unknown, index: number) => MapFeatureAccessibility;
|
|
909
987
|
id?: string;
|
|
910
988
|
}
|
|
911
989
|
|
|
@@ -924,10 +1002,12 @@ function MapGeoJSON({
|
|
|
924
1002
|
strokeWidth = 1,
|
|
925
1003
|
fillOpacity = 0.7,
|
|
926
1004
|
onPress,
|
|
1005
|
+
accessibility,
|
|
927
1006
|
id = 'geojson',
|
|
928
1007
|
}: MapGeoJSONProps) {
|
|
929
1008
|
const defaultFill = useToken('--color-primary', '#262626');
|
|
930
1009
|
const defaultStroke = useToken('--color-border', 'rgba(0,0,0,0.1)');
|
|
1010
|
+
useAccessibleMapFeatures(id, data, accessibility, onPress);
|
|
931
1011
|
|
|
932
1012
|
if (!MapLibre) return null;
|
|
933
1013
|
const { GeoJSONSource, Layer } = MapLibre;
|
|
@@ -976,6 +1056,8 @@ export interface MapClusterProps {
|
|
|
976
1056
|
maxZoom?: number;
|
|
977
1057
|
/** Fires with the pressed cluster or point. */
|
|
978
1058
|
onPress?: (feature: unknown) => void;
|
|
1059
|
+
/** Describes each source point for the synchronized nonvisual list. */
|
|
1060
|
+
accessibility?: (feature: unknown, index: number) => MapFeatureAccessibility;
|
|
979
1061
|
id?: string;
|
|
980
1062
|
}
|
|
981
1063
|
|
|
@@ -993,12 +1075,14 @@ function MapCluster({
|
|
|
993
1075
|
radius = 50,
|
|
994
1076
|
maxZoom = 14,
|
|
995
1077
|
onPress,
|
|
1078
|
+
accessibility,
|
|
996
1079
|
id = 'cluster',
|
|
997
1080
|
}: MapClusterProps) {
|
|
998
1081
|
const fill = useToken('--color-primary', '#262626');
|
|
999
1082
|
const onFill = useToken('--color-primary-foreground', '#fafafa');
|
|
1000
1083
|
const bubble = color ?? fill;
|
|
1001
1084
|
const ink = textColor ?? onFill;
|
|
1085
|
+
useAccessibleMapFeatures(id, data, accessibility, onPress);
|
|
1002
1086
|
|
|
1003
1087
|
if (!MapLibre) return null;
|
|
1004
1088
|
const { GeoJSONSource, Layer } = MapLibre;
|
|
@@ -1057,6 +1141,25 @@ function MapCluster({
|
|
|
1057
1141
|
}
|
|
1058
1142
|
MapCluster.displayName = 'Map.Cluster';
|
|
1059
1143
|
|
|
1144
|
+
function useAccessibleMapFeatures(
|
|
1145
|
+
id: string,
|
|
1146
|
+
data: unknown,
|
|
1147
|
+
accessibility: MapGeoJSONProps['accessibility'],
|
|
1148
|
+
onPress?: (feature: unknown) => void
|
|
1149
|
+
) {
|
|
1150
|
+
const registerAccessibleFeatures = useContext(MapContext)?.registerAccessibleFeatures;
|
|
1151
|
+
const features = useMemo(
|
|
1152
|
+
() => (accessibility ? describeMapFeatures(data, accessibility) : []),
|
|
1153
|
+
[accessibility, data]
|
|
1154
|
+
);
|
|
1155
|
+
|
|
1156
|
+
useEffect(() => {
|
|
1157
|
+
if (!accessibility || !registerAccessibleFeatures) return;
|
|
1158
|
+
registerAccessibleFeatures(id, { features, onPress });
|
|
1159
|
+
return () => registerAccessibleFeatures(id, null);
|
|
1160
|
+
}, [accessibility, features, id, onPress, registerAccessibleFeatures]);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1060
1163
|
export interface MapHeatmapProps {
|
|
1061
1164
|
/** Point features to spread. */
|
|
1062
1165
|
data: unknown;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AccessibilityState } from 'react-native';
|
|
2
|
+
|
|
3
|
+
/** The nonvisual description of one feature in a GeoJSON-backed layer. */
|
|
4
|
+
export interface MapFeatureAccessibility {
|
|
5
|
+
/** The feature's spoken name. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** What activating the feature does, when the layer has `onPress`. */
|
|
8
|
+
hint?: string;
|
|
9
|
+
/** Selection, disabled, expanded, checked, or busy state. */
|
|
10
|
+
state?: AccessibilityState;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type MapFeatureAccessibilityDescription = MapFeatureAccessibility & {
|
|
14
|
+
feature: unknown;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Describes the features in inline GeoJSON without maintaining a second list.
|
|
19
|
+
* URL sources cannot be read synchronously and remain decorative.
|
|
20
|
+
*/
|
|
21
|
+
export function describeMapFeatures(
|
|
22
|
+
data: unknown,
|
|
23
|
+
describe: (feature: unknown, index: number) => MapFeatureAccessibility
|
|
24
|
+
): MapFeatureAccessibilityDescription[] {
|
|
25
|
+
const record = isRecord(data) ? data : null;
|
|
26
|
+
const features =
|
|
27
|
+
record?.type === 'FeatureCollection' && Array.isArray(record.features)
|
|
28
|
+
? record.features
|
|
29
|
+
: record?.type === 'Feature'
|
|
30
|
+
? [data]
|
|
31
|
+
: [];
|
|
32
|
+
|
|
33
|
+
return features.map((feature, index) => ({
|
|
34
|
+
feature,
|
|
35
|
+
...describe(feature, index),
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
40
|
+
return typeof value === 'object' && value !== null;
|
|
41
|
+
}
|
|
@@ -119,6 +119,8 @@ interface MapLibreModule {
|
|
|
119
119
|
touchZoom?: boolean;
|
|
120
120
|
touchRotate?: boolean;
|
|
121
121
|
touchPitch?: boolean;
|
|
122
|
+
accessible?: boolean;
|
|
123
|
+
importantForAccessibility?: 'auto' | 'yes' | 'no' | 'no-hide-descendants';
|
|
122
124
|
onPress?: (event: NativeEvent<{ coordinates: LngLat }>) => void;
|
|
123
125
|
onDidFinishLoadingMap?: () => void;
|
|
124
126
|
onRegionDidChange?: (event: NativeEvent<ViewState>) => void;
|
|
@@ -57,7 +57,10 @@ import Animated, {
|
|
|
57
57
|
} from 'react-native-reanimated';
|
|
58
58
|
import { tv } from 'tailwind-variants';
|
|
59
59
|
import { useCSSVariable } from 'uniwind';
|
|
60
|
-
import
|
|
60
|
+
import Check from 'lucide-react-native/icons/check';
|
|
61
|
+
import ChevronLeft from 'lucide-react-native/icons/chevron-left';
|
|
62
|
+
import ChevronRight from 'lucide-react-native/icons/chevron-right';
|
|
63
|
+
import Circle from 'lucide-react-native/icons/circle';
|
|
61
64
|
import { Text, textChildren } from '../../primitives/text';
|
|
62
65
|
import { useDirection, useDirectionSign } from '../../hooks/use-direction';
|
|
63
66
|
import { selectionTick } from '../../utils/haptics';
|