@butternutbox/pawprint-native 1.1.2 → 1.2.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/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +12 -0
- package/dist/index.cjs +269 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +92 -5
- package/dist/index.d.ts +92 -5
- package/dist/index.js +270 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/atoms/Illustration/Illustration.tsx +0 -4
- package/src/components/molecules/FloatingActionButton/FloatingActionButton.stories.tsx +304 -0
- package/src/components/molecules/FloatingActionButton/FloatingActionButton.tsx +445 -0
- package/src/components/molecules/FloatingActionButton/index.ts +7 -0
- package/src/components/molecules/index.ts +1 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { LayoutChangeEvent, StyleSheet, View, ViewProps } from "react-native"
|
|
3
|
+
import Svg, {
|
|
4
|
+
Defs,
|
|
5
|
+
LinearGradient as SvgLinearGradient,
|
|
6
|
+
Rect,
|
|
7
|
+
Stop
|
|
8
|
+
} from "react-native-svg"
|
|
9
|
+
import Reanimated, {
|
|
10
|
+
Easing,
|
|
11
|
+
useAnimatedStyle,
|
|
12
|
+
useReducedMotion,
|
|
13
|
+
useSharedValue,
|
|
14
|
+
withDelay,
|
|
15
|
+
withRepeat,
|
|
16
|
+
withSequence,
|
|
17
|
+
withTiming
|
|
18
|
+
} from "react-native-reanimated"
|
|
19
|
+
import styled from "@emotion/native"
|
|
20
|
+
import { useTheme } from "@emotion/react"
|
|
21
|
+
import {
|
|
22
|
+
Close,
|
|
23
|
+
Gift,
|
|
24
|
+
KeyboardArrowRight,
|
|
25
|
+
KeyboardArrowUp
|
|
26
|
+
} from "@butternutbox/pawprint-icons/core"
|
|
27
|
+
import { Icon, type PawprintIcon } from "../../atoms/Icon"
|
|
28
|
+
import { IconButton, type IconButtonVariant } from "../../atoms/IconButton"
|
|
29
|
+
import { Link } from "../../atoms/Link"
|
|
30
|
+
import { Typography } from "../../atoms/Typography"
|
|
31
|
+
|
|
32
|
+
type FloatingActionButtonVariant = "primary" | "secondary"
|
|
33
|
+
type FloatingActionButtonIconButtonIcon = "chevronUp" | "chevronRight" | "close"
|
|
34
|
+
type FloatingActionButtonAnimation = "shine" | "nudge"
|
|
35
|
+
|
|
36
|
+
type FloatingActionButtonBaseProps = {
|
|
37
|
+
variant?: FloatingActionButtonVariant
|
|
38
|
+
icon?: PawprintIcon
|
|
39
|
+
children: React.ReactNode
|
|
40
|
+
linkLabel: string
|
|
41
|
+
animation?: FloatingActionButtonAnimation
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The link is mandatory, so at least one of `linkHref` / `onLinkPress` is
|
|
45
|
+
// required.
|
|
46
|
+
type FloatingActionButtonLinkProps =
|
|
47
|
+
| { linkHref: string; onLinkPress?: () => void }
|
|
48
|
+
| { linkHref?: string; onLinkPress: () => void }
|
|
49
|
+
|
|
50
|
+
type FloatingActionButtonIconButtonProps = {
|
|
51
|
+
iconButtonIcon: FloatingActionButtonIconButtonIcon
|
|
52
|
+
iconButtonLabel: string
|
|
53
|
+
iconButtonVariant?: IconButtonVariant
|
|
54
|
+
onIconButtonPress?: () => void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type FloatingActionButtonOwnProps = FloatingActionButtonBaseProps &
|
|
58
|
+
FloatingActionButtonLinkProps &
|
|
59
|
+
FloatingActionButtonIconButtonProps
|
|
60
|
+
|
|
61
|
+
export type FloatingActionButtonProps = FloatingActionButtonOwnProps &
|
|
62
|
+
Omit<ViewProps, keyof FloatingActionButtonBaseProps | "iconButtonIcon">
|
|
63
|
+
|
|
64
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
65
|
+
|
|
66
|
+
const ICON_BUTTON_CONFIG = {
|
|
67
|
+
chevronUp: { icon: KeyboardArrowUp, defaultVariant: "filled" },
|
|
68
|
+
chevronRight: { icon: KeyboardArrowRight, defaultVariant: "filled" },
|
|
69
|
+
close: { icon: Close, defaultVariant: "text" }
|
|
70
|
+
} as const
|
|
71
|
+
|
|
72
|
+
// TODO: Add token — no motion tokens exist yet
|
|
73
|
+
const ANIMATION_DELAY = 1500
|
|
74
|
+
const ANIMATION_ITERATIONS = 3
|
|
75
|
+
const SHINE_SWEEP = 1100
|
|
76
|
+
const SHINE_REST = 4900
|
|
77
|
+
const SHINE_BAND_RATIO = 0.4
|
|
78
|
+
// TODO: Add token — highlight opacity
|
|
79
|
+
const SHINE_OPACITY = 0.55
|
|
80
|
+
const NUDGE_STEP = 200
|
|
81
|
+
const NUDGE_REST = 3200
|
|
82
|
+
const NUDGE_DISTANCE = 3
|
|
83
|
+
|
|
84
|
+
const ShineOverlay = ({
|
|
85
|
+
radius,
|
|
86
|
+
highlight
|
|
87
|
+
}: {
|
|
88
|
+
radius: number
|
|
89
|
+
highlight: string
|
|
90
|
+
}) => {
|
|
91
|
+
const reducedMotion = useReducedMotion()
|
|
92
|
+
const [width, setWidth] = React.useState(0)
|
|
93
|
+
const progress = useSharedValue(0)
|
|
94
|
+
const bandWidth = width * SHINE_BAND_RATIO
|
|
95
|
+
|
|
96
|
+
React.useEffect(() => {
|
|
97
|
+
if (reducedMotion || !width) return
|
|
98
|
+
progress.value = withDelay(
|
|
99
|
+
ANIMATION_DELAY,
|
|
100
|
+
withRepeat(
|
|
101
|
+
withSequence(
|
|
102
|
+
withTiming(1, {
|
|
103
|
+
duration: SHINE_SWEEP,
|
|
104
|
+
easing: Easing.inOut(Easing.ease)
|
|
105
|
+
}),
|
|
106
|
+
withDelay(SHINE_REST, withTiming(0, { duration: 0 }))
|
|
107
|
+
),
|
|
108
|
+
ANIMATION_ITERATIONS
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
}, [progress, reducedMotion, width])
|
|
112
|
+
|
|
113
|
+
const bandStyle = useAnimatedStyle(() => ({
|
|
114
|
+
transform: [
|
|
115
|
+
{
|
|
116
|
+
translateX:
|
|
117
|
+
-bandWidth * 1.5 + progress.value * (width + bandWidth * 2.5)
|
|
118
|
+
},
|
|
119
|
+
{ skewX: "-20deg" }
|
|
120
|
+
]
|
|
121
|
+
}))
|
|
122
|
+
|
|
123
|
+
if (reducedMotion) return null
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<View
|
|
127
|
+
pointerEvents="none"
|
|
128
|
+
accessible={false}
|
|
129
|
+
importantForAccessibility="no-hide-descendants"
|
|
130
|
+
onLayout={(e: LayoutChangeEvent) => setWidth(e.nativeEvent.layout.width)}
|
|
131
|
+
style={[
|
|
132
|
+
StyleSheet.absoluteFill,
|
|
133
|
+
{ borderRadius: radius, overflow: "hidden" }
|
|
134
|
+
]}
|
|
135
|
+
>
|
|
136
|
+
{width > 0 && (
|
|
137
|
+
<Reanimated.View
|
|
138
|
+
style={[
|
|
139
|
+
{ position: "absolute", top: 0, bottom: 0, width: bandWidth },
|
|
140
|
+
bandStyle
|
|
141
|
+
]}
|
|
142
|
+
>
|
|
143
|
+
<Svg width="100%" height="100%">
|
|
144
|
+
<Defs>
|
|
145
|
+
<SvgLinearGradient id="fabShine" x1="0" y1="0" x2="1" y2="0">
|
|
146
|
+
<Stop offset="0" stopColor={highlight} stopOpacity={0} />
|
|
147
|
+
<Stop
|
|
148
|
+
offset="0.5"
|
|
149
|
+
stopColor={highlight}
|
|
150
|
+
stopOpacity={SHINE_OPACITY}
|
|
151
|
+
/>
|
|
152
|
+
<Stop offset="1" stopColor={highlight} stopOpacity={0} />
|
|
153
|
+
</SvgLinearGradient>
|
|
154
|
+
</Defs>
|
|
155
|
+
<Rect width="100%" height="100%" fill="url(#fabShine)" />
|
|
156
|
+
</Svg>
|
|
157
|
+
</Reanimated.View>
|
|
158
|
+
)}
|
|
159
|
+
</View>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Nudge variants are passed to IconButton as its `icon`, so only the glyph
|
|
164
|
+
// moves (matching web) while the button itself stays put. Created once at module
|
|
165
|
+
// level so the components stay stable across renders.
|
|
166
|
+
const createNudgingIcon = (
|
|
167
|
+
IconComponent: PawprintIcon,
|
|
168
|
+
axis: "translateX" | "translateY",
|
|
169
|
+
direction: 1 | -1
|
|
170
|
+
): PawprintIcon => {
|
|
171
|
+
const NudgingIcon = (props: React.ComponentProps<PawprintIcon>) => {
|
|
172
|
+
const reducedMotion = useReducedMotion()
|
|
173
|
+
const offset = useSharedValue(0)
|
|
174
|
+
|
|
175
|
+
React.useEffect(() => {
|
|
176
|
+
if (reducedMotion) return
|
|
177
|
+
const step = (to: number) =>
|
|
178
|
+
withTiming(to, {
|
|
179
|
+
duration: NUDGE_STEP,
|
|
180
|
+
easing: Easing.inOut(Easing.ease)
|
|
181
|
+
})
|
|
182
|
+
const distance = NUDGE_DISTANCE * direction
|
|
183
|
+
offset.value = withDelay(
|
|
184
|
+
ANIMATION_DELAY,
|
|
185
|
+
withRepeat(
|
|
186
|
+
withSequence(
|
|
187
|
+
step(distance),
|
|
188
|
+
step(0),
|
|
189
|
+
step(distance),
|
|
190
|
+
step(0),
|
|
191
|
+
withDelay(NUDGE_REST, withTiming(0, { duration: 0 }))
|
|
192
|
+
),
|
|
193
|
+
ANIMATION_ITERATIONS
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
}, [offset, reducedMotion])
|
|
197
|
+
|
|
198
|
+
const nudgeStyle = useAnimatedStyle(() =>
|
|
199
|
+
axis === "translateX"
|
|
200
|
+
? { transform: [{ translateX: offset.value }] }
|
|
201
|
+
: { transform: [{ translateY: offset.value }] }
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<Reanimated.View style={nudgeStyle}>
|
|
206
|
+
<IconComponent {...props} />
|
|
207
|
+
</Reanimated.View>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
NudgingIcon.displayName = `Nudging(${IconComponent.displayName ?? "Icon"})`
|
|
211
|
+
return NudgingIcon
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const NUDGE_ICONS = {
|
|
215
|
+
chevronUp: createNudgingIcon(KeyboardArrowUp, "translateY", -1),
|
|
216
|
+
chevronRight: createNudgingIcon(KeyboardArrowRight, "translateX", 1)
|
|
217
|
+
} as const
|
|
218
|
+
|
|
219
|
+
const StyledRoot = styled(View)<{
|
|
220
|
+
rootGap: number
|
|
221
|
+
rootPaddingH: number
|
|
222
|
+
rootPaddingV: number
|
|
223
|
+
rootBgColor: string
|
|
224
|
+
rootBorderWidth: number
|
|
225
|
+
rootBorderColor: string
|
|
226
|
+
rootBorderRadius: number
|
|
227
|
+
rootBoxShadow: string
|
|
228
|
+
}>(
|
|
229
|
+
({
|
|
230
|
+
rootGap,
|
|
231
|
+
rootPaddingH,
|
|
232
|
+
rootPaddingV,
|
|
233
|
+
rootBgColor,
|
|
234
|
+
rootBorderWidth,
|
|
235
|
+
rootBorderColor,
|
|
236
|
+
rootBorderRadius,
|
|
237
|
+
rootBoxShadow
|
|
238
|
+
}) => ({
|
|
239
|
+
flexDirection: "row",
|
|
240
|
+
alignItems: "center",
|
|
241
|
+
width: "100%",
|
|
242
|
+
gap: rootGap,
|
|
243
|
+
paddingHorizontal: rootPaddingH,
|
|
244
|
+
paddingVertical: rootPaddingV,
|
|
245
|
+
backgroundColor: rootBgColor,
|
|
246
|
+
borderWidth: rootBorderWidth,
|
|
247
|
+
borderColor: rootBorderColor,
|
|
248
|
+
borderRadius: rootBorderRadius,
|
|
249
|
+
boxShadow: rootBoxShadow
|
|
250
|
+
})
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
const StyledIconContainer = styled(View)<{
|
|
254
|
+
containerSize: number
|
|
255
|
+
containerRadius: number
|
|
256
|
+
containerBgColor: string
|
|
257
|
+
}>(({ containerSize, containerRadius, containerBgColor }) => ({
|
|
258
|
+
alignItems: "center",
|
|
259
|
+
justifyContent: "center",
|
|
260
|
+
flexShrink: 0,
|
|
261
|
+
width: containerSize,
|
|
262
|
+
height: containerSize,
|
|
263
|
+
borderRadius: containerRadius,
|
|
264
|
+
backgroundColor: containerBgColor
|
|
265
|
+
}))
|
|
266
|
+
|
|
267
|
+
const StyledContent = styled(View)({
|
|
268
|
+
flex: 1,
|
|
269
|
+
minWidth: 0,
|
|
270
|
+
alignItems: "flex-start",
|
|
271
|
+
justifyContent: "center"
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Floating pill that surfaces a short message with a link and an
|
|
276
|
+
* icon button. Used for persistent prompts such as offers and
|
|
277
|
+
* reminders.
|
|
278
|
+
*
|
|
279
|
+
* The component does not position itself — the consumer is responsible for
|
|
280
|
+
* placing it (e.g. `position: "absolute"` at the bottom right) so it can avoid
|
|
281
|
+
* other floating elements on the screen. It fills the width of its container.
|
|
282
|
+
*
|
|
283
|
+
* The icon button is always size `md` and limited to three icons: `chevronUp`,
|
|
284
|
+
* `chevronRight` and `close`. Its style defaults to the Figma pairing —
|
|
285
|
+
* `filled` (primary) for the chevrons and `text` for `close` — and can be
|
|
286
|
+
* overridden with `iconButtonVariant`.
|
|
287
|
+
*
|
|
288
|
+
* Note: Unlike the web version, handlers are `onLinkPress` and
|
|
289
|
+
* `onIconButtonPress` (web: `onLinkClick` / `onIconButtonClick`). The shadow
|
|
290
|
+
* uses `boxShadow`, which requires React Native 0.76+ (New Architecture).
|
|
291
|
+
*
|
|
292
|
+
* @param {"primary" | "secondary"} [variant="primary"] - Colour scheme.
|
|
293
|
+
* `primary` is pink with a white icon container; `secondary` is white with a
|
|
294
|
+
* cream icon container.
|
|
295
|
+
* @param {PawprintIcon} [icon=Gift] - Icon shown in the circular container on
|
|
296
|
+
* the left. Rendered at size `xl` in the primary icon colour.
|
|
297
|
+
* @param {React.ReactNode} children - Message copy. Nest a bold `Typography`
|
|
298
|
+
* to emphasise part of it.
|
|
299
|
+
* @param {string} linkLabel - Link text shown below the message. Use "Close"
|
|
300
|
+
* to dismiss, or e.g. "See more" when the icon button is `close`.
|
|
301
|
+
* @param {string} [linkHref] - **(required without `onLinkPress`)** URL opened
|
|
302
|
+
* via `Linking` when pressed.
|
|
303
|
+
* @param {() => void} [onLinkPress] - **(required without `linkHref`)** Link
|
|
304
|
+
* press handler, e.g. to dismiss, navigate to a screen or open a Modal or
|
|
305
|
+
* Drawer. Takes precedence over `linkHref`.
|
|
306
|
+
* @param {"shine" | "nudge"} [animation] - Optional attention animation, off
|
|
307
|
+
* by default. Runs 3 times after a short delay, then stops, and is disabled
|
|
308
|
+
* when the user prefers reduced motion. `shine` sweeps a soft highlight
|
|
309
|
+
* across the pill; `nudge` moves the chevron towards its direction (no effect
|
|
310
|
+
* with `close`).
|
|
311
|
+
* @param {"chevronUp" | "chevronRight" | "close"} iconButtonIcon - Icon button
|
|
312
|
+
* on the right.
|
|
313
|
+
* @param {"filled" | "outlined" | "text"} [iconButtonVariant] - Icon button
|
|
314
|
+
* style. Defaults to `filled` for `chevronUp` / `chevronRight` and `text`
|
|
315
|
+
* for `close`.
|
|
316
|
+
* @param {string} iconButtonLabel - Accessible label for the icon button.
|
|
317
|
+
* @param {() => void} [onIconButtonPress] - Icon button press handler.
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* ```tsx
|
|
321
|
+
* import { FloatingActionButton, Typography } from "@butternutbox/pawprint-native"
|
|
322
|
+
*
|
|
323
|
+
* <FloatingActionButton
|
|
324
|
+
* variant="primary"
|
|
325
|
+
* linkLabel="Close"
|
|
326
|
+
* onLinkPress={dismiss}
|
|
327
|
+
* iconButtonIcon="chevronUp"
|
|
328
|
+
* iconButtonLabel="View offer"
|
|
329
|
+
* onIconButtonPress={openDrawer}
|
|
330
|
+
* >
|
|
331
|
+
* <Typography size="lg" weight="bold">
|
|
332
|
+
* Free
|
|
333
|
+
* </Typography>{" "}
|
|
334
|
+
* gift in your next box
|
|
335
|
+
* </FloatingActionButton>
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
export const FloatingActionButton = React.forwardRef<
|
|
339
|
+
View,
|
|
340
|
+
FloatingActionButtonProps
|
|
341
|
+
>(
|
|
342
|
+
(
|
|
343
|
+
{
|
|
344
|
+
variant = "primary",
|
|
345
|
+
icon = Gift,
|
|
346
|
+
children,
|
|
347
|
+
linkLabel,
|
|
348
|
+
linkHref,
|
|
349
|
+
onLinkPress,
|
|
350
|
+
animation,
|
|
351
|
+
iconButtonIcon,
|
|
352
|
+
iconButtonLabel,
|
|
353
|
+
iconButtonVariant,
|
|
354
|
+
onIconButtonPress,
|
|
355
|
+
...rest
|
|
356
|
+
},
|
|
357
|
+
ref
|
|
358
|
+
) => {
|
|
359
|
+
const theme = useTheme()
|
|
360
|
+
const { floatingActionButton } = theme.tokens.components
|
|
361
|
+
const { colour, dimensions } = theme.tokens.semantics
|
|
362
|
+
const { shadow } = colour
|
|
363
|
+
const iconButton = ICON_BUTTON_CONFIG[iconButtonIcon]
|
|
364
|
+
|
|
365
|
+
const iconButtonElement = (
|
|
366
|
+
<IconButton
|
|
367
|
+
icon={
|
|
368
|
+
animation === "nudge" && iconButtonIcon !== "close"
|
|
369
|
+
? NUDGE_ICONS[iconButtonIcon]
|
|
370
|
+
: iconButton.icon
|
|
371
|
+
}
|
|
372
|
+
variant={iconButtonVariant ?? iconButton.defaultVariant}
|
|
373
|
+
size="md"
|
|
374
|
+
aria-label={iconButtonLabel}
|
|
375
|
+
onPress={onIconButtonPress}
|
|
376
|
+
/>
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<StyledRoot
|
|
381
|
+
ref={ref}
|
|
382
|
+
rootGap={parseTokenValue(floatingActionButton.spacing.gap)}
|
|
383
|
+
rootPaddingH={parseTokenValue(
|
|
384
|
+
floatingActionButton.spacing.horizontalPadding
|
|
385
|
+
)}
|
|
386
|
+
rootPaddingV={parseTokenValue(
|
|
387
|
+
floatingActionButton.spacing.verticalPadding
|
|
388
|
+
)}
|
|
389
|
+
rootBgColor={floatingActionButton.colour.background[variant]}
|
|
390
|
+
rootBorderWidth={parseTokenValue(
|
|
391
|
+
floatingActionButton.borderWidth.default
|
|
392
|
+
)}
|
|
393
|
+
rootBorderColor={floatingActionButton.colour.border[variant]}
|
|
394
|
+
rootBorderRadius={parseTokenValue(
|
|
395
|
+
floatingActionButton.borderRadius.default
|
|
396
|
+
)}
|
|
397
|
+
rootBoxShadow={`${shadow.md.offsetX} ${shadow.md.offsetY} ${shadow.md.blur} ${shadow.md.spread} ${shadow.md.color}`}
|
|
398
|
+
{...rest}
|
|
399
|
+
>
|
|
400
|
+
<StyledIconContainer
|
|
401
|
+
containerSize={parseTokenValue(
|
|
402
|
+
theme.tokens.primitives.dimension.sizing["44"]
|
|
403
|
+
)}
|
|
404
|
+
containerRadius={parseTokenValue(dimensions.borderRadius.round)}
|
|
405
|
+
containerBgColor={
|
|
406
|
+
variant === "secondary"
|
|
407
|
+
? colour.background.container.secondary
|
|
408
|
+
: colour.background.container.default
|
|
409
|
+
}
|
|
410
|
+
>
|
|
411
|
+
<Icon icon={icon} size="xl" colour="primary" />
|
|
412
|
+
</StyledIconContainer>
|
|
413
|
+
<StyledContent>
|
|
414
|
+
<Typography variant="body" size="lg" weight="medium">
|
|
415
|
+
{children}
|
|
416
|
+
</Typography>
|
|
417
|
+
<Link
|
|
418
|
+
href={linkHref}
|
|
419
|
+
onPress={onLinkPress}
|
|
420
|
+
size="md"
|
|
421
|
+
weight="medium"
|
|
422
|
+
{...(!linkHref && { accessibilityRole: "button" as const })}
|
|
423
|
+
>
|
|
424
|
+
{linkLabel}
|
|
425
|
+
</Link>
|
|
426
|
+
</StyledContent>
|
|
427
|
+
{iconButtonElement}
|
|
428
|
+
{animation === "shine" && (
|
|
429
|
+
<ShineOverlay
|
|
430
|
+
radius={parseTokenValue(floatingActionButton.borderRadius.default)}
|
|
431
|
+
highlight={theme.tokens.primitives.colour.system.neutral["1"]}
|
|
432
|
+
/>
|
|
433
|
+
)}
|
|
434
|
+
</StyledRoot>
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
FloatingActionButton.displayName = "FloatingActionButton"
|
|
440
|
+
|
|
441
|
+
export type {
|
|
442
|
+
FloatingActionButtonVariant,
|
|
443
|
+
FloatingActionButtonIconButtonIcon,
|
|
444
|
+
FloatingActionButtonAnimation
|
|
445
|
+
}
|