panelui-native 0.63.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/badge/index.js +1 -1
- package/lib/module/components/badge/index.js.map +1 -1
- package/lib/module/components/bar-chart/index.js +39 -2
- 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/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 +2 -2
- 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 +2 -2
- 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 +3 -3
- 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/plot/index.js +2 -2
- package/lib/module/components/plot/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/sortable/index.js +5 -55
- 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 +1 -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/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/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/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.map +1 -1
- package/lib/typescript/src/components/radio-group/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 +2 -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/badge/index.tsx +4 -1
- package/src/components/bar-chart/index.tsx +40 -3
- package/src/components/bottom-sheet/index.tsx +3 -3
- package/src/components/button/index.tsx +15 -21
- package/src/components/carousel/index.tsx +42 -20
- package/src/components/checkbox/index.tsx +1 -2
- package/src/components/dialog/index.tsx +3 -3
- 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 +2 -3
- package/src/components/kpi/index.tsx +3 -1
- package/src/components/line-chart/index.tsx +3 -2
- 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/plot/index.tsx +2 -1
- package/src/components/radio-group/index.tsx +1 -2
- package/src/components/sortable/index.tsx +12 -64
- 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 +23 -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 +30 -3
|
@@ -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,
|
|
@@ -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;
|
|
@@ -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,
|
|
@@ -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);
|
|
@@ -657,7 +658,7 @@ export interface LineChartAreaProps {
|
|
|
657
658
|
function LineChartArea({ dataKey, color, colorIndex = 1, opacity = 0.18 }: LineChartAreaProps) {
|
|
658
659
|
const { data, plot, domainMin, domainMax, curve, status, clipId } = useChart('LineChart.Area');
|
|
659
660
|
const fill = useSeriesColor(color, colorIndex);
|
|
660
|
-
const gradientId =
|
|
661
|
+
const gradientId = `panelui-area-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
661
662
|
|
|
662
663
|
const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
|
|
663
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';
|
|
@@ -34,6 +34,7 @@ import { AnimatedPressable } from '../../primitives/animated-pressable';
|
|
|
34
34
|
import { Text } from '../../primitives/text';
|
|
35
35
|
import { selectionTick } from '../../utils/haptics';
|
|
36
36
|
import { Label } from '../label';
|
|
37
|
+
import { normalize, precisionOf } from './number-input-math';
|
|
37
38
|
|
|
38
39
|
/** Wait before a held button starts repeating — a tap must not trip it. */
|
|
39
40
|
const REPEAT_DELAY = 400;
|
|
@@ -156,32 +157,6 @@ export interface NumberInputProps
|
|
|
156
157
|
haptics?: boolean;
|
|
157
158
|
}
|
|
158
159
|
|
|
159
|
-
/** Decimals implied by `step`, so display and math round to the same place. */
|
|
160
|
-
function precisionOf(step: number): number {
|
|
161
|
-
if (!isFinite(step)) return 0;
|
|
162
|
-
const s = String(step);
|
|
163
|
-
const dot = s.indexOf('.');
|
|
164
|
-
return dot === -1 ? 0 : s.length - dot - 1;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function clamp(value: number, min: number, max: number): number {
|
|
168
|
-
return Math.min(Math.max(value, min), max);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Snaps to the nearest step from a finite base (or 0 when `min` is open), then
|
|
173
|
-
* rounds to the step's precision so 0.1 + 0.2 does not drift to 0.30000004.
|
|
174
|
-
*/
|
|
175
|
-
function normalize(value: number, min: number, max: number, step: number): number {
|
|
176
|
-
const base = isFinite(min) ? min : 0;
|
|
177
|
-
const snapped = isFinite(step) && step > 0
|
|
178
|
-
? Math.round((value - base) / step) * step + base
|
|
179
|
-
: value;
|
|
180
|
-
const p = precisionOf(step);
|
|
181
|
-
const factor = 10 ** p;
|
|
182
|
-
return clamp(Math.round(snapped * factor) / factor, min, max);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
160
|
/**
|
|
186
161
|
* A number stepped by buttons or typed by hand, reconciled to one clamped,
|
|
187
162
|
* step-snapped value. See the file header for why it keeps both affordances.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** JavaScript numbers carry about fifteen reliable decimal digits. */
|
|
2
|
+
const MAX_DECIMAL_PRECISION = 15;
|
|
3
|
+
|
|
4
|
+
/** Decimals `step` implies, before the reliable-digits ceiling is applied. */
|
|
5
|
+
function requestedPrecision(step: number): number {
|
|
6
|
+
if (!Number.isFinite(step)) return 0;
|
|
7
|
+
|
|
8
|
+
const [coefficient = '', exponentText] = Math.abs(step).toString().toLowerCase().split('e');
|
|
9
|
+
const decimals = coefficient.split('.')[1]?.length ?? 0;
|
|
10
|
+
const exponent = exponentText === undefined ? 0 : Number(exponentText);
|
|
11
|
+
|
|
12
|
+
return Math.max(0, decimals - exponent);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Decimals implied by `step`, including numbers written with an exponent. */
|
|
16
|
+
export function precisionOf(step: number): number {
|
|
17
|
+
return Math.min(MAX_DECIMAL_PRECISION, requestedPrecision(step));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
21
|
+
return Math.min(Math.max(value, min), max);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Snaps to the nearest step from a finite base, then clears binary drift at
|
|
26
|
+
* the step's decimal precision.
|
|
27
|
+
*/
|
|
28
|
+
export function normalize(value: number, min: number, max: number, step: number): number {
|
|
29
|
+
const base = Number.isFinite(min) ? min : 0;
|
|
30
|
+
const offset = (value - base) / step;
|
|
31
|
+
const snapped =
|
|
32
|
+
Number.isFinite(step) && step > 0 && Number.isFinite(offset)
|
|
33
|
+
? Math.round(offset) * step + base
|
|
34
|
+
: value;
|
|
35
|
+
|
|
36
|
+
// A step finer than the reliable-digit ceiling cannot be tidied at its own
|
|
37
|
+
// precision: rounding at the ceiling instead lands on zero and takes the
|
|
38
|
+
// value with it. The snap already placed it on the step, so leave it there.
|
|
39
|
+
if (requestedPrecision(step) > MAX_DECIMAL_PRECISION) return clamp(snapped, min, max);
|
|
40
|
+
|
|
41
|
+
const precision = precisionOf(step);
|
|
42
|
+
const factor = 10 ** precision;
|
|
43
|
+
const scaled = snapped * factor;
|
|
44
|
+
// Keep the established rounding path while it is safe. toFixed avoids
|
|
45
|
+
// turning a finite large value into Infinity when scaling would overflow.
|
|
46
|
+
const rounded = Number.isFinite(scaled)
|
|
47
|
+
? Math.round(scaled) / factor
|
|
48
|
+
: Number(snapped.toFixed(precision));
|
|
49
|
+
return clamp(rounded, min, max);
|
|
50
|
+
}
|
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
isValidElement,
|
|
69
69
|
useContext,
|
|
70
70
|
useEffect,
|
|
71
|
+
useId,
|
|
71
72
|
useImperativeHandle,
|
|
72
73
|
useMemo,
|
|
73
74
|
useRef,
|
|
@@ -328,7 +329,7 @@ const PlotRoot = forwardRef<PlotHandle, PlotProps>(function PlotRoot(
|
|
|
328
329
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
329
330
|
const [series, setSeries] = useState<[string, string][]>([]);
|
|
330
331
|
const [activeIndexJS, setActiveIndexJS] = useState(-1);
|
|
331
|
-
const clipId =
|
|
332
|
+
const clipId = `panelui-plot-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
332
333
|
|
|
333
334
|
const reveal = useSharedValue(0);
|
|
334
335
|
const domainMin = useSharedValue(0);
|
|
@@ -45,7 +45,7 @@ const itemVariants = tv({
|
|
|
45
45
|
// `self-start` matters: the group is a column, so without it Yoga stretches
|
|
46
46
|
// the row to the full width and the dead space to the right of the label
|
|
47
47
|
// becomes part of the target. Tapping nothing would select the option.
|
|
48
|
-
row: 'flex-row items-center gap-2.5 self-start',
|
|
48
|
+
row: 'min-h-12 min-w-12 flex-row items-center gap-2.5 self-start',
|
|
49
49
|
indicator:
|
|
50
50
|
'h-5 w-5 items-center justify-center rounded-full border border-input bg-background',
|
|
51
51
|
label: 'text-sm text-foreground',
|
|
@@ -248,7 +248,6 @@ const RadioGroupItem = forwardRef<View, RadioGroupItemProps>(
|
|
|
248
248
|
accessibilityLabel={label}
|
|
249
249
|
disabled={disabled}
|
|
250
250
|
onPress={() => context.onValueChange(value)}
|
|
251
|
-
hitSlop={variant === 'card' ? undefined : 8}
|
|
252
251
|
className={slots.row({ className })}
|
|
253
252
|
>
|
|
254
253
|
{body}
|
|
@@ -89,6 +89,7 @@ import { useCSSVariable } from 'uniwind';
|
|
|
89
89
|
import { GripVerticalIcon, IconColorProvider } from '../../icons';
|
|
90
90
|
import { cn } from '../../utils/cn';
|
|
91
91
|
import { impactKnock, selectionTick } from '../../utils/haptics';
|
|
92
|
+
import { moveWithPinned, stepWithPinned } from './reorder';
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
95
|
* Rows getting out of the way of the one being carried. Quick, because they
|
|
@@ -228,66 +229,6 @@ function slotCenter(
|
|
|
228
229
|
return offset + (self === undefined ? 0 : (heights[self] ?? 0)) / 2;
|
|
229
230
|
}
|
|
230
231
|
|
|
231
|
-
/**
|
|
232
|
-
* The order after a row has moved, with pinned rows left where they were.
|
|
233
|
-
*
|
|
234
|
-
* The move is applied first, to the whole list, so a carried row can be dragged
|
|
235
|
-
* *past* a pinned one — refusing the move instead would make a pinned row a
|
|
236
|
-
* wall, and a row that holds its place is not the same as a row nothing may
|
|
237
|
-
* cross. The pinned ids are then put back at the indices they occupy in the
|
|
238
|
-
* laid-out order, and everything else falls into the slots that are left, in
|
|
239
|
-
* the order the move produced.
|
|
240
|
-
*
|
|
241
|
-
* `laid` rather than `list` is what the fixed indices are read from, because
|
|
242
|
-
* that is the one order a pinned row is guaranteed to be correctly placed in:
|
|
243
|
-
* it is where it was rendered, and holding its slot is the whole point.
|
|
244
|
-
*/
|
|
245
|
-
function moveWithPinned(
|
|
246
|
-
list: readonly string[],
|
|
247
|
-
laid: readonly string[],
|
|
248
|
-
pinned: Record<string, boolean>,
|
|
249
|
-
id: string,
|
|
250
|
-
from: number,
|
|
251
|
-
to: number
|
|
252
|
-
): string[] {
|
|
253
|
-
'worklet';
|
|
254
|
-
const moved = [...list];
|
|
255
|
-
moved.splice(from, 1);
|
|
256
|
-
moved.splice(to, 0, id);
|
|
257
|
-
|
|
258
|
-
const next: (string | undefined)[] = [];
|
|
259
|
-
let anyPinned = false;
|
|
260
|
-
for (let i = 0; i < moved.length; i += 1) next.push(undefined);
|
|
261
|
-
for (let i = 0; i < laid.length && i < next.length; i += 1) {
|
|
262
|
-
const at = laid[i];
|
|
263
|
-
if (at !== undefined && pinned[at]) {
|
|
264
|
-
next[i] = at;
|
|
265
|
-
anyPinned = true;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
if (!anyPinned) return moved;
|
|
269
|
-
|
|
270
|
-
const free: string[] = [];
|
|
271
|
-
for (let i = 0; i < moved.length; i += 1) {
|
|
272
|
-
const at = moved[i];
|
|
273
|
-
if (at !== undefined && !pinned[at]) free.push(at);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const result: string[] = [];
|
|
277
|
-
let f = 0;
|
|
278
|
-
for (let i = 0; i < next.length; i += 1) {
|
|
279
|
-
const held = next[i];
|
|
280
|
-
if (held !== undefined) {
|
|
281
|
-
result.push(held);
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
const take = free[f];
|
|
285
|
-
f += 1;
|
|
286
|
-
if (take !== undefined) result.push(take);
|
|
287
|
-
}
|
|
288
|
-
return result;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
232
|
/**
|
|
292
233
|
* Where the dragged row belongs now, given where its edges have reached.
|
|
293
234
|
*
|
|
@@ -760,15 +701,22 @@ function SortableRoot({
|
|
|
760
701
|
const current = rendered.value.indexOf(id);
|
|
761
702
|
if (current < 0) return;
|
|
762
703
|
|
|
763
|
-
const
|
|
764
|
-
|
|
704
|
+
const next = stepWithPinned(
|
|
705
|
+
rendered.value,
|
|
706
|
+
rendered.value,
|
|
707
|
+
pinned.value,
|
|
708
|
+
id,
|
|
709
|
+
current,
|
|
710
|
+
delta
|
|
711
|
+
);
|
|
712
|
+
const to = next.indexOf(id);
|
|
713
|
+
if (to === current) return;
|
|
765
714
|
|
|
766
|
-
const next = reorderItems(rendered.value, current, to);
|
|
767
715
|
order.value = next;
|
|
768
716
|
if (haptics) selectionTick();
|
|
769
717
|
onReorder?.(next, { id, from: current, to });
|
|
770
718
|
},
|
|
771
|
-
[haptics, onReorder, order, rendered]
|
|
719
|
+
[haptics, onReorder, order, pinned, rendered]
|
|
772
720
|
);
|
|
773
721
|
|
|
774
722
|
const context = useMemo<SortableContextValue>(
|