panelui-native 0.77.0 → 0.79.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/lib/module/components/area-chart/index.js +32 -25
- package/lib/module/components/area-chart/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js +97 -15
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/funnel-chart/index.js +15 -2
- package/lib/module/components/funnel-chart/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +19 -27
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/hex-chart/index.js +2 -0
- package/lib/module/components/hex-chart/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +38 -31
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/map/index.js +63 -39
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/map/map-children.js +54 -0
- package/lib/module/components/map/map-children.js.map +1 -0
- package/lib/module/components/map/maplibre.js +31 -3
- package/lib/module/components/map/maplibre.js.map +1 -1
- package/lib/module/components/markdown-editor/index.js +288 -34
- package/lib/module/components/markdown-editor/index.js.map +1 -1
- package/lib/module/components/markdown-editor/markdown-transforms.js +272 -9
- package/lib/module/components/markdown-editor/markdown-transforms.js.map +1 -1
- package/lib/module/components/marquee/index.js +205 -46
- package/lib/module/components/marquee/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +162 -34
- package/lib/module/components/panelside/index.js.map +1 -1
- package/lib/module/components/pie-chart/index.js +15 -2
- package/lib/module/components/pie-chart/index.js.map +1 -1
- package/lib/module/components/plot/index.js +42 -44
- package/lib/module/components/plot/index.js.map +1 -1
- package/lib/module/components/progress-button/index.js +617 -0
- package/lib/module/components/progress-button/index.js.map +1 -0
- package/lib/module/components/progress-button/progress-button-hold.js +91 -0
- package/lib/module/components/progress-button/progress-button-hold.js.map +1 -0
- package/lib/module/components/scatter-chart/index.js +6 -25
- package/lib/module/components/scatter-chart/index.js.map +1 -1
- package/lib/module/components/section-rail/index.js +40 -17
- package/lib/module/components/section-rail/index.js.map +1 -1
- package/lib/module/components/split-view/index.js +54 -10
- package/lib/module/components/split-view/index.js.map +1 -1
- package/lib/module/components/theme-selector/index.js +239 -0
- package/lib/module/components/theme-selector/index.js.map +1 -0
- package/lib/module/components/theme-selector/theme-preview.js +320 -0
- package/lib/module/components/theme-selector/theme-preview.js.map +1 -0
- package/lib/module/components/timeline/index.js +145 -14
- package/lib/module/components/timeline/index.js.map +1 -1
- package/lib/module/components/toast/index.js +45 -11
- package/lib/module/components/toast/index.js.map +1 -1
- package/lib/module/components/treemap-chart/index.js +63 -6
- package/lib/module/components/treemap-chart/index.js.map +1 -1
- package/lib/module/components/waterfall-chart/index.js +20 -4
- package/lib/module/components/waterfall-chart/index.js.map +1 -1
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-scroll-sections.js +44 -2
- package/lib/module/hooks/use-scroll-sections.js.map +1 -1
- package/lib/module/hooks/use-skeleton-handoff.js +58 -0
- package/lib/module/hooks/use-skeleton-handoff.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/funnel-chart/index.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/line-chart/index.d.ts +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/index.d.ts +8 -4
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/map-children.d.ts +41 -0
- package/lib/typescript/src/components/map/map-children.d.ts.map +1 -0
- package/lib/typescript/src/components/map/maplibre.d.ts +8 -4
- package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -1
- package/lib/typescript/src/components/markdown-editor/index.d.ts +64 -5
- package/lib/typescript/src/components/markdown-editor/index.d.ts.map +1 -1
- package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts +33 -5
- package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts.map +1 -1
- package/lib/typescript/src/components/marquee/index.d.ts +55 -2
- package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
- package/lib/typescript/src/components/panelside/index.d.ts +79 -4
- package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
- package/lib/typescript/src/components/pie-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/plot/index.d.ts +1 -3
- package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
- package/lib/typescript/src/components/progress-button/index.d.ts +311 -0
- package/lib/typescript/src/components/progress-button/index.d.ts.map +1 -0
- package/lib/typescript/src/components/progress-button/progress-button-hold.d.ts +60 -0
- package/lib/typescript/src/components/progress-button/progress-button-hold.d.ts.map +1 -0
- package/lib/typescript/src/components/scatter-chart/index.d.ts +1 -1
- package/lib/typescript/src/components/scatter-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -1
- package/lib/typescript/src/components/split-view/index.d.ts +10 -1
- package/lib/typescript/src/components/split-view/index.d.ts.map +1 -1
- package/lib/typescript/src/components/theme-selector/index.d.ts +107 -0
- package/lib/typescript/src/components/theme-selector/index.d.ts.map +1 -0
- package/lib/typescript/src/components/theme-selector/theme-preview.d.ts +32 -0
- package/lib/typescript/src/components/theme-selector/theme-preview.d.ts.map +1 -0
- package/lib/typescript/src/components/timeline/index.d.ts +7 -0
- package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
- package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
- package/lib/typescript/src/components/treemap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/index.d.ts +1 -0
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts +1 -0
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-skeleton-handoff.d.ts +11 -0
- package/lib/typescript/src/hooks/use-skeleton-handoff.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -4
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/area-chart/index.tsx +49 -37
- package/src/components/bottom-sheet/index.tsx +109 -11
- package/src/components/funnel-chart/index.tsx +10 -2
- package/src/components/heatmap-chart/index.tsx +27 -22
- package/src/components/hex-chart/index.tsx +22 -6
- package/src/components/line-chart/index.tsx +57 -43
- package/src/components/map/index.tsx +63 -36
- package/src/components/map/map-children.ts +66 -0
- package/src/components/map/maplibre.ts +32 -4
- package/src/components/markdown-editor/index.tsx +413 -35
- package/src/components/markdown-editor/markdown-transforms.ts +274 -10
- package/src/components/marquee/index.tsx +254 -58
- package/src/components/panelside/index.tsx +179 -40
- package/src/components/pie-chart/index.tsx +10 -2
- package/src/components/plot/index.tsx +66 -50
- package/src/components/progress-button/index.tsx +684 -0
- package/src/components/progress-button/progress-button-hold.ts +88 -0
- package/src/components/scatter-chart/index.tsx +4 -25
- package/src/components/section-rail/index.tsx +39 -19
- package/src/components/split-view/index.tsx +53 -8
- package/src/components/theme-selector/index.tsx +281 -0
- package/src/components/theme-selector/theme-preview.tsx +205 -0
- package/src/components/timeline/index.tsx +143 -17
- package/src/components/toast/index.tsx +60 -11
- package/src/components/treemap-chart/index.tsx +65 -8
- package/src/components/waterfall-chart/index.tsx +14 -4
- package/src/hooks/index.ts +5 -0
- package/src/hooks/use-scroll-sections.ts +52 -3
- package/src/hooks/use-skeleton-handoff.ts +69 -0
- package/src/index.ts +25 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The arithmetic behind a hold, kept out of the component so it can be tested
|
|
3
|
+
* without a renderer — the same split `splitter-math` and `ai-input-growth` use.
|
|
4
|
+
*
|
|
5
|
+
* None of it is worklet-only, so the component can call it from either runtime.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Milliseconds a hold has to be sustained before it counts. */
|
|
9
|
+
export const DEFAULT_HOLD_DURATION = 2000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Milliseconds a complete fill takes to rewind, when the hold length is not
|
|
13
|
+
* known — the default only exists so {@link releaseDuration} has one.
|
|
14
|
+
*
|
|
15
|
+
* The component always passes the hold's own duration instead: the drain is
|
|
16
|
+
* the fill running backwards at the same rate, so a two-second hold takes two
|
|
17
|
+
* seconds to give back.
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_RELEASE_DURATION = DEFAULT_HOLD_DURATION;
|
|
20
|
+
|
|
21
|
+
/** Milliseconds before an `autoReset` button offers itself again. */
|
|
22
|
+
export const DEFAULT_AUTO_RESET_DELAY = 1000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A hold duration that cannot produce a broken animation.
|
|
26
|
+
*
|
|
27
|
+
* Zero would complete on touch-down, which defeats the point of the control:
|
|
28
|
+
* a hold-to-confirm that confirms on a tap is a button with extra steps. A
|
|
29
|
+
* floor rather than a rejection, because the caller asking for `0` wants it
|
|
30
|
+
* fast, not broken.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveHoldDuration(duration: number | undefined): number {
|
|
33
|
+
if (duration === undefined) return DEFAULT_HOLD_DURATION;
|
|
34
|
+
if (!Number.isFinite(duration)) return DEFAULT_HOLD_DURATION;
|
|
35
|
+
return Math.max(200, duration);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How long the fill should take to drain from where it is.
|
|
40
|
+
*
|
|
41
|
+
* The fill, run backwards. `full` is the hold's own duration, so the drain
|
|
42
|
+
* covers the distance left at exactly the rate it was filled at: let go at
|
|
43
|
+
* nine tenths of a two-second hold and it takes 1.8 seconds to give back, the
|
|
44
|
+
* same 1.8 seconds it took to earn.
|
|
45
|
+
*
|
|
46
|
+
* That is the point of it. The wait was drawn on the button, so undoing the
|
|
47
|
+
* wait is worth drawing too — a fill that vanishes has been deleted, and a
|
|
48
|
+
* fill that travels back has been let go.
|
|
49
|
+
*/
|
|
50
|
+
export function releaseDuration(
|
|
51
|
+
progress: number,
|
|
52
|
+
full: number = DEFAULT_RELEASE_DURATION
|
|
53
|
+
): number {
|
|
54
|
+
// A worklet, because the only runtime that knows how far the fill actually
|
|
55
|
+
// got is the one animating it. Reading a shared value from JavaScript gives
|
|
56
|
+
// the value before the animation started, so the release has to be started
|
|
57
|
+
// from the UI thread — and that means this has to run there too.
|
|
58
|
+
'worklet';
|
|
59
|
+
const travelled = Math.min(1, Math.max(0, progress));
|
|
60
|
+
return Math.max(80, full * travelled);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether a hold that has been released should still be honoured.
|
|
65
|
+
*
|
|
66
|
+
* The fill is the promise, so anything short of the end is not a confirmation —
|
|
67
|
+
* there is no "close enough" here. It exists as a named function because the
|
|
68
|
+
* tempting version of this rule is a tolerance, and a tolerance means the
|
|
69
|
+
* button sometimes fires when the reader let go early on purpose.
|
|
70
|
+
*/
|
|
71
|
+
export function isComplete(progress: number): boolean {
|
|
72
|
+
'worklet';
|
|
73
|
+
return progress >= 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* How long it takes to cover the distance still ahead, at the fill's own rate.
|
|
78
|
+
*
|
|
79
|
+
* A press that arrives while the fill is on its way back should carry on from
|
|
80
|
+
* where it is rather than restarting the clock — otherwise a fill picked up at
|
|
81
|
+
* halfway takes the whole hold to cover the half that is left, and the second
|
|
82
|
+
* attempt is twice as slow as the first for no reason the reader can see.
|
|
83
|
+
*/
|
|
84
|
+
export function fillDuration(from: number, full: number): number {
|
|
85
|
+
'worklet';
|
|
86
|
+
const remaining = Math.min(1, Math.max(0, 1 - from));
|
|
87
|
+
return Math.max(80, full * remaining);
|
|
88
|
+
}
|
|
@@ -70,6 +70,7 @@ import { Text } from '../../primitives/text';
|
|
|
70
70
|
import { ChartAccessibilityData, type ChartAccessibilityProps } from '../../primitives/chart-accessibility';
|
|
71
71
|
import { compactNumber, useSeriesColor, xAt, yOf, type Plot } from '../../utils/chart';
|
|
72
72
|
import { cn } from '../../utils/cn';
|
|
73
|
+
import { useSkeletonHandoff } from '../../hooks/use-skeleton-handoff';
|
|
73
74
|
|
|
74
75
|
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
75
76
|
const AnimatedG = Animated.createAnimatedComponent(G);
|
|
@@ -81,9 +82,6 @@ const AnimatedG = Animated.createAnimatedComponent(G);
|
|
|
81
82
|
*/
|
|
82
83
|
const STAGGER = 0.4;
|
|
83
84
|
|
|
84
|
-
/** Milliseconds for the placeholder field to dissolve once the data arrives. */
|
|
85
|
-
const SKELETON_FADE = 220;
|
|
86
|
-
|
|
87
85
|
/** Milliseconds for a point to swell as it is selected, and settle as it is not. */
|
|
88
86
|
const SELECT_DURATION = 140;
|
|
89
87
|
|
|
@@ -206,7 +204,7 @@ export interface ScatterChartProps extends ViewProps, ChartAccessibilityProps<Sc
|
|
|
206
204
|
status?: ScatterChartStatus;
|
|
207
205
|
/** Width ÷ height. `1` suits a scatter plot: neither axis is the important one. */
|
|
208
206
|
aspectRatio?: number;
|
|
209
|
-
/** Milliseconds for the reveal on mount. */
|
|
207
|
+
/** Milliseconds for the reveal on mount. Defaults to `650`. */
|
|
210
208
|
animationDuration?: number;
|
|
211
209
|
/** Milliseconds for the axes to settle after the data changes. */
|
|
212
210
|
domainDuration?: number;
|
|
@@ -240,7 +238,7 @@ const ScatterChartRoot = forwardRef<ScatterChartHandle, ScatterChartProps>(
|
|
|
240
238
|
xDataKey = 'x',
|
|
241
239
|
status = 'ready',
|
|
242
240
|
aspectRatio = 1,
|
|
243
|
-
animationDuration =
|
|
241
|
+
animationDuration = 650,
|
|
244
242
|
domainDuration = 500,
|
|
245
243
|
xDomain,
|
|
246
244
|
yDomain,
|
|
@@ -869,27 +867,8 @@ function ScatterChartSkeleton({ count = 24, color }: ScatterChartSkeletonProps)
|
|
|
869
867
|
const { plot, status } = useChart('ScatterChart.Skeleton');
|
|
870
868
|
const token = useCSSVariable('--color-skeleton');
|
|
871
869
|
const fill = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
|
|
872
|
-
const reducedMotion = useReducedMotion();
|
|
873
|
-
|
|
874
|
-
const loading = status === 'loading';
|
|
875
|
-
const fade = useSharedValue(1);
|
|
876
|
-
// Mounted a beat longer than `loading`, so there is something to fade.
|
|
877
|
-
const [mounted, setMounted] = useState(loading);
|
|
878
870
|
|
|
879
|
-
|
|
880
|
-
if (loading) {
|
|
881
|
-
fade.value = 1;
|
|
882
|
-
setMounted(true);
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
if (reducedMotion) {
|
|
886
|
-
setMounted(false);
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
fade.value = withTiming(0, { duration: SKELETON_FADE }, (finished) => {
|
|
890
|
-
if (finished) runOnJS(setMounted)(false);
|
|
891
|
-
});
|
|
892
|
-
}, [loading, reducedMotion, fade]);
|
|
871
|
+
const { mounted, opacity: fade } = useSkeletonHandoff(status === 'loading');
|
|
893
872
|
|
|
894
873
|
const dots = useMemo(() => {
|
|
895
874
|
// A cheap deterministic scatter: two irrational-ish strides that do not
|
|
@@ -58,10 +58,10 @@ import { selectionTick } from '../../utils/haptics';
|
|
|
58
58
|
|
|
59
59
|
const SPRING = { damping: 20, stiffness: 260, mass: 0.6 } as const;
|
|
60
60
|
/** Bar width at the top level, and how much each nested level takes off it. */
|
|
61
|
-
const BAR_WIDTH =
|
|
62
|
-
const BAR_LEVEL_STEP =
|
|
61
|
+
const BAR_WIDTH = 14;
|
|
62
|
+
const BAR_LEVEL_STEP = 3;
|
|
63
63
|
/** How much wider the active bar gets, so position is readable at a glance. */
|
|
64
|
-
const BAR_ACTIVE_EXTRA =
|
|
64
|
+
const BAR_ACTIVE_EXTRA = 10;
|
|
65
65
|
/**
|
|
66
66
|
* How much of that extra the bars either side of the active one keep.
|
|
67
67
|
*
|
|
@@ -83,8 +83,17 @@ const BAR_REST_OPACITY = 0.32;
|
|
|
83
83
|
* that a jump which never lands does not mute the next one.
|
|
84
84
|
*/
|
|
85
85
|
const JUMP_TIMEOUT = 900;
|
|
86
|
+
/**
|
|
87
|
+
* How long the rail stays quiet *after* a jump has landed.
|
|
88
|
+
*
|
|
89
|
+
* A caller may set the value optimistically, before the scroll it triggers has
|
|
90
|
+
* started — so arriving at the target is not proof the journey is over. This
|
|
91
|
+
* covers the tail of the animation, in which the scroller passes through the
|
|
92
|
+
* sections either side of the destination and reports each of them.
|
|
93
|
+
*/
|
|
94
|
+
const JUMP_SETTLE = 260;
|
|
86
95
|
/** Indent per level in the expanded panel. */
|
|
87
|
-
const ITEM_INDENT =
|
|
96
|
+
const ITEM_INDENT = 10;
|
|
88
97
|
/**
|
|
89
98
|
* How wide the panel may grow by default. A row spends at least 40pt on
|
|
90
99
|
* indent, padding and the panel's own border before any text, so a tighter cap
|
|
@@ -92,7 +101,7 @@ const ITEM_INDENT = 12;
|
|
|
92
101
|
*/
|
|
93
102
|
const PANEL_MAX_WIDTH = '78%' as const;
|
|
94
103
|
/** …and a floor, so a rail with one short section still opens a readable panel. */
|
|
95
|
-
const PANEL_MIN_WIDTH =
|
|
104
|
+
const PANEL_MIN_WIDTH = 184;
|
|
96
105
|
|
|
97
106
|
export type SectionRailPlacement = 'left' | 'right';
|
|
98
107
|
export type SectionRailAlign = 'center' | 'top' | 'bottom';
|
|
@@ -232,7 +241,7 @@ function SectionRailRoot({
|
|
|
232
241
|
* the panel lit up a row nobody chose. Nothing between the tap and the
|
|
233
242
|
* arrival is a section the reader went to, so nothing between them ticks.
|
|
234
243
|
*/
|
|
235
|
-
const jumpTo = useRef<string | null>(null);
|
|
244
|
+
const jumpTo = useRef<{ target: string; landed: boolean } | null>(null);
|
|
236
245
|
|
|
237
246
|
const endJump = useCallback(() => {
|
|
238
247
|
jumpTo.current = null;
|
|
@@ -244,7 +253,7 @@ function SectionRailRoot({
|
|
|
244
253
|
|
|
245
254
|
const handleValueChange = useCallback(
|
|
246
255
|
(next: string) => {
|
|
247
|
-
jumpTo.current = next;
|
|
256
|
+
jumpTo.current = { target: next, landed: false };
|
|
248
257
|
if (jumpTimer.current) clearTimeout(jumpTimer.current);
|
|
249
258
|
/*
|
|
250
259
|
* A backstop, not the normal way out. A jump to a section the scroller
|
|
@@ -279,12 +288,23 @@ function SectionRailRoot({
|
|
|
279
288
|
return;
|
|
280
289
|
}
|
|
281
290
|
|
|
282
|
-
const
|
|
283
|
-
if (
|
|
284
|
-
// Still on the way.
|
|
285
|
-
//
|
|
286
|
-
if (value !== target) return;
|
|
287
|
-
|
|
291
|
+
const jump = jumpTo.current;
|
|
292
|
+
if (jump !== null) {
|
|
293
|
+
// Still on the way. Nothing the screen passes through is a section the
|
|
294
|
+
// reader went to, so nothing between the tap and the arrival ticks.
|
|
295
|
+
if (value !== jump.target) return;
|
|
296
|
+
|
|
297
|
+
// Arrived — worth feeling, once. The window stays armed a moment longer
|
|
298
|
+
// rather than closing here, because a caller that sets the value
|
|
299
|
+
// optimistically hands it to us *before* the scroll starts: closing on
|
|
300
|
+
// the first match would disarm the rail for the whole animation, which
|
|
301
|
+
// is the entire journey it was there to cover. Every section the screen
|
|
302
|
+
// then passed lit a row and fired a tick of its own, several of them
|
|
303
|
+
// inside a few hundred milliseconds.
|
|
304
|
+
if (jump.landed) return;
|
|
305
|
+
jump.landed = true;
|
|
306
|
+
if (jumpTimer.current) clearTimeout(jumpTimer.current);
|
|
307
|
+
jumpTimer.current = setTimeout(endJump, JUMP_SETTLE);
|
|
288
308
|
}
|
|
289
309
|
|
|
290
310
|
if (haptics) selectionTick();
|
|
@@ -366,10 +386,10 @@ function SectionRailTrigger({ className, children, ...props }: SectionRailTrigge
|
|
|
366
386
|
accessibilityState={{ expanded: open }}
|
|
367
387
|
onPress={() => setOpen(!open)}
|
|
368
388
|
// Generous padding is the hit target; the bars themselves are hairlines.
|
|
369
|
-
hitSlop={
|
|
389
|
+
hitSlop={10}
|
|
370
390
|
className={cn(
|
|
371
|
-
'gap-
|
|
372
|
-
placement === 'right' ? 'items-end ps-
|
|
391
|
+
'gap-1.5 py-2.5',
|
|
392
|
+
placement === 'right' ? 'items-end ps-5 pe-2' : 'items-start ps-2 pe-5',
|
|
373
393
|
className
|
|
374
394
|
)}
|
|
375
395
|
{...props}
|
|
@@ -549,7 +569,7 @@ function SectionRailPanel({
|
|
|
549
569
|
style={[style, { maxWidth, minWidth: PANEL_MIN_WIDTH }]}
|
|
550
570
|
accessibilityRole="menu"
|
|
551
571
|
className={cn(
|
|
552
|
-
'gap-0.5 rounded-
|
|
572
|
+
'gap-0.5 rounded-xl border border-border bg-popover p-1.5 shadow-lg',
|
|
553
573
|
className
|
|
554
574
|
)}
|
|
555
575
|
{...props}
|
|
@@ -582,9 +602,9 @@ function SectionRailItem({ className, value, level = 0, children }: SectionRailI
|
|
|
582
602
|
onPress={() => onValueChange?.(value)}
|
|
583
603
|
// `paddingStart`, not `paddingLeft`: the indent has to fall on the same
|
|
584
604
|
// side as the bar it belongs to, which is the trailing edge under RTL.
|
|
585
|
-
style={{ paddingStart:
|
|
605
|
+
style={{ paddingStart: 10 + level * ITEM_INDENT }}
|
|
586
606
|
className={cn(
|
|
587
|
-
'rounded-lg py-
|
|
607
|
+
'rounded-lg py-1.5 pe-2 active:bg-accent',
|
|
588
608
|
selected && 'bg-accent',
|
|
589
609
|
className
|
|
590
610
|
)}
|
|
@@ -79,11 +79,45 @@ const splitViewVariants = tv({
|
|
|
79
79
|
root: 'w-full flex-col overflow-hidden',
|
|
80
80
|
top: 'w-full overflow-hidden',
|
|
81
81
|
bottom: 'w-full flex-1 overflow-hidden',
|
|
82
|
-
dragArea: 'w-full items-center justify-center
|
|
83
|
-
handle: '
|
|
82
|
+
dragArea: 'w-full items-center justify-center',
|
|
83
|
+
handle: 'rounded-full',
|
|
84
|
+
},
|
|
85
|
+
variants: {
|
|
86
|
+
/*
|
|
87
|
+
* How the split is drawn. The mechanics are the same either way — this is
|
|
88
|
+
* only what the reader sees, and what they see decides what they think the
|
|
89
|
+
* control is.
|
|
90
|
+
*
|
|
91
|
+
* `panes` gives each half a surface of its own on a recessed ground, with
|
|
92
|
+
* the grip in the gap between them. Two things you can resize, and a
|
|
93
|
+
* handle that belongs to neither.
|
|
94
|
+
*
|
|
95
|
+
* `seam` is a hairline grip on a shared background: no surfaces, no gap,
|
|
96
|
+
* the two halves reading as one region with a line through it. It suits a
|
|
97
|
+
* split inside something that already has a surface — a card, a sheet —
|
|
98
|
+
* where a second one is a box in a box.
|
|
99
|
+
*/
|
|
100
|
+
variant: {
|
|
101
|
+
panes: {
|
|
102
|
+
root: 'bg-inset',
|
|
103
|
+
top: 'rounded-2xl bg-surface',
|
|
104
|
+
bottom: 'rounded-2xl bg-surface',
|
|
105
|
+
dragArea: 'py-3',
|
|
106
|
+
handle: 'h-1.5 w-12 bg-muted-foreground/50',
|
|
107
|
+
},
|
|
108
|
+
seam: {
|
|
109
|
+
dragArea: 'py-2',
|
|
110
|
+
handle: 'h-1 w-10 bg-muted-foreground/30',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
defaultVariants: {
|
|
115
|
+
variant: 'panes',
|
|
84
116
|
},
|
|
85
117
|
});
|
|
86
118
|
|
|
119
|
+
export type SplitViewVariant = 'panes' | 'seam';
|
|
120
|
+
|
|
87
121
|
interface SplitViewContextValue {
|
|
88
122
|
/** The top pane's height in points. Written by drags, read by both panes. */
|
|
89
123
|
topHeight: SharedValue<number>;
|
|
@@ -101,6 +135,7 @@ interface SplitViewContextValue {
|
|
|
101
135
|
snapTo: (index: number) => void;
|
|
102
136
|
measureDragArea: (height: number) => void;
|
|
103
137
|
dragAreaHeight: number;
|
|
138
|
+
variant: SplitViewVariant;
|
|
104
139
|
}
|
|
105
140
|
|
|
106
141
|
const SplitViewContext = createContext<SplitViewContextValue | null>(null);
|
|
@@ -123,6 +158,13 @@ export function useSplitView() {
|
|
|
123
158
|
|
|
124
159
|
export interface SplitViewProps extends ViewProps {
|
|
125
160
|
className?: string;
|
|
161
|
+
/**
|
|
162
|
+
* How the split is drawn. `panes` gives each half its own rounded surface on
|
|
163
|
+
* a recessed ground, with the grip in the gap between them; `seam` is a
|
|
164
|
+
* hairline grip on a shared background, for a split inside something that
|
|
165
|
+
* already has a surface of its own.
|
|
166
|
+
*/
|
|
167
|
+
variant?: SplitViewVariant;
|
|
126
168
|
/**
|
|
127
169
|
* Heights the seam settles on. A number at or below `1` is a fraction of the
|
|
128
170
|
* room the panes share; anything larger is points. Defaults to
|
|
@@ -154,6 +196,7 @@ export interface SplitViewProps extends ViewProps {
|
|
|
154
196
|
|
|
155
197
|
function SplitViewRoot({
|
|
156
198
|
className,
|
|
199
|
+
variant = 'panes',
|
|
157
200
|
snapPoints,
|
|
158
201
|
minHeight,
|
|
159
202
|
maxHeight,
|
|
@@ -167,7 +210,7 @@ function SplitViewRoot({
|
|
|
167
210
|
onLayout,
|
|
168
211
|
...props
|
|
169
212
|
}: SplitViewProps) {
|
|
170
|
-
const { root } = splitViewVariants();
|
|
213
|
+
const { root } = splitViewVariants({ variant });
|
|
171
214
|
const reducedMotion = useReducedMotion();
|
|
172
215
|
const animate = !reducedMotion;
|
|
173
216
|
|
|
@@ -284,6 +327,7 @@ function SplitViewRoot({
|
|
|
284
327
|
snapTo,
|
|
285
328
|
measureDragArea,
|
|
286
329
|
dragAreaHeight,
|
|
330
|
+
variant,
|
|
287
331
|
}),
|
|
288
332
|
[
|
|
289
333
|
topHeight,
|
|
@@ -298,6 +342,7 @@ function SplitViewRoot({
|
|
|
298
342
|
snapTo,
|
|
299
343
|
measureDragArea,
|
|
300
344
|
dragAreaHeight,
|
|
345
|
+
variant,
|
|
301
346
|
]
|
|
302
347
|
);
|
|
303
348
|
|
|
@@ -325,7 +370,7 @@ export interface SplitViewPaneProps extends ViewProps {
|
|
|
325
370
|
*/
|
|
326
371
|
function SplitViewTop({ className, children, style, ...props }: SplitViewPaneProps) {
|
|
327
372
|
const context = useSplitViewContext('SplitView.Top');
|
|
328
|
-
const { top } = splitViewVariants();
|
|
373
|
+
const { top } = splitViewVariants({ variant: context.variant });
|
|
329
374
|
|
|
330
375
|
const animatedStyle = useAnimatedStyle(() => ({
|
|
331
376
|
height: context.topHeight.value,
|
|
@@ -356,8 +401,8 @@ function SplitViewTop({ className, children, style, ...props }: SplitViewPanePro
|
|
|
356
401
|
* every frame of a drag rather than only at rest.
|
|
357
402
|
*/
|
|
358
403
|
function SplitViewBottom({ className, children, ...props }: SplitViewPaneProps) {
|
|
359
|
-
useSplitViewContext('SplitView.Bottom');
|
|
360
|
-
const { bottom } = splitViewVariants();
|
|
404
|
+
const context = useSplitViewContext('SplitView.Bottom');
|
|
405
|
+
const { bottom } = splitViewVariants({ variant: context.variant });
|
|
361
406
|
|
|
362
407
|
return (
|
|
363
408
|
<View className={bottom({ className })} {...props}>
|
|
@@ -390,7 +435,7 @@ function SplitViewDragArea({
|
|
|
390
435
|
...props
|
|
391
436
|
}: SplitViewDragAreaProps) {
|
|
392
437
|
const context = useSplitViewContext('SplitView.DragArea');
|
|
393
|
-
const { dragArea } = splitViewVariants();
|
|
438
|
+
const { dragArea } = splitViewVariants({ variant: context.variant });
|
|
394
439
|
const { topHeight, dragging, points, minPx, maxPx, room, disabled, snapTo } = context;
|
|
395
440
|
|
|
396
441
|
const start = useSharedValue(0);
|
|
@@ -504,7 +549,7 @@ export interface SplitViewHandleProps extends ViewProps {
|
|
|
504
549
|
*/
|
|
505
550
|
function SplitViewHandle({ className, children, style, ...props }: SplitViewHandleProps) {
|
|
506
551
|
const context = useSplitViewContext('SplitView.Handle');
|
|
507
|
-
const { handle } = splitViewVariants();
|
|
552
|
+
const { handle } = splitViewVariants({ variant: context.variant });
|
|
508
553
|
const { animate } = context;
|
|
509
554
|
|
|
510
555
|
const animatedStyle = useAnimatedStyle(() => {
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThemeSelector — light, dark, or whatever the device is set to.
|
|
3
|
+
*
|
|
4
|
+
* Three miniatures of a screen, the chosen one ringed. A row of words would
|
|
5
|
+
* take a third of the space and say the same thing, but "System" is the option
|
|
6
|
+
* people hesitate over, and a picture of a screen split down the middle
|
|
7
|
+
* explains it in less time than a sentence does.
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* <ThemeSelector label="Choose a theme" />
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* ## It reads the theme rather than remembering it
|
|
14
|
+
*
|
|
15
|
+
* There is already one answer to "which theme is this" — Uniwind's — so the
|
|
16
|
+
* selector asks for it instead of keeping a copy that can disagree. Change the
|
|
17
|
+
* theme anywhere else in the app and the ring moves. Pass `value` to show
|
|
18
|
+
* something else, for a settings screen that stages a choice before applying
|
|
19
|
+
* it.
|
|
20
|
+
*
|
|
21
|
+
* ## Light and dark stay inside the family
|
|
22
|
+
*
|
|
23
|
+
* A reader in Moon who picks Light gets Moon's light theme, not the default
|
|
24
|
+
* one. System is the exception, and cannot be otherwise: the device knows only
|
|
25
|
+
* light and dark, so following it means following those two.
|
|
26
|
+
*/
|
|
27
|
+
import { createContext, useCallback, useContext, useMemo, type ReactNode } from 'react';
|
|
28
|
+
import { Pressable, View, type PressableProps, type ViewProps } from 'react-native';
|
|
29
|
+
import { tv } from 'tailwind-variants';
|
|
30
|
+
import { useUniwind } from 'uniwind';
|
|
31
|
+
import { Text, textChildren } from '../../primitives/text';
|
|
32
|
+
import { useTheme, useThemeMode } from '../../theme/use-theme';
|
|
33
|
+
import { ThemePreview, type ThemePreviewVariant } from './theme-preview';
|
|
34
|
+
|
|
35
|
+
export type { ThemePreviewVariant } from './theme-preview';
|
|
36
|
+
|
|
37
|
+
/** What the selector chooses between. */
|
|
38
|
+
export type ThemeSelection = 'system' | 'light' | 'dark';
|
|
39
|
+
|
|
40
|
+
/** How big the miniatures are drawn. */
|
|
41
|
+
export type ThemeSelectorSize = 'sm' | 'md';
|
|
42
|
+
|
|
43
|
+
/** The three, in the order a selector draws them when it is not told. */
|
|
44
|
+
const OPTIONS: readonly ThemeSelection[] = ['system', 'light', 'dark'];
|
|
45
|
+
|
|
46
|
+
/** What each one is called, when the caller does not say. */
|
|
47
|
+
const LABELS: Record<ThemeSelection, string> = {
|
|
48
|
+
system: 'System',
|
|
49
|
+
light: 'Light',
|
|
50
|
+
dark: 'Dark',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** How wide the miniature is drawn, per size. */
|
|
54
|
+
const PREVIEW_WIDTH: Record<ThemeSelectorSize, number> = { sm: 72, md: 88 };
|
|
55
|
+
|
|
56
|
+
const themeSelectorVariants = tv({
|
|
57
|
+
slots: {
|
|
58
|
+
root: 'gap-3',
|
|
59
|
+
legend: 'font-medium',
|
|
60
|
+
options: 'flex-row flex-wrap items-start gap-4',
|
|
61
|
+
option: 'items-center gap-2',
|
|
62
|
+
/*
|
|
63
|
+
* The ring lives on a box around the artwork rather than on the artwork
|
|
64
|
+
* itself. React Native has no outline, so a border on the preview would
|
|
65
|
+
* take a strip out of the drawing every time the option was chosen — the
|
|
66
|
+
* miniature would change size as you picked it.
|
|
67
|
+
*/
|
|
68
|
+
frame: 'overflow-hidden rounded-xl border-2 p-1',
|
|
69
|
+
/*
|
|
70
|
+
* The artwork's own corners, rounded to sit concentrically inside the
|
|
71
|
+
* frame: the frame's radius less the padding between them. A square
|
|
72
|
+
* miniature inside a rounded ring reads as a screenshot somebody pasted in
|
|
73
|
+
* — the two edges have to curve together or neither should.
|
|
74
|
+
*/
|
|
75
|
+
preview: 'overflow-hidden rounded-lg',
|
|
76
|
+
label: 'text-center',
|
|
77
|
+
},
|
|
78
|
+
variants: {
|
|
79
|
+
selected: {
|
|
80
|
+
true: { frame: 'border-primary', label: 'text-foreground' },
|
|
81
|
+
false: { frame: 'border-transparent', label: 'text-muted-foreground' },
|
|
82
|
+
},
|
|
83
|
+
disabled: {
|
|
84
|
+
true: { root: 'opacity-40' },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
defaultVariants: { selected: false },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
interface ThemeSelectorContextValue {
|
|
91
|
+
value: ThemeSelection;
|
|
92
|
+
select: (value: ThemeSelection) => void;
|
|
93
|
+
variant: ThemePreviewVariant;
|
|
94
|
+
size: ThemeSelectorSize;
|
|
95
|
+
disabled: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const ThemeSelectorContext = createContext<ThemeSelectorContextValue | null>(null);
|
|
99
|
+
|
|
100
|
+
function useThemeSelector(part: string): ThemeSelectorContextValue {
|
|
101
|
+
const value = useContext(ThemeSelectorContext);
|
|
102
|
+
if (!value) throw new Error(`${part} must be used inside a <ThemeSelector>.`);
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Which of the three the app is on, read from the theme itself.
|
|
108
|
+
*
|
|
109
|
+
* `hasAdaptiveThemes` is the part that makes this possible: it is true exactly
|
|
110
|
+
* while the theme is following the device, which is the one thing the theme's
|
|
111
|
+
* own name cannot tell you — `system` resolves to `light` or `dark` the moment
|
|
112
|
+
* it is applied, and the two are indistinguishable afterwards.
|
|
113
|
+
*/
|
|
114
|
+
export function useThemeSelection(): ThemeSelection {
|
|
115
|
+
const { hasAdaptiveThemes } = useUniwind();
|
|
116
|
+
const { mode } = useThemeMode();
|
|
117
|
+
return hasAdaptiveThemes ? 'system' : mode;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ThemeSelectorProps extends Omit<ViewProps, 'children'> {
|
|
121
|
+
className?: string;
|
|
122
|
+
/**
|
|
123
|
+
* Show this as chosen instead of whatever the app is actually on. For a
|
|
124
|
+
* settings screen that stages a choice before applying it; left unset, the
|
|
125
|
+
* selector reads the live theme.
|
|
126
|
+
*/
|
|
127
|
+
value?: ThemeSelection;
|
|
128
|
+
/** Fires with the option pressed, before the theme changes. */
|
|
129
|
+
onValueChange?: (value: ThemeSelection) => void;
|
|
130
|
+
/**
|
|
131
|
+
* Apply the choice. On by default — a theme selector that does not select a
|
|
132
|
+
* theme is a radio group. Turn it off to store the choice and apply it
|
|
133
|
+
* yourself, which is what an app that persists the preference wants.
|
|
134
|
+
*/
|
|
135
|
+
applyTheme?: boolean;
|
|
136
|
+
/** The heading above the row. Left out, there is none. */
|
|
137
|
+
label?: string;
|
|
138
|
+
/**
|
|
139
|
+
* Which miniature is drawn. `window` is an app screen with a panel on it;
|
|
140
|
+
* `card` is a framed card with an accent, cut on the diagonal for system.
|
|
141
|
+
*/
|
|
142
|
+
variant?: ThemePreviewVariant;
|
|
143
|
+
/**
|
|
144
|
+
* How wide the miniatures are drawn. `sm` for a settings row that has other
|
|
145
|
+
* things on it; `md` when choosing the theme is what the screen is for.
|
|
146
|
+
*/
|
|
147
|
+
size?: ThemeSelectorSize;
|
|
148
|
+
/** Stop the row being pressed, and dim it to say so. */
|
|
149
|
+
disabled?: boolean;
|
|
150
|
+
/**
|
|
151
|
+
* The options, in the order you want them. Left out, the selector draws
|
|
152
|
+
* system, light and dark in that order — which is the whole component.
|
|
153
|
+
*/
|
|
154
|
+
children?: ReactNode;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function ThemeSelectorRoot({
|
|
158
|
+
className,
|
|
159
|
+
value,
|
|
160
|
+
onValueChange,
|
|
161
|
+
applyTheme = true,
|
|
162
|
+
label,
|
|
163
|
+
variant = 'window',
|
|
164
|
+
size = 'md',
|
|
165
|
+
disabled = false,
|
|
166
|
+
children,
|
|
167
|
+
...props
|
|
168
|
+
}: ThemeSelectorProps) {
|
|
169
|
+
const slots = themeSelectorVariants({ disabled });
|
|
170
|
+
const { setTheme } = useTheme();
|
|
171
|
+
const { setMode } = useThemeMode();
|
|
172
|
+
const live = useThemeSelection();
|
|
173
|
+
const current = value ?? live;
|
|
174
|
+
|
|
175
|
+
const select = useCallback(
|
|
176
|
+
(next: ThemeSelection) => {
|
|
177
|
+
onValueChange?.(next);
|
|
178
|
+
if (!applyTheme) return;
|
|
179
|
+
// `setMode` rather than `setTheme`, so a reader in a named family stays
|
|
180
|
+
// in it. Following the device is the one choice that cannot: the device
|
|
181
|
+
// has no opinion beyond light and dark.
|
|
182
|
+
if (next === 'system') setTheme('system');
|
|
183
|
+
else setMode(next);
|
|
184
|
+
},
|
|
185
|
+
[applyTheme, onValueChange, setTheme, setMode]
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const context = useMemo<ThemeSelectorContextValue>(
|
|
189
|
+
() => ({ value: current, select, variant, size, disabled }),
|
|
190
|
+
[current, select, variant, size, disabled]
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<ThemeSelectorContext.Provider value={context}>
|
|
195
|
+
<View
|
|
196
|
+
accessibilityRole="radiogroup"
|
|
197
|
+
accessibilityLabel={label}
|
|
198
|
+
{...props}
|
|
199
|
+
className={slots.root({ className })}
|
|
200
|
+
>
|
|
201
|
+
{label ? (
|
|
202
|
+
<Text size="sm" className={slots.legend()}>
|
|
203
|
+
{label}
|
|
204
|
+
</Text>
|
|
205
|
+
) : null}
|
|
206
|
+
<View className={slots.options()}>
|
|
207
|
+
{children
|
|
208
|
+
? textChildren(children)
|
|
209
|
+
: OPTIONS.map((option) => (
|
|
210
|
+
<ThemeSelectorOption key={option} value={option} />
|
|
211
|
+
))}
|
|
212
|
+
</View>
|
|
213
|
+
</View>
|
|
214
|
+
</ThemeSelectorContext.Provider>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface ThemeSelectorOptionProps
|
|
219
|
+
extends Omit<PressableProps, 'children' | 'onPress'> {
|
|
220
|
+
className?: string;
|
|
221
|
+
/** Which of the three this option chooses. */
|
|
222
|
+
value: ThemeSelection;
|
|
223
|
+
/** What it is called under the miniature. Defaults to System, Light or Dark. */
|
|
224
|
+
label?: string;
|
|
225
|
+
/** Replaces the drawn miniature. */
|
|
226
|
+
children?: ReactNode;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* One of the three.
|
|
231
|
+
*
|
|
232
|
+
* The whole thing is the target — the miniature and the word under it — because
|
|
233
|
+
* at this size the word is the easier of the two to hit and pressing it should
|
|
234
|
+
* not miss.
|
|
235
|
+
*/
|
|
236
|
+
function ThemeSelectorOption({
|
|
237
|
+
className,
|
|
238
|
+
value,
|
|
239
|
+
label,
|
|
240
|
+
children,
|
|
241
|
+
...props
|
|
242
|
+
}: ThemeSelectorOptionProps) {
|
|
243
|
+
const context = useThemeSelector('ThemeSelector.Option');
|
|
244
|
+
const selected = context.value === value;
|
|
245
|
+
const slots = themeSelectorVariants({ selected, disabled: context.disabled });
|
|
246
|
+
const name = label ?? LABELS[value];
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<Pressable
|
|
250
|
+
accessibilityRole="radio"
|
|
251
|
+
accessibilityLabel={name}
|
|
252
|
+
accessibilityState={{ selected, disabled: context.disabled }}
|
|
253
|
+
disabled={context.disabled}
|
|
254
|
+
{...props}
|
|
255
|
+
onPress={() => context.select(value)}
|
|
256
|
+
className={slots.option({ className })}
|
|
257
|
+
>
|
|
258
|
+
<View className={slots.frame()}>
|
|
259
|
+
<View className={slots.preview()}>
|
|
260
|
+
{children ?? (
|
|
261
|
+
<ThemePreview
|
|
262
|
+
mode={value}
|
|
263
|
+
variant={context.variant}
|
|
264
|
+
width={PREVIEW_WIDTH[context.size]}
|
|
265
|
+
/>
|
|
266
|
+
)}
|
|
267
|
+
</View>
|
|
268
|
+
</View>
|
|
269
|
+
<Text size="sm" className={slots.label()}>
|
|
270
|
+
{name}
|
|
271
|
+
</Text>
|
|
272
|
+
</Pressable>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
ThemeSelectorRoot.displayName = 'ThemeSelector';
|
|
277
|
+
ThemeSelectorOption.displayName = 'ThemeSelector.Option';
|
|
278
|
+
|
|
279
|
+
export const ThemeSelector = Object.assign(ThemeSelectorRoot, {
|
|
280
|
+
Option: ThemeSelectorOption,
|
|
281
|
+
});
|