panelui-native 0.46.0 → 0.49.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 -1
- package/lib/module/components/accordion/index.js +32 -4
- package/lib/module/components/accordion/index.js.map +1 -1
- package/lib/module/components/button/index.js +83 -15
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/button-group/index.js +186 -0
- package/lib/module/components/button-group/index.js.map +1 -0
- package/lib/module/components/color-picker/index.js +110 -1
- package/lib/module/components/color-picker/index.js.map +1 -1
- package/lib/module/components/date-time-picker/index.js +23 -5
- package/lib/module/components/date-time-picker/index.js.map +1 -1
- package/lib/module/components/fab/index.js +514 -0
- package/lib/module/components/fab/index.js.map +1 -0
- package/lib/module/components/markdown-editor/index.js +406 -0
- package/lib/module/components/markdown-editor/index.js.map +1 -0
- package/lib/module/components/markdown-editor/markdown-transforms.js +243 -0
- package/lib/module/components/markdown-editor/markdown-transforms.js.map +1 -0
- package/lib/module/components/questionnaire/index.js +1312 -0
- package/lib/module/components/questionnaire/index.js.map +1 -0
- package/lib/module/components/tabs/index.js +94 -18
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/time-picker/index.js +34 -6
- package/lib/module/components/time-picker/index.js.map +1 -1
- package/lib/module/components/tree/index.js +500 -0
- package/lib/module/components/tree/index.js.map +1 -0
- package/lib/module/icons/index.js +217 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +6 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts +21 -0
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +21 -0
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button-group/index.d.ts +212 -0
- package/lib/typescript/src/components/button-group/index.d.ts.map +1 -0
- package/lib/typescript/src/components/color-picker/index.d.ts +82 -1
- package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/date-time-picker/index.d.ts +7 -1
- package/lib/typescript/src/components/date-time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/fab/index.d.ts +285 -0
- package/lib/typescript/src/components/fab/index.d.ts.map +1 -0
- package/lib/typescript/src/components/markdown-editor/index.d.ts +102 -0
- package/lib/typescript/src/components/markdown-editor/index.d.ts.map +1 -0
- package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts +76 -0
- package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts.map +1 -0
- package/lib/typescript/src/components/questionnaire/index.d.ts +336 -0
- package/lib/typescript/src/components/questionnaire/index.d.ts.map +1 -0
- package/lib/typescript/src/components/tabs/index.d.ts +9 -0
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/index.d.ts +19 -1
- package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tree/index.d.ts +125 -0
- package/lib/typescript/src/components/tree/index.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +22 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +8 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/accordion/index.tsx +48 -6
- package/src/components/button/index.tsx +97 -15
- package/src/components/button-group/index.tsx +199 -0
- package/src/components/color-picker/index.tsx +140 -3
- package/src/components/date-time-picker/index.tsx +39 -3
- package/src/components/fab/index.tsx +583 -0
- package/src/components/markdown-editor/index.tsx +526 -0
- package/src/components/markdown-editor/markdown-transforms.ts +228 -0
- package/src/components/questionnaire/index.tsx +1615 -0
- package/src/components/tabs/index.tsx +100 -28
- package/src/components/time-picker/index.tsx +42 -6
- package/src/components/tree/index.tsx +564 -0
- package/src/icons/index.tsx +154 -0
- package/src/index.ts +77 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef, type ReactNode } from 'react';
|
|
1
|
+
import { createContext, forwardRef, useContext, type ReactNode } from 'react';
|
|
2
2
|
import { View } from 'react-native';
|
|
3
3
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
4
4
|
import { useCSSVariable } from 'uniwind';
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type AnimatedPressableProps,
|
|
8
8
|
} from '../../primitives/animated-pressable';
|
|
9
9
|
import { Text, textChildren } from '../../primitives/text';
|
|
10
|
+
import { cn } from '../../utils/cn';
|
|
10
11
|
import { IconColorProvider } from '../../icons';
|
|
11
12
|
import { getNativeUI, getSwiftUIModifiers } from '../../native';
|
|
12
13
|
import { Spinner } from '../spinner';
|
|
@@ -72,6 +73,65 @@ const buttonVariants = tv({
|
|
|
72
73
|
|
|
73
74
|
type ButtonVariantProps = VariantProps<typeof buttonVariants>;
|
|
74
75
|
|
|
76
|
+
/** How a button looks. */
|
|
77
|
+
export type ButtonVariant = NonNullable<ButtonVariantProps['variant']>;
|
|
78
|
+
/** How big a button is. `icon` is a square. */
|
|
79
|
+
export type ButtonSize = NonNullable<ButtonVariantProps['size']>;
|
|
80
|
+
|
|
81
|
+
/* -------------------------------------------------------------------------- *
|
|
82
|
+
* Grouping
|
|
83
|
+
*
|
|
84
|
+
* A button reads this; `ButtonGroup` writes it. The context lives here rather
|
|
85
|
+
* than beside the group because the button is the consumer, and a component
|
|
86
|
+
* should not have to import the thing that contains it to find out that it is
|
|
87
|
+
* contained — which would also make the two import each other.
|
|
88
|
+
* -------------------------------------------------------------------------- */
|
|
89
|
+
|
|
90
|
+
export interface ButtonGroupContextValue {
|
|
91
|
+
/** Fills in for a button that did not choose one. */
|
|
92
|
+
variant?: ButtonVariant;
|
|
93
|
+
/** Fills in for a button that did not choose one. */
|
|
94
|
+
size?: ButtonSize;
|
|
95
|
+
/** The buttons are joined into one shape, so each drops its own. */
|
|
96
|
+
attached: boolean;
|
|
97
|
+
/** Segments share the row equally. */
|
|
98
|
+
fullWidth: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ButtonGroupContext = createContext<ButtonGroupContextValue | null>(null);
|
|
102
|
+
|
|
103
|
+
/** Announces to every button below it that it is part of one control. */
|
|
104
|
+
export function ButtonGroupProvider({
|
|
105
|
+
value,
|
|
106
|
+
children,
|
|
107
|
+
}: {
|
|
108
|
+
value: ButtonGroupContextValue;
|
|
109
|
+
children: ReactNode;
|
|
110
|
+
}) {
|
|
111
|
+
return <ButtonGroupContext.Provider value={value}>{children}</ButtonGroupContext.Provider>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The enclosing group, if there is one. `null` for a button standing alone. */
|
|
115
|
+
export function useButtonGroup(): ButtonGroupContextValue | null {
|
|
116
|
+
return useContext(ButtonGroupContext);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* What an attached button gives up so the group can draw the shape once.
|
|
121
|
+
*
|
|
122
|
+
* The radius and the shadow go because the group owns both — a rounded,
|
|
123
|
+
* shadowed segment inside a rounded, shadowed container is two shapes where
|
|
124
|
+
* there should be one. The border stays but turns transparent rather than
|
|
125
|
+
* being removed: it is holding a pixel of the button's height, and dropping it
|
|
126
|
+
* would make an `outline` segment a hair shorter than a `ghost` one beside it.
|
|
127
|
+
*
|
|
128
|
+
* The press feedback changes too. A button on its own shrinks slightly when
|
|
129
|
+
* pressed, which inside a joined run would pull the segment away from its
|
|
130
|
+
* neighbours and show the container through the gap. A background is the same
|
|
131
|
+
* signal without the movement.
|
|
132
|
+
*/
|
|
133
|
+
const ATTACHED = 'rounded-none border-transparent shadow-none active:bg-accent';
|
|
134
|
+
|
|
75
135
|
const SPINNER_SIZE = { sm: 'sm', md: 'sm', lg: 'md', icon: 'sm' } as const;
|
|
76
136
|
|
|
77
137
|
/**
|
|
@@ -240,10 +300,28 @@ export const Button = forwardRef<View, ButtonProps>(
|
|
|
240
300
|
) => {
|
|
241
301
|
const isDisabled = disabled || loading;
|
|
242
302
|
const nativeUI = native ? getNativeUI() : null;
|
|
303
|
+
|
|
304
|
+
/*
|
|
305
|
+
* A group fills in what a button did not say for itself, and never
|
|
306
|
+
* overrides what it did: a run of buttons should look like one control
|
|
307
|
+
* without every segment repeating the same two props, and the odd segment
|
|
308
|
+
* that wants to stand out — the selected one, the destructive one — has to
|
|
309
|
+
* be able to say so.
|
|
310
|
+
*
|
|
311
|
+
* `native` is deliberately outside all of this. The platform draws that
|
|
312
|
+
* button, so it has no border, radius or shadow for a group to take over,
|
|
313
|
+
* and joining several of them would produce a row of platform buttons with
|
|
314
|
+
* a border drawn around it rather than a segmented control.
|
|
315
|
+
*/
|
|
316
|
+
const group = useButtonGroup();
|
|
317
|
+
const attached = !native && group?.attached === true;
|
|
318
|
+
const resolvedVariant = variant ?? group?.variant;
|
|
319
|
+
const resolvedSize = size ?? group?.size;
|
|
320
|
+
|
|
243
321
|
const { root, label, spinner } = buttonVariants({
|
|
244
|
-
variant,
|
|
245
|
-
size,
|
|
246
|
-
fullWidth,
|
|
322
|
+
variant: resolvedVariant,
|
|
323
|
+
size: resolvedSize,
|
|
324
|
+
fullWidth: fullWidth ?? (attached && group?.fullWidth),
|
|
247
325
|
disabled: isDisabled,
|
|
248
326
|
});
|
|
249
327
|
|
|
@@ -252,11 +330,11 @@ export const Button = forwardRef<View, ButtonProps>(
|
|
|
252
330
|
// hardcode a hex that is wrong in one theme or the other.
|
|
253
331
|
const themedColor = useCSSVariable(
|
|
254
332
|
CONTENT_COLOR_VAR[
|
|
255
|
-
|
|
333
|
+
resolvedVariant === 'destructive' ? 'primary' : (resolvedVariant ?? 'primary')
|
|
256
334
|
]
|
|
257
335
|
);
|
|
258
336
|
const contentColor =
|
|
259
|
-
|
|
337
|
+
resolvedVariant === 'destructive'
|
|
260
338
|
? '#ffffff'
|
|
261
339
|
: typeof themedColor === 'string'
|
|
262
340
|
? themedColor
|
|
@@ -265,7 +343,7 @@ export const Button = forwardRef<View, ButtonProps>(
|
|
|
265
343
|
if (nativeUI) {
|
|
266
344
|
const { Host, Button: NativeButton, RNHostView } = nativeUI;
|
|
267
345
|
const isStringLabel = typeof children === 'string';
|
|
268
|
-
const prominent =
|
|
346
|
+
const prominent = resolvedVariant === 'primary' || resolvedVariant === 'destructive';
|
|
269
347
|
|
|
270
348
|
/*
|
|
271
349
|
* Looks the portable props cannot ask for.
|
|
@@ -285,8 +363,10 @@ export const Button = forwardRef<View, ButtonProps>(
|
|
|
285
363
|
const nativeModifiers = swiftUI
|
|
286
364
|
? [
|
|
287
365
|
glass ? swiftUI.buttonStyle(prominent ? 'glassProminent' : 'glass') : null,
|
|
288
|
-
|
|
289
|
-
isStringLabel
|
|
366
|
+
resolvedSize === 'icon' ? swiftUI.buttonBorderShape('circle') : null,
|
|
367
|
+
isStringLabel
|
|
368
|
+
? swiftUI.controlSize(NATIVE_CONTROL_SIZE[resolvedSize ?? 'md'])
|
|
369
|
+
: null,
|
|
290
370
|
].filter(Boolean)
|
|
291
371
|
: [];
|
|
292
372
|
|
|
@@ -303,7 +383,7 @@ export const Button = forwardRef<View, ButtonProps>(
|
|
|
303
383
|
<Host matchContents>
|
|
304
384
|
<NativeButton
|
|
305
385
|
label={isStringLabel ? children : undefined}
|
|
306
|
-
variant={NATIVE_VARIANT[
|
|
386
|
+
variant={NATIVE_VARIANT[resolvedVariant ?? 'primary']}
|
|
307
387
|
disabled={isDisabled}
|
|
308
388
|
/*
|
|
309
389
|
* A square for an icon button, a height for a labelled one — and
|
|
@@ -316,9 +396,9 @@ export const Button = forwardRef<View, ButtonProps>(
|
|
|
316
396
|
* the result.
|
|
317
397
|
*/
|
|
318
398
|
style={
|
|
319
|
-
|
|
399
|
+
resolvedSize === 'icon'
|
|
320
400
|
? { width: NATIVE_ICON_FRAME, height: NATIVE_ICON_FRAME }
|
|
321
|
-
: { height: NATIVE_HEIGHT[
|
|
401
|
+
: { height: NATIVE_HEIGHT[resolvedSize ?? 'md'] }
|
|
322
402
|
}
|
|
323
403
|
modifiers={nativeModifiers.length ? nativeModifiers : undefined}
|
|
324
404
|
onPress={props.onPress}
|
|
@@ -337,7 +417,7 @@ export const Button = forwardRef<View, ButtonProps>(
|
|
|
337
417
|
sits on ends up off to one side of a circle that is not. */}
|
|
338
418
|
<View
|
|
339
419
|
style={
|
|
340
|
-
|
|
420
|
+
resolvedSize === 'icon'
|
|
341
421
|
? {
|
|
342
422
|
padding: NATIVE_ICON_PADDING,
|
|
343
423
|
alignItems: 'center',
|
|
@@ -366,11 +446,13 @@ export const Button = forwardRef<View, ButtonProps>(
|
|
|
366
446
|
accessibilityRole="button"
|
|
367
447
|
accessibilityState={{ disabled: isDisabled, busy: loading }}
|
|
368
448
|
disabled={isDisabled}
|
|
369
|
-
|
|
449
|
+
// Before the spread, so a caller can still ask for the scale back.
|
|
450
|
+
pressScale={attached ? 1 : undefined}
|
|
451
|
+
className={root({ className: cn(attached && ATTACHED, className) })}
|
|
370
452
|
{...props}
|
|
371
453
|
>
|
|
372
454
|
{loading ? (
|
|
373
|
-
<Spinner size={SPINNER_SIZE[
|
|
455
|
+
<Spinner size={SPINNER_SIZE[resolvedSize ?? 'md']} className={spinner()} />
|
|
374
456
|
) : (
|
|
375
457
|
startContent
|
|
376
458
|
)}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ButtonGroup — several buttons drawn as one control.
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* <ButtonGroup>
|
|
6
|
+
* <Button startContent={<ListChecksIcon size={16} />}>Tasks</Button>
|
|
7
|
+
* <Button startContent={<CalendarIcon size={16} />}>Agenda</Button>
|
|
8
|
+
* <Button startContent={<ChartIcon size={16} />}>Board</Button>
|
|
9
|
+
* </ButtonGroup>
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* The buttons stay buttons. Anything a `Button` does — an icon, a badge, a
|
|
13
|
+
* loading state, a disabled segment, opening a Popover — it still does inside a
|
|
14
|
+
* group, because the group is a container rather than a component that takes a
|
|
15
|
+
* list of items and renders them for you. A list-of-items API has to grow a
|
|
16
|
+
* prop for every one of those things; this one has none of them and can do all
|
|
17
|
+
* of them.
|
|
18
|
+
*
|
|
19
|
+
* ## Why the container draws the border
|
|
20
|
+
*
|
|
21
|
+
* A joined run could be built by giving the first and last segments their
|
|
22
|
+
* corners and squaring the ones between, then collapsing every shared edge with
|
|
23
|
+
* a negative margin. That works on the web and is a stack of off-by-one
|
|
24
|
+
* problems on a phone: the hairlines land on different fractions of a pixel per
|
|
25
|
+
* device, and a run that wraps has no first or last segment any more.
|
|
26
|
+
*
|
|
27
|
+
* So the group draws the shape once — one border, one radius, one shadow,
|
|
28
|
+
* clipped — and the buttons inside it draw none of their own. The dividers are
|
|
29
|
+
* real one-pixel views the group puts between its children, which is also why
|
|
30
|
+
* they are always exactly one pixel and always in the same place.
|
|
31
|
+
*
|
|
32
|
+
* ## What it passes down
|
|
33
|
+
*
|
|
34
|
+
* `variant` and `size` fill in for a button that did not choose its own, so a
|
|
35
|
+
* run of six does not repeat the same two props six times. A segment that wants
|
|
36
|
+
* to stand out — the selected one, the destructive one — sets its own and wins.
|
|
37
|
+
*
|
|
38
|
+
* ```tsx
|
|
39
|
+
* <ButtonGroup size="sm">
|
|
40
|
+
* <Button>Day</Button>
|
|
41
|
+
* <Button variant="secondary">Week</Button> {/* the selected one *\/}
|
|
42
|
+
* <Button>Month</Button>
|
|
43
|
+
* </ButtonGroup>
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* ## Not a selection control
|
|
47
|
+
*
|
|
48
|
+
* This joins buttons; what they mean is yours. For a control that owns which
|
|
49
|
+
* one is on, reach for `ToggleButtonGroup`, and for switching between panels of
|
|
50
|
+
* content reach for `Tabs` — a segmented run of buttons that swaps a screen is
|
|
51
|
+
* navigation, and navigation should say so to a screen reader.
|
|
52
|
+
*/
|
|
53
|
+
import { Children, Fragment, forwardRef, useMemo, type ReactNode } from 'react';
|
|
54
|
+
import { View, type ViewProps } from 'react-native';
|
|
55
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
56
|
+
import {
|
|
57
|
+
ButtonGroupProvider,
|
|
58
|
+
type ButtonGroupContextValue,
|
|
59
|
+
type ButtonSize,
|
|
60
|
+
type ButtonVariant,
|
|
61
|
+
} from '../button';
|
|
62
|
+
|
|
63
|
+
/** Which way the run reads. */
|
|
64
|
+
export type ButtonGroupOrientation = 'horizontal' | 'vertical';
|
|
65
|
+
|
|
66
|
+
const buttonGroupVariants = tv({
|
|
67
|
+
slots: {
|
|
68
|
+
root: 'items-stretch',
|
|
69
|
+
divider: 'bg-border',
|
|
70
|
+
},
|
|
71
|
+
variants: {
|
|
72
|
+
orientation: {
|
|
73
|
+
horizontal: { root: 'flex-row', divider: 'w-px' },
|
|
74
|
+
vertical: { root: 'flex-col', divider: 'h-px' },
|
|
75
|
+
},
|
|
76
|
+
attached: {
|
|
77
|
+
// `overflow-hidden` is what squares the inner corners and rounds the
|
|
78
|
+
// outer ones without any segment knowing which it is.
|
|
79
|
+
true: { root: 'overflow-hidden border border-input bg-popover shadow-sm' },
|
|
80
|
+
false: { root: 'gap-2' },
|
|
81
|
+
},
|
|
82
|
+
/* Matched to the height the buttons will be, since a radius that suited a
|
|
83
|
+
36pt run reads as a rectangle around a 48pt one. */
|
|
84
|
+
size: {
|
|
85
|
+
sm: { root: 'rounded-lg' },
|
|
86
|
+
md: { root: 'rounded-xl' },
|
|
87
|
+
lg: { root: 'rounded-xl' },
|
|
88
|
+
icon: { root: 'rounded-xl' },
|
|
89
|
+
},
|
|
90
|
+
fullWidth: {
|
|
91
|
+
true: { root: 'w-full' },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
compoundVariants: [
|
|
95
|
+
// Nothing is being joined, so the radius belongs to each button instead.
|
|
96
|
+
{ attached: false, class: { root: 'rounded-none' } },
|
|
97
|
+
],
|
|
98
|
+
defaultVariants: {
|
|
99
|
+
orientation: 'horizontal',
|
|
100
|
+
attached: true,
|
|
101
|
+
size: 'md',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
type ButtonGroupVariantProps = VariantProps<typeof buttonGroupVariants>;
|
|
106
|
+
|
|
107
|
+
export interface ButtonGroupProps
|
|
108
|
+
extends ViewProps,
|
|
109
|
+
Omit<ButtonGroupVariantProps, 'size' | 'attached'> {
|
|
110
|
+
className?: string;
|
|
111
|
+
/** Which way the run reads. Vertical is the toolbar down the side of a canvas. */
|
|
112
|
+
orientation?: ButtonGroupOrientation;
|
|
113
|
+
/** Fills in for any button that did not choose its own. */
|
|
114
|
+
variant?: ButtonVariant;
|
|
115
|
+
/** Fills in for any button that did not choose its own, and sets the radius. */
|
|
116
|
+
size?: ButtonSize;
|
|
117
|
+
/**
|
|
118
|
+
* Draw the run as one joined shape.
|
|
119
|
+
*
|
|
120
|
+
* On by default — that is what a group is. Turn it off for a plain row of
|
|
121
|
+
* separate buttons that should still share a variant and a size, which is a
|
|
122
|
+
* toolbar rather than a segmented control.
|
|
123
|
+
*/
|
|
124
|
+
attached?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Span the container, with the segments sharing it equally.
|
|
127
|
+
*
|
|
128
|
+
* Equally, not by content: a row of segments at their natural widths is a row
|
|
129
|
+
* whose divisions move when the labels change, and a picker whose halves are
|
|
130
|
+
* different sizes reads as though one of them matters more.
|
|
131
|
+
*/
|
|
132
|
+
fullWidth?: boolean;
|
|
133
|
+
children?: ReactNode;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const ButtonGroupRoot = forwardRef<View, ButtonGroupProps>(
|
|
137
|
+
(
|
|
138
|
+
{
|
|
139
|
+
className,
|
|
140
|
+
orientation = 'horizontal',
|
|
141
|
+
variant,
|
|
142
|
+
size = 'md',
|
|
143
|
+
attached = true,
|
|
144
|
+
fullWidth = false,
|
|
145
|
+
children,
|
|
146
|
+
...props
|
|
147
|
+
},
|
|
148
|
+
ref
|
|
149
|
+
) => {
|
|
150
|
+
const { root, divider } = buttonGroupVariants({
|
|
151
|
+
orientation,
|
|
152
|
+
attached,
|
|
153
|
+
size,
|
|
154
|
+
fullWidth,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const context = useMemo<ButtonGroupContextValue>(
|
|
158
|
+
() => ({ variant, size, attached, fullWidth }),
|
|
159
|
+
[variant, size, attached, fullWidth]
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
/*
|
|
163
|
+
* `Children.toArray` rather than the raw children: it drops the nulls a
|
|
164
|
+
* conditional segment leaves behind and flattens fragments, so a run built
|
|
165
|
+
* by a `map` or by `{canEdit && <Button/>}` gets its dividers in the right
|
|
166
|
+
* places instead of one before a segment that is not there.
|
|
167
|
+
*/
|
|
168
|
+
const items = Children.toArray(children);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<ButtonGroupProvider value={context}>
|
|
172
|
+
<View
|
|
173
|
+
ref={ref}
|
|
174
|
+
// A run of related actions is a toolbar; a screen reader announcing
|
|
175
|
+
// the group is what tells someone the buttons in it belong together.
|
|
176
|
+
accessibilityRole="toolbar"
|
|
177
|
+
className={root({ className })}
|
|
178
|
+
{...props}
|
|
179
|
+
>
|
|
180
|
+
{items.map((child, index) => (
|
|
181
|
+
// Index keys: these fragments have no identity of their own, and
|
|
182
|
+
// the children keep whatever keys they were already given.
|
|
183
|
+
<Fragment key={index}>
|
|
184
|
+
{index > 0 && attached ? <View className={divider()} /> : null}
|
|
185
|
+
{/* The share of the row is taken by a wrapper rather than by the
|
|
186
|
+
button, so a segment that is a Popover trigger — a button
|
|
187
|
+
inside something else — still gets one. */}
|
|
188
|
+
{fullWidth ? <View className="flex-1">{child}</View> : child}
|
|
189
|
+
</Fragment>
|
|
190
|
+
))}
|
|
191
|
+
</View>
|
|
192
|
+
</ButtonGroupProvider>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
ButtonGroupRoot.displayName = 'ButtonGroup';
|
|
198
|
+
|
|
199
|
+
export const ButtonGroup = ButtonGroupRoot;
|
|
@@ -28,6 +28,28 @@
|
|
|
28
28
|
* a control with no alpha is one that has no `ColorPicker.Alpha`, not one with
|
|
29
29
|
* a prop turned off.
|
|
30
30
|
*
|
|
31
|
+
* ## Folding it away
|
|
32
|
+
*
|
|
33
|
+
* A picker is a page's worth of controls in service of one value, and that
|
|
34
|
+
* value is read far more often than it is changed — so `presentation` puts the
|
|
35
|
+
* controls behind the row that reads it out:
|
|
36
|
+
*
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <ColorPicker value={accent} onValueChange={setAccent} presentation="popover">
|
|
39
|
+
* <ColorPicker.Trigger>
|
|
40
|
+
* <ColorPicker.Field label="Accent" />
|
|
41
|
+
* </ColorPicker.Trigger>
|
|
42
|
+
* <ColorPicker.Content>
|
|
43
|
+
* <ColorPicker.Area />
|
|
44
|
+
* <ColorPicker.Hue />
|
|
45
|
+
* </ColorPicker.Content>
|
|
46
|
+
* </ColorPicker>
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* `Content` re-provides the picker's context around what it holds, because the
|
|
50
|
+
* panel is portalled above the rest of the screen and is therefore no longer
|
|
51
|
+
* below the picker in the tree by the time the parts inside it go looking.
|
|
52
|
+
*
|
|
31
53
|
* Works controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`).
|
|
32
54
|
*/
|
|
33
55
|
import {
|
|
@@ -40,6 +62,7 @@ import {
|
|
|
40
62
|
useMemo,
|
|
41
63
|
useRef,
|
|
42
64
|
useState,
|
|
65
|
+
type ReactElement,
|
|
43
66
|
type ReactNode,
|
|
44
67
|
} from 'react';
|
|
45
68
|
import {
|
|
@@ -81,7 +104,9 @@ import {
|
|
|
81
104
|
type ColorFormat,
|
|
82
105
|
type HsvaColor,
|
|
83
106
|
} from '../../utils/color';
|
|
107
|
+
import { cn } from '../../utils/cn';
|
|
84
108
|
import { selectionTick } from '../../utils/haptics';
|
|
109
|
+
import { Popover, type PopoverContentProps } from '../popover';
|
|
85
110
|
|
|
86
111
|
/** Settles a thumb that was moved by something other than a finger. */
|
|
87
112
|
const TIMING = { duration: 140 } as const;
|
|
@@ -206,6 +231,16 @@ function useColorPicker(part: string) {
|
|
|
206
231
|
/** Where a picker starts when it is handed a value it cannot read. */
|
|
207
232
|
const FALLBACK: HsvaColor = { h: 0, s: 1, v: 1, a: 1 };
|
|
208
233
|
|
|
234
|
+
/** How the controls get onto the screen. */
|
|
235
|
+
export type ColorPickerPresentation = 'inline' | 'popover' | 'bottom-sheet';
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Floor for the panel's width when it takes the trigger's. A swatch row is a
|
|
239
|
+
* narrow trigger, and a square you drag on is not a control that survives being
|
|
240
|
+
* squeezed to match one.
|
|
241
|
+
*/
|
|
242
|
+
const CONTENT_MIN_WIDTH = 268;
|
|
243
|
+
|
|
209
244
|
export interface ColorPickerProps extends Omit<ColorPickerVariantProps, 'disabled'> {
|
|
210
245
|
className?: string;
|
|
211
246
|
/** Controlled colour. Leave unset and pass `defaultValue` to run uncontrolled. */
|
|
@@ -233,6 +268,19 @@ export interface ColorPickerProps extends Omit<ColorPickerVariantProps, 'disable
|
|
|
233
268
|
* buzz proportional to speed.
|
|
234
269
|
*/
|
|
235
270
|
haptics?: boolean;
|
|
271
|
+
/**
|
|
272
|
+
* How the controls get onto the screen.
|
|
273
|
+
*
|
|
274
|
+
* `inline` stacks them where they are written, and is the default. The other
|
|
275
|
+
* two put them behind a `ColorPicker.Trigger` and draw them in a
|
|
276
|
+
* `ColorPicker.Content` — which is the arrangement a colour usually wants,
|
|
277
|
+
* since a picker is a page's worth of controls in service of one value that
|
|
278
|
+
* is looked at far more often than it is changed.
|
|
279
|
+
*/
|
|
280
|
+
presentation?: ColorPickerPresentation;
|
|
281
|
+
/** Controlled open state of the panel. Ignored by `inline`. */
|
|
282
|
+
open?: boolean;
|
|
283
|
+
onOpenChange?: (open: boolean) => void;
|
|
236
284
|
/** The parts, in the order they should stack. */
|
|
237
285
|
children: ReactNode;
|
|
238
286
|
}
|
|
@@ -249,6 +297,9 @@ const ColorPickerRoot = forwardRef<View, ColorPickerProps>(
|
|
|
249
297
|
disabled = false,
|
|
250
298
|
haptics = false,
|
|
251
299
|
size = 'md',
|
|
300
|
+
presentation = 'inline',
|
|
301
|
+
open,
|
|
302
|
+
onOpenChange,
|
|
252
303
|
children,
|
|
253
304
|
},
|
|
254
305
|
ref
|
|
@@ -319,6 +370,16 @@ const ColorPickerRoot = forwardRef<View, ColorPickerProps>(
|
|
|
319
370
|
[hue, saturation, brightness, opacity, emit, format, disabled, haptics, size]
|
|
320
371
|
);
|
|
321
372
|
|
|
373
|
+
if (presentation !== 'inline') {
|
|
374
|
+
return (
|
|
375
|
+
<ColorPickerContext.Provider value={context}>
|
|
376
|
+
<Popover open={open} onOpenChange={onOpenChange} presentation={presentation}>
|
|
377
|
+
{children}
|
|
378
|
+
</Popover>
|
|
379
|
+
</ColorPickerContext.Provider>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
322
383
|
return (
|
|
323
384
|
<ColorPickerContext.Provider value={context}>
|
|
324
385
|
<View ref={ref} className={slots.root({ className })} collapsable={false}>
|
|
@@ -331,6 +392,64 @@ const ColorPickerRoot = forwardRef<View, ColorPickerProps>(
|
|
|
331
392
|
|
|
332
393
|
ColorPickerRoot.displayName = 'ColorPicker';
|
|
333
394
|
|
|
395
|
+
/* ------------------------------------------------------------------ *
|
|
396
|
+
* Trigger and Content — the picker folded away behind the value.
|
|
397
|
+
* ------------------------------------------------------------------ */
|
|
398
|
+
|
|
399
|
+
export interface ColorPickerTriggerProps {
|
|
400
|
+
/** One element, cloned with an `onPress` that opens the panel. */
|
|
401
|
+
children: ReactElement<{ onPress?: (...args: unknown[]) => void }>;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* What you press to open the picker. `ColorPicker.Field` is the obvious child —
|
|
406
|
+
* it already reads out the colour it would let you change — but anything that
|
|
407
|
+
* takes an `onPress` will do.
|
|
408
|
+
*/
|
|
409
|
+
function ColorPickerTrigger({ children }: ColorPickerTriggerProps) {
|
|
410
|
+
useColorPicker('ColorPicker.Trigger');
|
|
411
|
+
return <Popover.Trigger>{children}</Popover.Trigger>;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
ColorPickerTrigger.displayName = 'ColorPicker.Trigger';
|
|
415
|
+
|
|
416
|
+
export interface ColorPickerContentProps extends PopoverContentProps {}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* The panel the controls are drawn in.
|
|
420
|
+
*
|
|
421
|
+
* It re-provides the picker's context around its children, and has to: the
|
|
422
|
+
* panel is rendered through a portal, above everything else on the screen, so
|
|
423
|
+
* by the time the parts inside it look for the picker they are no longer
|
|
424
|
+
* anywhere below it in the tree. Without this the first `ColorPicker.Area`
|
|
425
|
+
* inside a popover would throw.
|
|
426
|
+
*
|
|
427
|
+
* Defaults to the trigger's width, floored, so a panel opened from a labelled
|
|
428
|
+
* strip lines up under it rather than announcing itself as a different object.
|
|
429
|
+
*/
|
|
430
|
+
function ColorPickerContent({
|
|
431
|
+
className,
|
|
432
|
+
width = 'trigger',
|
|
433
|
+
minWidth = CONTENT_MIN_WIDTH,
|
|
434
|
+
children,
|
|
435
|
+
...props
|
|
436
|
+
}: ColorPickerContentProps) {
|
|
437
|
+
const ctx = useColorPicker('ColorPicker.Content');
|
|
438
|
+
|
|
439
|
+
return (
|
|
440
|
+
<Popover.Content
|
|
441
|
+
width={width}
|
|
442
|
+
minWidth={minWidth}
|
|
443
|
+
className={cn('gap-3', className)}
|
|
444
|
+
{...props}
|
|
445
|
+
>
|
|
446
|
+
<ColorPickerContext.Provider value={ctx}>{children}</ColorPickerContext.Provider>
|
|
447
|
+
</Popover.Content>
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
ColorPickerContent.displayName = 'ColorPicker.Content';
|
|
452
|
+
|
|
334
453
|
/* ------------------------------------------------------------------ *
|
|
335
454
|
* The checkerboard behind anything translucent.
|
|
336
455
|
* ------------------------------------------------------------------ */
|
|
@@ -1083,6 +1202,14 @@ export interface ColorPickerFieldProps {
|
|
|
1083
1202
|
showValue?: boolean;
|
|
1084
1203
|
/** Extra classes for the swatch. */
|
|
1085
1204
|
swatchClassName?: string;
|
|
1205
|
+
/**
|
|
1206
|
+
* Makes the strip pressable, and a button to a screen reader.
|
|
1207
|
+
*
|
|
1208
|
+
* Mostly you do not pass this yourself: `ColorPicker.Trigger` clones the
|
|
1209
|
+
* strip with one, which is what turns the row into the thing that opens the
|
|
1210
|
+
* picker.
|
|
1211
|
+
*/
|
|
1212
|
+
onPress?: (...args: unknown[]) => void;
|
|
1086
1213
|
/** Anything to put after the swatch — a copy button, a reset. */
|
|
1087
1214
|
children?: ReactNode;
|
|
1088
1215
|
}
|
|
@@ -1095,7 +1222,7 @@ export interface ColorPickerFieldProps {
|
|
|
1095
1222
|
* A drag through a hundred frames of the same hex costs one render.
|
|
1096
1223
|
*/
|
|
1097
1224
|
const ColorPickerField = forwardRef<View, ColorPickerFieldProps>(
|
|
1098
|
-
({ className, label, showValue = true, swatchClassName, children }, ref) => {
|
|
1225
|
+
({ className, label, showValue = true, swatchClassName, onPress, children }, ref) => {
|
|
1099
1226
|
const ctx = useColorPicker('ColorPicker.Field');
|
|
1100
1227
|
const slots = colorPickerVariants({ size: ctx.size });
|
|
1101
1228
|
const diameter = FIELD_SWATCH[ctx.size];
|
|
@@ -1145,13 +1272,21 @@ const ColorPickerField = forwardRef<View, ColorPickerFieldProps>(
|
|
|
1145
1272
|
),
|
|
1146
1273
|
}));
|
|
1147
1274
|
|
|
1275
|
+
// Pressable only when it has somewhere to go. A row that highlights under
|
|
1276
|
+
// a finger and then does nothing is a worse lie than a row that does not
|
|
1277
|
+
// react at all.
|
|
1278
|
+
const Row = onPress ? Pressable : View;
|
|
1279
|
+
|
|
1148
1280
|
return (
|
|
1149
|
-
<
|
|
1281
|
+
<Row
|
|
1150
1282
|
ref={ref}
|
|
1283
|
+
onPress={onPress}
|
|
1284
|
+
disabled={onPress ? ctx.disabled : undefined}
|
|
1151
1285
|
className={slots.field({ className })}
|
|
1152
1286
|
// One thing being read out, not three: a label, a value and a swatch
|
|
1153
1287
|
// announced separately are three stops that each say a third of it.
|
|
1154
1288
|
accessible
|
|
1289
|
+
accessibilityRole={onPress ? 'button' : undefined}
|
|
1155
1290
|
accessibilityLabel={label ? `${label}, ${printed}` : printed}
|
|
1156
1291
|
>
|
|
1157
1292
|
{label ? <Text className={slots.fieldLabel()}>{label}</Text> : null}
|
|
@@ -1168,7 +1303,7 @@ const ColorPickerField = forwardRef<View, ColorPickerFieldProps>(
|
|
|
1168
1303
|
<Animated.View style={[StyleSheet.absoluteFill, fillStyle]} />
|
|
1169
1304
|
</View>
|
|
1170
1305
|
{children}
|
|
1171
|
-
</
|
|
1306
|
+
</Row>
|
|
1172
1307
|
);
|
|
1173
1308
|
}
|
|
1174
1309
|
);
|
|
@@ -1606,6 +1741,8 @@ const ColorPickerWheel = forwardRef<View, ColorPickerWheelProps>(
|
|
|
1606
1741
|
ColorPickerWheel.displayName = 'ColorPicker.Wheel';
|
|
1607
1742
|
|
|
1608
1743
|
export const ColorPicker = Object.assign(ColorPickerRoot, {
|
|
1744
|
+
Trigger: ColorPickerTrigger,
|
|
1745
|
+
Content: ColorPickerContent,
|
|
1609
1746
|
Field: ColorPickerField,
|
|
1610
1747
|
Area: ColorPickerArea,
|
|
1611
1748
|
Wheel: ColorPickerWheel,
|