panelui-native 0.66.1 → 0.71.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 +31 -1
- package/lib/module/components/accordion/index.js +28 -9
- package/lib/module/components/accordion/index.js.map +1 -1
- package/lib/module/components/area-chart/index.js +18 -1
- package/lib/module/components/area-chart/index.js.map +1 -1
- package/lib/module/components/bar-chart/index.js +18 -1
- package/lib/module/components/bar-chart/index.js.map +1 -1
- package/lib/module/components/button/index.js +14 -7
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/candlestick-chart/index.js +18 -1
- package/lib/module/components/candlestick-chart/index.js.map +1 -1
- package/lib/module/components/collapsible/index.js +280 -0
- package/lib/module/components/collapsible/index.js.map +1 -0
- package/lib/module/components/flow/index.js +66 -3
- package/lib/module/components/flow/index.js.map +1 -1
- package/lib/module/components/form/index.js +7 -5
- package/lib/module/components/form/index.js.map +1 -1
- package/lib/module/components/form/typed-form.js +11 -0
- package/lib/module/components/form/typed-form.js.map +1 -0
- package/lib/module/components/form/use-form.js +4 -0
- package/lib/module/components/form/use-form.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +27 -1
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +18 -1
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/live-line-chart/index.js +103 -9
- package/lib/module/components/live-line-chart/index.js.map +1 -1
- package/lib/module/components/live-line-chart/live-line-accessibility.js +47 -0
- package/lib/module/components/live-line-chart/live-line-accessibility.js.map +1 -0
- package/lib/module/components/live-line-chart/live-line-lifecycle.js +33 -0
- package/lib/module/components/live-line-chart/live-line-lifecycle.js.map +1 -0
- package/lib/module/components/marquee/index.js +194 -0
- package/lib/module/components/marquee/index.js.map +1 -0
- package/lib/module/components/message-scroller/index.js +172 -10
- package/lib/module/components/message-scroller/index.js.map +1 -1
- package/lib/module/components/message-scroller/message-scroller-math.js +23 -0
- package/lib/module/components/message-scroller/message-scroller-math.js.map +1 -1
- package/lib/module/components/meter/index.js +268 -0
- package/lib/module/components/meter/index.js.map +1 -0
- package/lib/module/components/meter/meter-scale.js +142 -0
- package/lib/module/components/meter/meter-scale.js.map +1 -0
- package/lib/module/components/planner/index.js +788 -0
- package/lib/module/components/planner/index.js.map +1 -0
- package/lib/module/components/planner/planner-announcement.js +30 -0
- package/lib/module/components/planner/planner-announcement.js.map +1 -0
- package/lib/module/components/planner/planner-entries.js +132 -0
- package/lib/module/components/planner/planner-entries.js.map +1 -0
- package/lib/module/components/planner/planner-grid-navigation.js +33 -0
- package/lib/module/components/planner/planner-grid-navigation.js.map +1 -0
- package/lib/module/components/planner/planner-lifecycle.js +45 -0
- package/lib/module/components/planner/planner-lifecycle.js.map +1 -0
- package/lib/module/components/popover/index.js +2 -2
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/radar-chart/index.js +18 -1
- package/lib/module/components/radar-chart/index.js.map +1 -1
- package/lib/module/components/scatter-chart/index.js +18 -1
- package/lib/module/components/scatter-chart/index.js.map +1 -1
- package/lib/module/components/skeleton/index.js +45 -7
- package/lib/module/components/skeleton/index.js.map +1 -1
- package/lib/module/components/spinner/index.js +63 -14
- package/lib/module/components/spinner/index.js.map +1 -1
- package/lib/module/components/time-picker/index.js +46 -21
- package/lib/module/components/time-picker/index.js.map +1 -1
- package/lib/module/components/time-picker/ruler-window.js +25 -0
- package/lib/module/components/time-picker/ruler-window.js.map +1 -0
- package/lib/module/hooks/breakpoint-contract.js +29 -0
- package/lib/module/hooks/breakpoint-contract.js.map +1 -0
- package/lib/module/hooks/index.js +1 -1
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-breakpoint.js +46 -1
- package/lib/module/hooks/use-breakpoint.js.map +1 -1
- package/lib/module/index.js +5 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/chart-accessibility.js +68 -0
- package/lib/module/primitives/chart-accessibility.js.map +1 -0
- package/lib/module/primitives/focus-restoration-store.js +52 -0
- package/lib/module/primitives/focus-restoration-store.js.map +1 -0
- package/lib/module/primitives/portal.js +36 -2
- package/lib/module/primitives/portal.js.map +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts +4 -0
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bar-chart/index.d.ts +2 -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 +2 -1
- package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/collapsible/index.d.ts +76 -0
- package/lib/typescript/src/components/collapsible/index.d.ts.map +1 -0
- package/lib/typescript/src/components/flow/index.d.ts +7 -1
- package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
- package/lib/typescript/src/components/form/index.d.ts +18 -5
- package/lib/typescript/src/components/form/index.d.ts.map +1 -1
- package/lib/typescript/src/components/form/typed-form.d.ts +7 -0
- package/lib/typescript/src/components/form/typed-form.d.ts.map +1 -0
- package/lib/typescript/src/components/form/use-form.d.ts +4 -0
- package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/live-line-chart/index.d.ts +21 -7
- package/lib/typescript/src/components/live-line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/live-line-chart/live-line-accessibility.d.ts +22 -0
- package/lib/typescript/src/components/live-line-chart/live-line-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/live-line-chart/live-line-lifecycle.d.ts +23 -0
- package/lib/typescript/src/components/live-line-chart/live-line-lifecycle.d.ts.map +1 -0
- package/lib/typescript/src/components/marquee/index.d.ts +73 -0
- package/lib/typescript/src/components/marquee/index.d.ts.map +1 -0
- package/lib/typescript/src/components/message-scroller/index.d.ts +30 -2
- package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts +10 -0
- package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts.map +1 -1
- package/lib/typescript/src/components/meter/index.d.ts +273 -0
- package/lib/typescript/src/components/meter/index.d.ts.map +1 -0
- package/lib/typescript/src/components/meter/meter-scale.d.ts +96 -0
- package/lib/typescript/src/components/meter/meter-scale.d.ts.map +1 -0
- package/lib/typescript/src/components/planner/index.d.ts +274 -0
- package/lib/typescript/src/components/planner/index.d.ts.map +1 -0
- package/lib/typescript/src/components/planner/planner-announcement.d.ts +15 -0
- package/lib/typescript/src/components/planner/planner-announcement.d.ts.map +1 -0
- package/lib/typescript/src/components/planner/planner-entries.d.ts +75 -0
- package/lib/typescript/src/components/planner/planner-entries.d.ts.map +1 -0
- package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts +4 -0
- package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts.map +1 -0
- package/lib/typescript/src/components/planner/planner-lifecycle.d.ts +20 -0
- package/lib/typescript/src/components/planner/planner-lifecycle.d.ts.map +1 -0
- package/lib/typescript/src/components/radar-chart/index.d.ts +4 -0
- package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/scatter-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/scatter-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/skeleton/index.d.ts +25 -2
- package/lib/typescript/src/components/skeleton/index.d.ts.map +1 -1
- package/lib/typescript/src/components/spinner/index.d.ts +25 -2
- package/lib/typescript/src/components/spinner/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/index.d.ts +5 -6
- package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/ruler-window.d.ts +12 -0
- package/lib/typescript/src/components/time-picker/ruler-window.d.ts.map +1 -0
- package/lib/typescript/src/hooks/breakpoint-contract.d.ts +6 -0
- package/lib/typescript/src/hooks/breakpoint-contract.d.ts.map +1 -0
- package/lib/typescript/src/hooks/index.d.ts +1 -1
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-breakpoint.d.ts +22 -0
- package/lib/typescript/src/hooks/use-breakpoint.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/chart-accessibility.d.ts +33 -0
- package/lib/typescript/src/primitives/chart-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/primitives/focus-restoration-store.d.ts +24 -0
- package/lib/typescript/src/primitives/focus-restoration-store.d.ts.map +1 -0
- package/lib/typescript/src/primitives/portal.d.ts +4 -0
- package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
- package/lib/typescript/types/form-api.type-test.d.ts +2 -0
- package/lib/typescript/types/form-api.type-test.d.ts.map +1 -0
- package/package.json +63 -4
- package/src/components/accordion/index.tsx +31 -8
- package/src/components/area-chart/index.tsx +27 -2
- package/src/components/bar-chart/index.tsx +27 -2
- package/src/components/button/index.tsx +11 -4
- package/src/components/candlestick-chart/index.tsx +30 -2
- package/src/components/collapsible/index.tsx +278 -0
- package/src/components/flow/index.tsx +93 -14
- package/src/components/form/index.tsx +32 -5
- package/src/components/form/typed-form.ts +13 -0
- package/src/components/form/use-form.ts +9 -0
- package/src/components/heatmap-chart/index.tsx +49 -3
- package/src/components/line-chart/index.tsx +27 -2
- package/src/components/live-line-chart/index.tsx +130 -13
- package/src/components/live-line-chart/live-line-accessibility.ts +61 -0
- package/src/components/live-line-chart/live-line-lifecycle.ts +63 -0
- package/src/components/marquee/index.tsx +232 -0
- package/src/components/message-scroller/index.tsx +234 -6
- package/src/components/message-scroller/message-scroller-math.ts +38 -0
- package/src/components/meter/index.tsx +341 -0
- package/src/components/meter/meter-scale.ts +185 -0
- package/src/components/planner/index.tsx +997 -0
- package/src/components/planner/planner-announcement.ts +37 -0
- package/src/components/planner/planner-entries.ts +155 -0
- package/src/components/planner/planner-grid-navigation.ts +45 -0
- package/src/components/planner/planner-lifecycle.ts +76 -0
- package/src/components/popover/index.tsx +3 -3
- package/src/components/radar-chart/index.tsx +24 -0
- package/src/components/scatter-chart/index.tsx +27 -2
- package/src/components/skeleton/index.tsx +59 -5
- package/src/components/spinner/index.tsx +80 -13
- package/src/components/time-picker/index.tsx +57 -29
- package/src/components/time-picker/ruler-window.ts +24 -0
- package/src/hooks/breakpoint-contract.ts +31 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/use-breakpoint.ts +61 -10
- package/src/index.ts +43 -0
- package/src/primitives/chart-accessibility.ts +102 -0
- package/src/primitives/focus-restoration-store.ts +70 -0
- package/src/primitives/portal.tsx +34 -2
|
@@ -36,12 +36,11 @@
|
|
|
36
36
|
*
|
|
37
37
|
* ## Scroll offset, not gesture maths
|
|
38
38
|
*
|
|
39
|
-
* The wheel, the clock's list and the ruler are
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* scrolling never round-trips through React — only the settled value does.
|
|
39
|
+
* The wheel, the clock's list and the ruler are snapping scroll views, and the
|
|
40
|
+
* selection is `Math.round(offset / itemSize)`. That buys momentum,
|
|
41
|
+
* deceleration, edge bounce and platform-correct fling physics for nothing,
|
|
42
|
+
* and none of it would be worth rebuilding on a pan gesture. The ruler keeps
|
|
43
|
+
* that full scroll range but mounts only an overscanned window of its ticks.
|
|
45
44
|
*/
|
|
46
45
|
import {
|
|
47
46
|
useCallback,
|
|
@@ -79,6 +78,7 @@ import {
|
|
|
79
78
|
accessibilityValueForIndex,
|
|
80
79
|
indexForAccessibilityAction,
|
|
81
80
|
} from './accessibility';
|
|
81
|
+
import { rulerWindow } from './ruler-window';
|
|
82
82
|
import {
|
|
83
83
|
clampTime,
|
|
84
84
|
displayHour,
|
|
@@ -848,10 +848,20 @@ function RulerFace({
|
|
|
848
848
|
*/
|
|
849
849
|
const [width, setWidth] = useState(0);
|
|
850
850
|
const pad = Math.max(0, width / 2 - TICK_SPACING / 2);
|
|
851
|
+
const [windowIndex, setWindowIndex] = useState(index);
|
|
852
|
+
// A controlled/accessibility jump renders its destination before the effect
|
|
853
|
+
// moves the scroll view there; an in-flight gesture follows the scroll window.
|
|
854
|
+
const windowAnchor = index === resting.current ? windowIndex : index;
|
|
855
|
+
const window = rulerWindow(times.length, windowAnchor);
|
|
851
856
|
|
|
852
|
-
const
|
|
853
|
-
(
|
|
854
|
-
|
|
857
|
+
const showIndex = useCallback(
|
|
858
|
+
(next: number) => {
|
|
859
|
+
const nextStart = rulerWindow(times.length, next).start;
|
|
860
|
+
setWindowIndex((current) =>
|
|
861
|
+
rulerWindow(times.length, current).start === nextStart ? current : next
|
|
862
|
+
);
|
|
863
|
+
},
|
|
864
|
+
[times.length]
|
|
855
865
|
);
|
|
856
866
|
|
|
857
867
|
// The scale runs the same rest-and-resync machinery the wheel's columns do,
|
|
@@ -872,10 +882,14 @@ function RulerFace({
|
|
|
872
882
|
|
|
873
883
|
// Unanimated, for the reason given on the wheel's columns: an animated
|
|
874
884
|
// programmatic scroll reports itself as a gesture and the correction loops.
|
|
875
|
-
const snapTo = useCallback(
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
885
|
+
const snapTo = useCallback(
|
|
886
|
+
(to: number) => {
|
|
887
|
+
resting.current = to;
|
|
888
|
+
showIndex(to);
|
|
889
|
+
ref.current?.scrollTo({ x: to * TICK_SPACING, animated: false });
|
|
890
|
+
},
|
|
891
|
+
[showIndex]
|
|
892
|
+
);
|
|
879
893
|
|
|
880
894
|
useEffect(() => {
|
|
881
895
|
if (index === resting.current) return;
|
|
@@ -934,6 +948,13 @@ function RulerFace({
|
|
|
934
948
|
[atRest, cancelTimer, settleAt]
|
|
935
949
|
);
|
|
936
950
|
|
|
951
|
+
const onScroll = useCallback(
|
|
952
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
953
|
+
showIndex(Math.round(event.nativeEvent.contentOffset.x / TICK_SPACING));
|
|
954
|
+
},
|
|
955
|
+
[showIndex]
|
|
956
|
+
);
|
|
957
|
+
|
|
937
958
|
useEffect(() => cancelTimer, [cancelTimer]);
|
|
938
959
|
|
|
939
960
|
const stepsPerHour = Math.max(1, Math.round(60 / minuteStep));
|
|
@@ -985,9 +1006,11 @@ function RulerFace({
|
|
|
985
1006
|
contentOffset={{ x: index * TICK_SPACING, y: 0 }}
|
|
986
1007
|
scrollEnabled={!disabled}
|
|
987
1008
|
showsHorizontalScrollIndicator={false}
|
|
988
|
-
|
|
1009
|
+
snapToInterval={TICK_SPACING}
|
|
989
1010
|
disableIntervalMomentum
|
|
990
1011
|
decelerationRate="fast"
|
|
1012
|
+
onScroll={onScroll}
|
|
1013
|
+
scrollEventThrottle={16}
|
|
991
1014
|
onScrollBeginDrag={startMoving}
|
|
992
1015
|
onScrollEndDrag={onDragEnd}
|
|
993
1016
|
onMomentumScrollBegin={startMoving}
|
|
@@ -1009,22 +1032,27 @@ function RulerFace({
|
|
|
1009
1032
|
at most — which is a legend, not a scale, and reads as clutter
|
|
1010
1033
|
beside a number set at 36px.
|
|
1011
1034
|
*/}
|
|
1012
|
-
{
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
className="items-center justify-center"
|
|
1017
|
-
>
|
|
1035
|
+
<View style={{ width: window.start * TICK_SPACING }} />
|
|
1036
|
+
{times.slice(window.start, window.end).map((_, offset) => {
|
|
1037
|
+
const i = window.start + offset;
|
|
1038
|
+
return (
|
|
1018
1039
|
<View
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1040
|
+
key={i}
|
|
1041
|
+
style={{ width: TICK_SPACING }}
|
|
1042
|
+
className="items-center justify-center"
|
|
1043
|
+
>
|
|
1044
|
+
<View
|
|
1045
|
+
className={cn(
|
|
1046
|
+
'w-0.5 rounded-full',
|
|
1047
|
+
// Taller and darker on the hour, so the scale reads as hours
|
|
1048
|
+
// rather than as an undifferentiated comb.
|
|
1049
|
+
i % stepsPerHour === 0 ? 'h-7 bg-muted-foreground' : 'h-4 bg-border'
|
|
1050
|
+
)}
|
|
1051
|
+
/>
|
|
1052
|
+
</View>
|
|
1053
|
+
);
|
|
1054
|
+
})}
|
|
1055
|
+
<View style={{ width: (times.length - window.end) * TICK_SPACING }} />
|
|
1028
1056
|
</ScrollView>
|
|
1029
1057
|
|
|
1030
1058
|
{/* Over the scale rather than in it: the indicator marks the centre of
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Maximum tick cells mounted by the ruler, regardless of the minute step. */
|
|
2
|
+
export const RULER_WINDOW_SIZE = 121;
|
|
3
|
+
|
|
4
|
+
const WINDOW_SHIFT = 40;
|
|
5
|
+
|
|
6
|
+
export interface RulerWindow {
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A chunked, overscanned slice around the visible tick. It moves only once per
|
|
13
|
+
* WINDOW_SHIFT ticks, leaving at least forty ticks ahead of a normal scroll.
|
|
14
|
+
*/
|
|
15
|
+
export function rulerWindow(itemCount: number, visibleIndex: number): RulerWindow {
|
|
16
|
+
const count = Math.max(0, Math.floor(itemCount));
|
|
17
|
+
if (count <= RULER_WINDOW_SIZE) return { start: 0, end: count };
|
|
18
|
+
|
|
19
|
+
const index = Math.min(Math.max(Math.round(visibleIndex), 0), count - 1);
|
|
20
|
+
const chunk = Math.floor(index / WINDOW_SHIFT) * WINDOW_SHIFT;
|
|
21
|
+
const before = Math.floor((RULER_WINDOW_SIZE - WINDOW_SHIFT) / 2);
|
|
22
|
+
const start = Math.min(Math.max(chunk - before, 0), count - RULER_WINDOW_SIZE);
|
|
23
|
+
return { start, end: start + RULER_WINDOW_SIZE };
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type BreakpointDefinition = Readonly<Record<string, number>>;
|
|
2
|
+
|
|
3
|
+
/** Validate declaration order once, when an app creates its responsive contract. */
|
|
4
|
+
export function breakpointEntries<const T extends BreakpointDefinition>(definition: T) {
|
|
5
|
+
const entries = Object.entries(definition) as [keyof T & string, number][];
|
|
6
|
+
if (!entries.length) throw new Error('Breakpoints must declare at least one threshold.');
|
|
7
|
+
let previous = -1;
|
|
8
|
+
for (const [name, threshold] of entries) {
|
|
9
|
+
if (!name || !Number.isFinite(threshold) || threshold < 0) {
|
|
10
|
+
throw new Error(`Breakpoint "${name}" must have a finite, non-negative threshold.`);
|
|
11
|
+
}
|
|
12
|
+
if (threshold <= previous) {
|
|
13
|
+
throw new Error('Breakpoint thresholds must be strictly ascending in declaration order.');
|
|
14
|
+
}
|
|
15
|
+
previous = threshold;
|
|
16
|
+
}
|
|
17
|
+
return entries;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Largest satisfied name, with `base` as the deterministic below-range fallback. */
|
|
21
|
+
export function breakpointAt<Name extends string>(
|
|
22
|
+
entries: readonly (readonly [Name, number])[],
|
|
23
|
+
width: number
|
|
24
|
+
): Name | 'base' {
|
|
25
|
+
let current: Name | 'base' = 'base';
|
|
26
|
+
for (const [name, threshold] of entries) {
|
|
27
|
+
if (width < threshold) break;
|
|
28
|
+
current = name;
|
|
29
|
+
}
|
|
30
|
+
return current;
|
|
31
|
+
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -10,8 +10,12 @@ export {
|
|
|
10
10
|
} from './use-disclosure';
|
|
11
11
|
export {
|
|
12
12
|
useBreakpoint,
|
|
13
|
+
createBreakpoints,
|
|
13
14
|
BREAKPOINTS,
|
|
14
15
|
type Breakpoint,
|
|
16
|
+
type BreakpointContract,
|
|
17
|
+
type BreakpointProviderProps,
|
|
18
|
+
type BreakpointResult,
|
|
15
19
|
type UseBreakpointResult,
|
|
16
20
|
} from './use-breakpoint';
|
|
17
21
|
export { useKeyboard, type UseKeyboardResult } from './use-keyboard';
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import { createContext, createElement, useContext, type ReactNode } from 'react';
|
|
1
2
|
import { useWindowDimensions } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
breakpointAt,
|
|
5
|
+
breakpointEntries,
|
|
6
|
+
type BreakpointDefinition,
|
|
7
|
+
} from './breakpoint-contract';
|
|
2
8
|
|
|
3
9
|
/** Tailwind's breakpoints, minus the ones no phone or tablet reaches. */
|
|
4
10
|
export const BREAKPOINTS = { sm: 640, md: 768, lg: 1024, xl: 1280 } as const;
|
|
11
|
+
const DEFAULT_ENTRIES = breakpointEntries(BREAKPOINTS);
|
|
5
12
|
|
|
6
13
|
export type Breakpoint = keyof typeof BREAKPOINTS;
|
|
7
14
|
|
|
@@ -15,6 +22,59 @@ export interface UseBreakpointResult {
|
|
|
15
22
|
isLandscape: boolean;
|
|
16
23
|
}
|
|
17
24
|
|
|
25
|
+
export interface BreakpointResult<Name extends string> {
|
|
26
|
+
current: Name | 'base';
|
|
27
|
+
isAtLeast: (breakpoint: Name) => boolean;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
isLandscape: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface BreakpointProviderProps {
|
|
34
|
+
children?: ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface BreakpointContract<Name extends string> {
|
|
38
|
+
Provider: (props: BreakpointProviderProps) => ReactNode;
|
|
39
|
+
useBreakpoint: () => BreakpointResult<Name>;
|
|
40
|
+
breakpoints: Readonly<Record<Name, number>>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create app-specific responsive names. One Provider owns the window
|
|
45
|
+
* subscription; every bound hook below it reads the same dimensions.
|
|
46
|
+
*/
|
|
47
|
+
export function createBreakpoints<const T extends BreakpointDefinition>(
|
|
48
|
+
definition: T
|
|
49
|
+
): BreakpointContract<keyof T & string> {
|
|
50
|
+
type Name = keyof T & string;
|
|
51
|
+
const breakpoints = Object.freeze({ ...definition }) as Readonly<T>;
|
|
52
|
+
const entries = breakpointEntries(breakpoints);
|
|
53
|
+
const Context = createContext<{ width: number; height: number } | null>(null);
|
|
54
|
+
|
|
55
|
+
function Provider({ children }: BreakpointProviderProps) {
|
|
56
|
+
const dimensions = useWindowDimensions();
|
|
57
|
+
return createElement(Context.Provider, { value: dimensions }, children);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function useBoundBreakpoint(): BreakpointResult<Name> {
|
|
61
|
+
const dimensions = useContext(Context);
|
|
62
|
+
if (!dimensions) {
|
|
63
|
+
throw new Error('This breakpoint hook must be used inside its Breakpoint Provider.');
|
|
64
|
+
}
|
|
65
|
+
const { width, height } = dimensions;
|
|
66
|
+
return {
|
|
67
|
+
current: breakpointAt(entries, width),
|
|
68
|
+
isAtLeast: (name) => width >= breakpoints[name]!,
|
|
69
|
+
width,
|
|
70
|
+
height,
|
|
71
|
+
isLandscape: width > height,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { Provider, useBreakpoint: useBoundBreakpoint, breakpoints };
|
|
76
|
+
}
|
|
77
|
+
|
|
18
78
|
/**
|
|
19
79
|
* Responsive state from the window size — the React Native answer to a media
|
|
20
80
|
* query, which has no equivalent here.
|
|
@@ -31,16 +91,7 @@ export interface UseBreakpointResult {
|
|
|
31
91
|
export function useBreakpoint(): UseBreakpointResult {
|
|
32
92
|
const { width, height } = useWindowDimensions();
|
|
33
93
|
|
|
34
|
-
const current
|
|
35
|
-
width >= BREAKPOINTS.xl
|
|
36
|
-
? 'xl'
|
|
37
|
-
: width >= BREAKPOINTS.lg
|
|
38
|
-
? 'lg'
|
|
39
|
-
: width >= BREAKPOINTS.md
|
|
40
|
-
? 'md'
|
|
41
|
-
: width >= BREAKPOINTS.sm
|
|
42
|
-
? 'sm'
|
|
43
|
-
: 'base';
|
|
94
|
+
const current = breakpointAt(DEFAULT_ENTRIES, width);
|
|
44
95
|
|
|
45
96
|
return {
|
|
46
97
|
current,
|
package/src/index.ts
CHANGED
|
@@ -416,6 +416,14 @@ export {
|
|
|
416
416
|
type ChipVariant,
|
|
417
417
|
type ChipSize,
|
|
418
418
|
} from './components/chip';
|
|
419
|
+
export {
|
|
420
|
+
Collapsible,
|
|
421
|
+
type CollapsibleProps,
|
|
422
|
+
type CollapsibleTriggerProps,
|
|
423
|
+
type CollapsibleIndicatorProps,
|
|
424
|
+
type CollapsibleContentProps,
|
|
425
|
+
type CollapsibleVariant,
|
|
426
|
+
} from './components/collapsible';
|
|
419
427
|
export {
|
|
420
428
|
ColorPicker,
|
|
421
429
|
type ColorPickerProps,
|
|
@@ -485,10 +493,14 @@ export {
|
|
|
485
493
|
} from './components/field';
|
|
486
494
|
export {
|
|
487
495
|
Form,
|
|
496
|
+
createForm,
|
|
488
497
|
useForm,
|
|
489
498
|
useField,
|
|
490
499
|
type FormProps,
|
|
491
500
|
type FormFieldProps,
|
|
501
|
+
type TypedForm,
|
|
502
|
+
type TypedFormProps,
|
|
503
|
+
type TypedFormFieldProps,
|
|
492
504
|
type FormApi,
|
|
493
505
|
type FieldErrors,
|
|
494
506
|
type FieldTouched,
|
|
@@ -724,6 +736,11 @@ export {
|
|
|
724
736
|
type MarkerIconProps,
|
|
725
737
|
type MarkerContentProps,
|
|
726
738
|
} from './components/marker';
|
|
739
|
+
export {
|
|
740
|
+
Marquee,
|
|
741
|
+
type MarqueeProps,
|
|
742
|
+
type MarqueeDirection,
|
|
743
|
+
} from './components/marquee';
|
|
727
744
|
export {
|
|
728
745
|
Menu,
|
|
729
746
|
type MenuProps,
|
|
@@ -742,6 +759,12 @@ export {
|
|
|
742
759
|
type MenuItemVariant,
|
|
743
760
|
type MenuRadioIndicator,
|
|
744
761
|
} from './components/menu';
|
|
762
|
+
export {
|
|
763
|
+
Meter,
|
|
764
|
+
type MeterProps,
|
|
765
|
+
type MeterColor,
|
|
766
|
+
type MeterThreshold,
|
|
767
|
+
} from './components/meter';
|
|
745
768
|
export {
|
|
746
769
|
MessageScroller,
|
|
747
770
|
useMessageScroller,
|
|
@@ -779,6 +802,26 @@ export {
|
|
|
779
802
|
type PopoverAnchorRect,
|
|
780
803
|
type PopoverAnchorControls,
|
|
781
804
|
} from './components/popover';
|
|
805
|
+
export {
|
|
806
|
+
Planner,
|
|
807
|
+
type PlannerProps,
|
|
808
|
+
type PlannerEntry,
|
|
809
|
+
type PlannerCategory,
|
|
810
|
+
type PlannerHeaderProps,
|
|
811
|
+
type PlannerTitleProps,
|
|
812
|
+
type PlannerTodayProps,
|
|
813
|
+
type PlannerNavProps,
|
|
814
|
+
type PlannerActionProps,
|
|
815
|
+
type PlannerGridProps,
|
|
816
|
+
type PlannerDayProps,
|
|
817
|
+
type PlannerDayState,
|
|
818
|
+
type PlannerDayRenderer,
|
|
819
|
+
type PlannerLegendProps,
|
|
820
|
+
type PlannerSummaryProps,
|
|
821
|
+
type PlannerFooterProps,
|
|
822
|
+
type PlannerDetailsProps,
|
|
823
|
+
type PlannerCountedCategory,
|
|
824
|
+
} from './components/planner';
|
|
782
825
|
export {
|
|
783
826
|
Post,
|
|
784
827
|
type PostProps,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createElement } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ChartAccessibilityProps<Datum> {
|
|
4
|
+
/** Spoken summary. Defaults to the chart family and number of data entries. */
|
|
5
|
+
accessibilityLabel?: string;
|
|
6
|
+
/** Additional guidance spoken after the chart summary. */
|
|
7
|
+
accessibilityHint?: string;
|
|
8
|
+
/** Overrides the spoken label for one data entry. */
|
|
9
|
+
accessibilityLabelForDatum?: (datum: Datum, index: number) => string;
|
|
10
|
+
/** Makes each data entry actionable without making the SVG geometry focusable. */
|
|
11
|
+
onAccessibilityDatumPress?: (datum: Datum, index: number) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChartAccessibilityItem<Datum> {
|
|
15
|
+
datum: Datum;
|
|
16
|
+
index: number;
|
|
17
|
+
label: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function spoken(value: unknown): string | null {
|
|
21
|
+
if (value instanceof Date) return value.toLocaleDateString();
|
|
22
|
+
if (typeof value === 'string' || typeof value === 'number') return String(value);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Pure semantic projection, shared by tests and the native accessibility tree. */
|
|
27
|
+
export function chartAccessibilityModel<Datum>(
|
|
28
|
+
chart: string,
|
|
29
|
+
data: Datum[],
|
|
30
|
+
valueOf: (datum: Datum) => [string, unknown][],
|
|
31
|
+
labelForDatum?: (datum: Datum, index: number) => string
|
|
32
|
+
): { summary: string; items: ChartAccessibilityItem<Datum>[] } {
|
|
33
|
+
return {
|
|
34
|
+
summary: `${chart}, ${data.length} ${data.length === 1 ? 'data entry' : 'data entries'}`,
|
|
35
|
+
items: data.map((datum, index) => ({
|
|
36
|
+
datum,
|
|
37
|
+
index,
|
|
38
|
+
label:
|
|
39
|
+
labelForDatum?.(datum, index) ??
|
|
40
|
+
valueOf(datum)
|
|
41
|
+
.map(([label, value]) => {
|
|
42
|
+
const text = spoken(value);
|
|
43
|
+
return text === null ? null : `${label}, ${text}`;
|
|
44
|
+
})
|
|
45
|
+
.filter((part): part is string => part !== null)
|
|
46
|
+
.join('. '),
|
|
47
|
+
})),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ChartAccessibilityDataProps<Datum>
|
|
52
|
+
extends ChartAccessibilityProps<Datum> {
|
|
53
|
+
chart: string;
|
|
54
|
+
data: Datum[];
|
|
55
|
+
valueOf: (datum: Datum) => [string, unknown][];
|
|
56
|
+
disabled?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A compact semantic sibling to the visual plot. The SVG remains one
|
|
61
|
+
* decorative subtree while screen readers receive one summary and one entry
|
|
62
|
+
* per datum, rather than every path, grid line and marker.
|
|
63
|
+
*/
|
|
64
|
+
export function ChartAccessibilityData<Datum>({
|
|
65
|
+
chart,
|
|
66
|
+
data,
|
|
67
|
+
valueOf,
|
|
68
|
+
disabled = false,
|
|
69
|
+
accessibilityLabel,
|
|
70
|
+
accessibilityHint,
|
|
71
|
+
accessibilityLabelForDatum,
|
|
72
|
+
onAccessibilityDatumPress,
|
|
73
|
+
}: ChartAccessibilityDataProps<Datum>) {
|
|
74
|
+
if (disabled) return null;
|
|
75
|
+
// Keep the pure semantic projection importable by Node tests without asking
|
|
76
|
+
// Node to parse React Native's Flow entry point.
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
78
|
+
const { Pressable, View } = require('react-native') as typeof import('react-native');
|
|
79
|
+
const model = chartAccessibilityModel(chart, data, valueOf, accessibilityLabelForDatum);
|
|
80
|
+
|
|
81
|
+
return createElement(
|
|
82
|
+
View,
|
|
83
|
+
{ style: { position: 'absolute', left: -10_000, width: 1, height: 1 } },
|
|
84
|
+
createElement(View, {
|
|
85
|
+
accessible: true,
|
|
86
|
+
accessibilityRole: 'image',
|
|
87
|
+
accessibilityLabel: accessibilityLabel ?? model.summary,
|
|
88
|
+
accessibilityHint,
|
|
89
|
+
}),
|
|
90
|
+
...model.items.map((item) =>
|
|
91
|
+
createElement(Pressable, {
|
|
92
|
+
key: item.index,
|
|
93
|
+
accessible: true,
|
|
94
|
+
accessibilityRole: onAccessibilityDatumPress ? 'button' : 'text',
|
|
95
|
+
accessibilityLabel: item.label,
|
|
96
|
+
onPress: onAccessibilityDatumPress
|
|
97
|
+
? () => onAccessibilityDatumPress(item.datum, item.index)
|
|
98
|
+
: undefined,
|
|
99
|
+
})
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
interface FocusDocument {
|
|
2
|
+
activeElement?: FocusTarget | null;
|
|
3
|
+
body?: FocusTarget | null;
|
|
4
|
+
documentElement?: FocusTarget | null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface FocusTarget {
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
isConnected?: boolean;
|
|
10
|
+
focus: (options?: { preventScroll?: boolean }) => void;
|
|
11
|
+
getAttribute?: (name: string) => string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type RestoreFocus = () => boolean;
|
|
15
|
+
|
|
16
|
+
interface FocusOwner {
|
|
17
|
+
owner: string;
|
|
18
|
+
released: boolean;
|
|
19
|
+
restore: RestoreFocus;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Restores nested overlays in last-opened order, even when parents close first. */
|
|
23
|
+
export class FocusRestorationStore {
|
|
24
|
+
private owners: FocusOwner[] = [];
|
|
25
|
+
|
|
26
|
+
acquire(owner: string, restore: RestoreFocus): (() => void) {
|
|
27
|
+
const entry = { owner, released: false, restore };
|
|
28
|
+
this.owners.push(entry);
|
|
29
|
+
|
|
30
|
+
return () => {
|
|
31
|
+
if (entry.released) return;
|
|
32
|
+
entry.released = true;
|
|
33
|
+
this.restoreReleasedOwners();
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private restoreReleasedOwners() {
|
|
38
|
+
const candidates: RestoreFocus[] = [];
|
|
39
|
+
while (this.owners.at(-1)?.released) {
|
|
40
|
+
const entry = this.owners.pop();
|
|
41
|
+
if (entry) candidates.push(entry.restore);
|
|
42
|
+
}
|
|
43
|
+
candidates.some((restore) => restore());
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Captures the current browser focus without evaluating DOM APIs on native. */
|
|
48
|
+
export function captureFocusRestore(
|
|
49
|
+
document = (globalThis as { document?: FocusDocument }).document
|
|
50
|
+
): RestoreFocus {
|
|
51
|
+
const target = document?.activeElement;
|
|
52
|
+
if (!target || target === document?.body || target === document?.documentElement) {
|
|
53
|
+
return () => false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
if (target.isConnected === false || target.disabled) return false;
|
|
58
|
+
if (target.getAttribute?.('aria-disabled') === 'true') return false;
|
|
59
|
+
try {
|
|
60
|
+
target.focus({ preventScroll: true });
|
|
61
|
+
} catch {
|
|
62
|
+
try {
|
|
63
|
+
target.focus();
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return document?.activeElement === target;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type ReactNode,
|
|
9
9
|
} from 'react';
|
|
10
10
|
import { ModalIsolationStore } from './modal-isolation-store';
|
|
11
|
+
import { captureFocusRestore, FocusRestorationStore } from './focus-restoration-store';
|
|
11
12
|
|
|
12
13
|
type PortalMap = ReadonlyMap<string, ReactNode>;
|
|
13
14
|
|
|
@@ -41,16 +42,21 @@ class PortalStore {
|
|
|
41
42
|
|
|
42
43
|
const PortalContext = createContext<PortalStore | null>(null);
|
|
43
44
|
const ModalIsolationContext = createContext<ModalIsolationStore | null>(null);
|
|
45
|
+
const FocusRestorationContext = createContext<FocusRestorationStore | null>(null);
|
|
44
46
|
|
|
45
47
|
export function PortalProvider({ children }: { children: ReactNode }) {
|
|
46
48
|
const portalStoreRef = useRef<PortalStore | null>(null);
|
|
47
49
|
const modalStoreRef = useRef<ModalIsolationStore | null>(null);
|
|
50
|
+
const focusStoreRef = useRef<FocusRestorationStore | null>(null);
|
|
48
51
|
portalStoreRef.current ??= new PortalStore();
|
|
49
52
|
modalStoreRef.current ??= new ModalIsolationStore();
|
|
53
|
+
focusStoreRef.current ??= new FocusRestorationStore();
|
|
50
54
|
return (
|
|
51
55
|
<PortalContext.Provider value={portalStoreRef.current}>
|
|
52
56
|
<ModalIsolationContext.Provider value={modalStoreRef.current}>
|
|
53
|
-
{
|
|
57
|
+
<FocusRestorationContext.Provider value={focusStoreRef.current}>
|
|
58
|
+
{children}
|
|
59
|
+
</FocusRestorationContext.Provider>
|
|
54
60
|
</ModalIsolationContext.Provider>
|
|
55
61
|
</PortalContext.Provider>
|
|
56
62
|
);
|
|
@@ -80,9 +86,27 @@ export function Portal({ children }: { children: ReactNode }) {
|
|
|
80
86
|
/** A portal that owns modal focus until it unmounts. */
|
|
81
87
|
export function ModalPortal({ children }: { children: ReactNode }) {
|
|
82
88
|
const store = useModalIsolationStore('ModalPortal');
|
|
89
|
+
const focusStore = useFocusRestorationStore('ModalPortal');
|
|
83
90
|
const owner = useId();
|
|
84
91
|
|
|
85
|
-
useEffect(() =>
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const releaseIsolation = store.acquire(owner);
|
|
94
|
+
const releaseFocus = focusStore.acquire(owner, captureFocusRestore());
|
|
95
|
+
return () => {
|
|
96
|
+
releaseIsolation();
|
|
97
|
+
releaseFocus();
|
|
98
|
+
};
|
|
99
|
+
}, [focusStore, owner, store]);
|
|
100
|
+
|
|
101
|
+
return <Portal>{children}</Portal>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A non-isolating portal that returns browser focus to what opened it. */
|
|
105
|
+
export function FocusRestorePortal({ children }: { children: ReactNode }) {
|
|
106
|
+
const store = useFocusRestorationStore('FocusRestorePortal');
|
|
107
|
+
const owner = useId();
|
|
108
|
+
|
|
109
|
+
useEffect(() => store.acquire(owner, captureFocusRestore()), [owner, store]);
|
|
86
110
|
|
|
87
111
|
return <Portal>{children}</Portal>;
|
|
88
112
|
}
|
|
@@ -101,6 +125,14 @@ function useModalIsolationStore(component: string): ModalIsolationStore {
|
|
|
101
125
|
return store;
|
|
102
126
|
}
|
|
103
127
|
|
|
128
|
+
function useFocusRestorationStore(component: string): FocusRestorationStore {
|
|
129
|
+
const store = useContext(FocusRestorationContext);
|
|
130
|
+
if (!store) {
|
|
131
|
+
throw new Error(`${component} must be used within a <PanelUIProvider>`);
|
|
132
|
+
}
|
|
133
|
+
return store;
|
|
134
|
+
}
|
|
135
|
+
|
|
104
136
|
/** Mount point for portaled content. PanelUIProvider renders one automatically. */
|
|
105
137
|
export function PortalHost() {
|
|
106
138
|
const store = usePortalStore('PortalHost');
|