@butternutbox/pawprint-native 0.10.10 → 0.11.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 +8 -8
- package/CHANGELOG.md +23 -0
- package/dist/index.cjs +284 -165
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -1
- package/dist/index.d.ts +45 -1
- package/dist/index.js +285 -166
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__mocks__/react-native-reanimated.tsx +35 -1
- package/src/__mocks__/react-native.tsx +9 -0
- package/src/components/molecules/Checkbox/Checkbox.tsx +102 -105
- package/src/components/molecules/NumberField/NumberField.tsx +5 -1
- package/src/components/molecules/StackedNotifications/StackedNotifications.stories.tsx +220 -0
- package/src/components/molecules/StackedNotifications/StackedNotifications.test.tsx +202 -0
- package/src/components/molecules/StackedNotifications/StackedNotifications.tsx +247 -0
- package/src/components/molecules/StackedNotifications/index.ts +6 -0
- package/src/components/molecules/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
|
|
3
3
|
const AnimatedView = React.forwardRef<HTMLDivElement, Record<string, unknown>>(
|
|
4
|
-
(
|
|
4
|
+
(
|
|
5
|
+
{
|
|
6
|
+
children,
|
|
7
|
+
style,
|
|
8
|
+
accessibilityRole,
|
|
9
|
+
accessibilityLabel,
|
|
10
|
+
accessibilityElementsHidden,
|
|
11
|
+
importantForAccessibility,
|
|
12
|
+
..._rest
|
|
13
|
+
},
|
|
14
|
+
ref
|
|
15
|
+
) => {
|
|
5
16
|
const flatStyle = Array.isArray(style)
|
|
6
17
|
? Object.assign({}, ...style.filter(Boolean))
|
|
7
18
|
: style
|
|
19
|
+
|
|
20
|
+
const domProps: Record<string, unknown> = {}
|
|
21
|
+
if (accessibilityRole) domProps.role = accessibilityRole
|
|
22
|
+
if (accessibilityLabel) domProps["aria-label"] = accessibilityLabel
|
|
23
|
+
if (
|
|
24
|
+
accessibilityElementsHidden ||
|
|
25
|
+
importantForAccessibility === "no-hide-descendants"
|
|
26
|
+
)
|
|
27
|
+
domProps["aria-hidden"] = true
|
|
28
|
+
|
|
8
29
|
return (
|
|
9
30
|
<div
|
|
10
31
|
ref={ref}
|
|
11
32
|
style={
|
|
12
33
|
typeof flatStyle === "object" ? (flatStyle as object) : undefined
|
|
13
34
|
}
|
|
35
|
+
{...domProps}
|
|
14
36
|
>
|
|
15
37
|
{children as React.ReactNode}
|
|
16
38
|
</div>
|
|
@@ -35,6 +57,7 @@ export const runOnJS =
|
|
|
35
57
|
fn(...args)
|
|
36
58
|
|
|
37
59
|
export const withTiming = <T,>(value: T) => value
|
|
60
|
+
export const withSpring = <T,>(value: T) => value
|
|
38
61
|
|
|
39
62
|
// Animation builder stub — supports chaining .duration() / .easing() / .delay()
|
|
40
63
|
const createBuilder = () => {
|
|
@@ -63,6 +86,17 @@ export const FadeInRight = createBuilder()
|
|
|
63
86
|
export const FadeOutRight = createBuilder()
|
|
64
87
|
export const ZoomIn = createBuilder()
|
|
65
88
|
export const ZoomOut = createBuilder()
|
|
89
|
+
|
|
90
|
+
// Keyframe stub — supports chaining .duration() / .delay()
|
|
91
|
+
export class Keyframe {
|
|
92
|
+
constructor(_definitions: Record<string, unknown>) {}
|
|
93
|
+
duration() {
|
|
94
|
+
return this
|
|
95
|
+
}
|
|
96
|
+
delay() {
|
|
97
|
+
return this
|
|
98
|
+
}
|
|
99
|
+
}
|
|
66
100
|
export class ComplexAnimationBuilder {
|
|
67
101
|
static duration() {
|
|
68
102
|
return new ComplexAnimationBuilder()
|
|
@@ -9,6 +9,8 @@ export const View = React.forwardRef<HTMLDivElement, any>(
|
|
|
9
9
|
accessibilityLabel,
|
|
10
10
|
accessibilityState,
|
|
11
11
|
accessibilityValue,
|
|
12
|
+
accessibilityElementsHidden,
|
|
13
|
+
importantForAccessibility,
|
|
12
14
|
accessible: _accessible,
|
|
13
15
|
children,
|
|
14
16
|
style,
|
|
@@ -24,6 +26,11 @@ export const View = React.forwardRef<HTMLDivElement, any>(
|
|
|
24
26
|
accessibilityRole === "adjustable" ? "slider" : accessibilityRole
|
|
25
27
|
}
|
|
26
28
|
if (accessibilityLabel) domProps["aria-label"] = accessibilityLabel
|
|
29
|
+
if (
|
|
30
|
+
accessibilityElementsHidden ||
|
|
31
|
+
importantForAccessibility === "no-hide-descendants"
|
|
32
|
+
)
|
|
33
|
+
domProps["aria-hidden"] = true
|
|
27
34
|
if (accessibilityState?.disabled) domProps["aria-disabled"] = true
|
|
28
35
|
if (accessibilityState?.selected) domProps["aria-selected"] = true
|
|
29
36
|
if (accessibilityState?.checked !== undefined)
|
|
@@ -90,6 +97,8 @@ export const Pressable = React.forwardRef<HTMLButtonElement, any>(
|
|
|
90
97
|
if (accessibilityLabel) domProps["aria-label"] = accessibilityLabel
|
|
91
98
|
if (accessibilityState?.disabled) domProps["aria-disabled"] = true
|
|
92
99
|
if (accessibilityState?.selected) domProps["aria-selected"] = true
|
|
100
|
+
if (accessibilityState?.checked !== undefined)
|
|
101
|
+
domProps["aria-checked"] = accessibilityState.checked
|
|
93
102
|
if (accessibilityState?.expanded !== undefined)
|
|
94
103
|
domProps["aria-expanded"] = accessibilityState.expanded
|
|
95
104
|
if (accessibilityState?.busy) domProps["aria-busy"] = true
|
|
@@ -2,7 +2,6 @@ import React from "react"
|
|
|
2
2
|
import { View, Pressable, PressableProps } from "react-native"
|
|
3
3
|
import styled from "@emotion/native"
|
|
4
4
|
import { useTheme } from "@emotion/react"
|
|
5
|
-
import * as CheckboxPrimitive from "@rn-primitives/checkbox"
|
|
6
5
|
import { Check } from "@butternutbox/pawprint-icons/core"
|
|
7
6
|
import { Icon } from "../../atoms/Icon"
|
|
8
7
|
import { Typography } from "../../atoms/Typography"
|
|
@@ -158,126 +157,124 @@ export const Checkbox = React.forwardRef<View, CheckboxProps>(
|
|
|
158
157
|
const [internalChecked, setInternalChecked] = React.useState(defaultChecked)
|
|
159
158
|
const isChecked = isControlled ? controlledChecked : internalChecked
|
|
160
159
|
|
|
161
|
-
const
|
|
160
|
+
const handlePress = () => {
|
|
161
|
+
if (disabled) return
|
|
162
|
+
const next = !isChecked
|
|
162
163
|
if (!isControlled) {
|
|
163
|
-
setInternalChecked(
|
|
164
|
+
setInternalChecked(next)
|
|
164
165
|
}
|
|
165
|
-
onCheckedChange?.(
|
|
166
|
+
onCheckedChange?.(next)
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
const isTile = variant === "tile"
|
|
169
170
|
const controlSize = parseTokenValue(checkbox.size.control.default)
|
|
170
171
|
|
|
172
|
+
// A single Pressable owns the tap. The primitive's native build does not
|
|
173
|
+
// forward `asChild`, so wrapping it would nest a second Pressable that
|
|
174
|
+
// swallows the press on device — mirror Radio and drive state directly.
|
|
171
175
|
return (
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
<StyledRootPressable
|
|
177
|
+
ref={ref}
|
|
178
|
+
onPress={handlePress}
|
|
175
179
|
disabled={disabled}
|
|
176
|
-
|
|
180
|
+
accessibilityRole="checkbox"
|
|
181
|
+
accessibilityState={{ checked: isChecked, disabled }}
|
|
182
|
+
rootFlexAlign={isTile ? "center" : "flex-start"}
|
|
183
|
+
rootGap={parseTokenValue(
|
|
184
|
+
isTile ? checkbox.checkboxTile.spacing.gap : checkbox.spacing.gap
|
|
185
|
+
)}
|
|
186
|
+
rootOpacity={
|
|
187
|
+
disabled ? parseFloat(theme.tokens.primitives.opacity["40"]) : 1
|
|
188
|
+
}
|
|
189
|
+
rootPaddingVertical={
|
|
190
|
+
isTile
|
|
191
|
+
? parseTokenValue(checkbox.checkboxTile.spacing.verticalPadding)
|
|
192
|
+
: undefined
|
|
193
|
+
}
|
|
194
|
+
rootPaddingHorizontal={
|
|
195
|
+
isTile
|
|
196
|
+
? parseTokenValue(checkbox.checkboxTile.spacing.horizontalPadding)
|
|
197
|
+
: undefined
|
|
198
|
+
}
|
|
199
|
+
rootMaxWidth={
|
|
200
|
+
isTile
|
|
201
|
+
? parseTokenValue(checkbox.checkboxTile.size.maxWidth)
|
|
202
|
+
: undefined
|
|
203
|
+
}
|
|
204
|
+
rootBgColor={
|
|
205
|
+
isTile
|
|
206
|
+
? isChecked
|
|
207
|
+
? checkbox.checkboxTile.colour.background.selected
|
|
208
|
+
: checkbox.checkboxTile.colour.background.default
|
|
209
|
+
: undefined
|
|
210
|
+
}
|
|
211
|
+
rootBorderWidth={
|
|
212
|
+
isTile
|
|
213
|
+
? parseTokenValue(checkbox.checkboxTile.borderWidth.default)
|
|
214
|
+
: undefined
|
|
215
|
+
}
|
|
216
|
+
rootBorderColor={
|
|
217
|
+
isTile
|
|
218
|
+
? isChecked
|
|
219
|
+
? checkbox.checkboxTile.colour.border.selected
|
|
220
|
+
: checkbox.checkboxTile.colour.border.default
|
|
221
|
+
: undefined
|
|
222
|
+
}
|
|
223
|
+
rootBorderRadius={
|
|
224
|
+
isTile
|
|
225
|
+
? parseTokenValue(checkbox.checkboxTile.borderRadius.default)
|
|
226
|
+
: undefined
|
|
227
|
+
}
|
|
228
|
+
style={typeof style === "function" ? undefined : style}
|
|
229
|
+
{...rest}
|
|
177
230
|
>
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
disabled ? parseFloat(theme.tokens.primitives.opacity["40"]) : 1
|
|
186
|
-
}
|
|
187
|
-
rootPaddingVertical={
|
|
188
|
-
isTile
|
|
189
|
-
? parseTokenValue(checkbox.checkboxTile.spacing.verticalPadding)
|
|
190
|
-
: undefined
|
|
191
|
-
}
|
|
192
|
-
rootPaddingHorizontal={
|
|
193
|
-
isTile
|
|
194
|
-
? parseTokenValue(checkbox.checkboxTile.spacing.horizontalPadding)
|
|
195
|
-
: undefined
|
|
196
|
-
}
|
|
197
|
-
rootMaxWidth={
|
|
198
|
-
isTile
|
|
199
|
-
? parseTokenValue(checkbox.checkboxTile.size.maxWidth)
|
|
200
|
-
: undefined
|
|
201
|
-
}
|
|
202
|
-
rootBgColor={
|
|
203
|
-
isTile
|
|
204
|
-
? isChecked
|
|
205
|
-
? checkbox.checkboxTile.colour.background.selected
|
|
206
|
-
: checkbox.checkboxTile.colour.background.default
|
|
207
|
-
: undefined
|
|
231
|
+
<StyledControl
|
|
232
|
+
controlSize={controlSize}
|
|
233
|
+
controlBorderWidth={parseTokenValue(checkbox.control.border.default)}
|
|
234
|
+
controlBorderColor={
|
|
235
|
+
isChecked
|
|
236
|
+
? checkbox.colour.background.selected
|
|
237
|
+
: checkbox.colour.border.default
|
|
208
238
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
: undefined
|
|
239
|
+
controlBorderRadius={parseTokenValue(dimensions.borderRadius.xs)}
|
|
240
|
+
controlBgColor={
|
|
241
|
+
isChecked ? checkbox.colour.background.selected : "transparent"
|
|
213
242
|
}
|
|
214
|
-
rootBorderColor={
|
|
215
|
-
isTile
|
|
216
|
-
? isChecked
|
|
217
|
-
? checkbox.checkboxTile.colour.border.selected
|
|
218
|
-
: checkbox.checkboxTile.colour.border.default
|
|
219
|
-
: undefined
|
|
220
|
-
}
|
|
221
|
-
rootBorderRadius={
|
|
222
|
-
isTile
|
|
223
|
-
? parseTokenValue(checkbox.checkboxTile.borderRadius.default)
|
|
224
|
-
: undefined
|
|
225
|
-
}
|
|
226
|
-
style={typeof style === "function" ? undefined : style}
|
|
227
|
-
{...rest}
|
|
228
243
|
>
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
controlBorderWidth={parseTokenValue(
|
|
232
|
-
checkbox.control.border.default
|
|
233
|
-
)}
|
|
234
|
-
controlBorderColor={
|
|
235
|
-
isChecked
|
|
236
|
-
? checkbox.colour.background.selected
|
|
237
|
-
: checkbox.colour.border.default
|
|
238
|
-
}
|
|
239
|
-
controlBorderRadius={parseTokenValue(dimensions.borderRadius.xs)}
|
|
240
|
-
controlBgColor={
|
|
241
|
-
isChecked ? checkbox.colour.background.selected : "transparent"
|
|
242
|
-
}
|
|
243
|
-
>
|
|
244
|
-
<CheckboxPrimitive.Indicator>
|
|
245
|
-
<Icon icon={Check} size="xs" colour="alt" />
|
|
246
|
-
</CheckboxPrimitive.Indicator>
|
|
247
|
-
</StyledControl>
|
|
244
|
+
{isChecked && <Icon icon={Check} size="xs" colour="alt" />}
|
|
245
|
+
</StyledControl>
|
|
248
246
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
>
|
|
257
|
-
{label}
|
|
258
|
-
</Typography>
|
|
259
|
-
)}
|
|
260
|
-
{subText && (
|
|
261
|
-
<Typography
|
|
262
|
-
token={checkbox.typography.subText}
|
|
263
|
-
color={checkbox.colour.text.subtext}
|
|
264
|
-
>
|
|
265
|
-
{subText}
|
|
266
|
-
</Typography>
|
|
267
|
-
)}
|
|
268
|
-
</StyledContent>
|
|
269
|
-
|
|
270
|
-
{isTile && illustration && (
|
|
271
|
-
<StyledIllustration
|
|
272
|
-
illustrationSize={parseTokenValue(
|
|
273
|
-
checkbox.checkboxTile.size.illustration.default
|
|
274
|
-
)}
|
|
247
|
+
<StyledContent
|
|
248
|
+
contentGap={parseTokenValue(checkbox.spacing.content.gap)}
|
|
249
|
+
>
|
|
250
|
+
{label && (
|
|
251
|
+
<Typography
|
|
252
|
+
token={checkbox.typography.label}
|
|
253
|
+
color={checkbox.colour.text.title}
|
|
275
254
|
>
|
|
276
|
-
{
|
|
277
|
-
</
|
|
255
|
+
{label}
|
|
256
|
+
</Typography>
|
|
278
257
|
)}
|
|
279
|
-
|
|
280
|
-
|
|
258
|
+
{subText && (
|
|
259
|
+
<Typography
|
|
260
|
+
token={checkbox.typography.subText}
|
|
261
|
+
color={checkbox.colour.text.subtext}
|
|
262
|
+
>
|
|
263
|
+
{subText}
|
|
264
|
+
</Typography>
|
|
265
|
+
)}
|
|
266
|
+
</StyledContent>
|
|
267
|
+
|
|
268
|
+
{isTile && illustration && (
|
|
269
|
+
<StyledIllustration
|
|
270
|
+
illustrationSize={parseTokenValue(
|
|
271
|
+
checkbox.checkboxTile.size.illustration.default
|
|
272
|
+
)}
|
|
273
|
+
>
|
|
274
|
+
{illustration}
|
|
275
|
+
</StyledIllustration>
|
|
276
|
+
)}
|
|
277
|
+
</StyledRootPressable>
|
|
281
278
|
)
|
|
282
279
|
}
|
|
283
280
|
)
|
|
@@ -41,7 +41,7 @@ const StyledRoot = styled(View)<{
|
|
|
41
41
|
fullWidth?: boolean
|
|
42
42
|
}>(({ rootGap, fullWidth }) => ({
|
|
43
43
|
gap: rootGap,
|
|
44
|
-
...(fullWidth
|
|
44
|
+
...(fullWidth ? { width: "100%" } : { alignItems: "center" })
|
|
45
45
|
}))
|
|
46
46
|
|
|
47
47
|
const StyledLabelGroup = styled(View)<{
|
|
@@ -134,6 +134,7 @@ export const NumberField = React.forwardRef<View, NumberFieldProps>(
|
|
|
134
134
|
<Typography
|
|
135
135
|
token={typoTokens.label}
|
|
136
136
|
color={tokens.colour.text.label}
|
|
137
|
+
align="center"
|
|
137
138
|
>
|
|
138
139
|
{label}
|
|
139
140
|
</Typography>
|
|
@@ -142,6 +143,7 @@ export const NumberField = React.forwardRef<View, NumberFieldProps>(
|
|
|
142
143
|
<Typography
|
|
143
144
|
token={typoTokens.smallLabel}
|
|
144
145
|
color={tokens.colour.text.description}
|
|
146
|
+
align="center"
|
|
145
147
|
>
|
|
146
148
|
{smallLabel}
|
|
147
149
|
</Typography>
|
|
@@ -172,6 +174,7 @@ export const NumberField = React.forwardRef<View, NumberFieldProps>(
|
|
|
172
174
|
: tokens.typography.small.smallLabel
|
|
173
175
|
}
|
|
174
176
|
color={tokens.colour.text.description}
|
|
177
|
+
align="center"
|
|
175
178
|
>
|
|
176
179
|
{description}
|
|
177
180
|
</Typography>
|
|
@@ -184,6 +187,7 @@ export const NumberField = React.forwardRef<View, NumberFieldProps>(
|
|
|
184
187
|
: tokens.typography.small.smallLabel
|
|
185
188
|
}
|
|
186
189
|
color={theme.tokens.components.inputs.colour.description.error}
|
|
190
|
+
align="center"
|
|
187
191
|
>
|
|
188
192
|
{error}
|
|
189
193
|
</Typography>
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, StyleSheet } from "react-native"
|
|
3
|
+
import { StackedNotifications } from "./StackedNotifications"
|
|
4
|
+
import type { StackedNotificationItem } from "./StackedNotifications"
|
|
5
|
+
import { Button } from "../../atoms/Button"
|
|
6
|
+
import { Tag } from "../../atoms/Tag"
|
|
7
|
+
import { Typography } from "../../atoms/Typography"
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
title: "Molecules/StackedNotifications",
|
|
11
|
+
component: StackedNotifications,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: "fullscreen"
|
|
14
|
+
},
|
|
15
|
+
argTypes: {
|
|
16
|
+
items: {
|
|
17
|
+
control: "object",
|
|
18
|
+
description:
|
|
19
|
+
"Active notifications, oldest first, newest last. Fully controlled."
|
|
20
|
+
},
|
|
21
|
+
bottomOffset: {
|
|
22
|
+
control: "number",
|
|
23
|
+
description: "Distance from the bottom edge in pixels"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A bounded "screen" so the bottom-pinned stack is visible in the canvas.
|
|
29
|
+
// The component itself is position: absolute; bottom, so it needs a sized,
|
|
30
|
+
// relatively-positioned container to float within.
|
|
31
|
+
const Backdrop = ({
|
|
32
|
+
label = "Screen content — the notification floats over this, pinned to the bottom.",
|
|
33
|
+
children
|
|
34
|
+
}: {
|
|
35
|
+
label?: string
|
|
36
|
+
children?: React.ReactNode
|
|
37
|
+
}) => (
|
|
38
|
+
<View style={styles.backdrop}>
|
|
39
|
+
<Typography variant="body" size="md">
|
|
40
|
+
{label}
|
|
41
|
+
</Typography>
|
|
42
|
+
{children}
|
|
43
|
+
</View>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
// --- Playground ---
|
|
47
|
+
|
|
48
|
+
const PLAYGROUND_ITEMS: StackedNotificationItem[] = [
|
|
49
|
+
{
|
|
50
|
+
id: "chicken",
|
|
51
|
+
type: "info",
|
|
52
|
+
message: "Chicken is a common protein — we'll avoid it in future boxes."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "beef",
|
|
56
|
+
type: "warning",
|
|
57
|
+
message: "Beef can cause reactions in some dogs. We've flagged it."
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
export const Playground = {
|
|
62
|
+
args: {
|
|
63
|
+
items: PLAYGROUND_ITEMS
|
|
64
|
+
},
|
|
65
|
+
render: (args: React.ComponentProps<typeof StackedNotifications>) => (
|
|
66
|
+
<Backdrop>
|
|
67
|
+
<StackedNotifications {...args} />
|
|
68
|
+
</Backdrop>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Stacked (peek)
|
|
73
|
+
|
|
74
|
+
const STACKED_ITEMS: StackedNotificationItem[] = [
|
|
75
|
+
{
|
|
76
|
+
id: "chicken",
|
|
77
|
+
type: "info",
|
|
78
|
+
message: "Chicken is a common protein — we'll avoid it in future boxes."
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "beef",
|
|
82
|
+
type: "warning",
|
|
83
|
+
message: "Beef can cause reactions in some dogs. We've flagged it."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "dairy",
|
|
87
|
+
type: "info",
|
|
88
|
+
message: "Dairy selected — noted for your dog's plan."
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "wheat",
|
|
92
|
+
type: "warning",
|
|
93
|
+
message: "Wheat can trigger reactions. We've flagged it for the vet team."
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
export const Stacked = {
|
|
98
|
+
name: "Stacked (peek)",
|
|
99
|
+
render: () => (
|
|
100
|
+
<Backdrop label="Four notifications are queued, but only the front card plus two peeking behind it are shown.">
|
|
101
|
+
<StackedNotifications items={STACKED_ITEMS} />
|
|
102
|
+
</Backdrop>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Variants ---
|
|
107
|
+
|
|
108
|
+
export const Variants = {
|
|
109
|
+
name: "Info & Warning",
|
|
110
|
+
render: () => (
|
|
111
|
+
<Backdrop>
|
|
112
|
+
<StackedNotifications
|
|
113
|
+
items={[
|
|
114
|
+
{
|
|
115
|
+
id: "info",
|
|
116
|
+
type: "info",
|
|
117
|
+
message: "Info notification — a positive, helpful message."
|
|
118
|
+
}
|
|
119
|
+
]}
|
|
120
|
+
/>
|
|
121
|
+
<View style={styles.spacer} />
|
|
122
|
+
<StackedNotifications
|
|
123
|
+
bottomOffset={120}
|
|
124
|
+
items={[
|
|
125
|
+
{
|
|
126
|
+
id: "warning",
|
|
127
|
+
type: "warning",
|
|
128
|
+
message: "Warning notification — a negative, cautionary message."
|
|
129
|
+
}
|
|
130
|
+
]}
|
|
131
|
+
/>
|
|
132
|
+
</Backdrop>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- Interactive: mimics the allergens wizard screen ---
|
|
137
|
+
|
|
138
|
+
const ALLERGENS = [
|
|
139
|
+
{ id: "chicken", label: "Chicken", type: "info" as const },
|
|
140
|
+
{ id: "beef", label: "Beef", type: "warning" as const },
|
|
141
|
+
{ id: "dairy", label: "Dairy", type: "info" as const },
|
|
142
|
+
{ id: "wheat", label: "Wheat", type: "warning" as const }
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
const MESSAGES: Record<string, string> = {
|
|
146
|
+
chicken: "Chicken selected — we'll leave it out of future boxes.",
|
|
147
|
+
beef: "Beef can trigger reactions. We've flagged it for the vet team.",
|
|
148
|
+
dairy: "Dairy selected — noted for your dog's plan.",
|
|
149
|
+
wheat: "Wheat can trigger reactions. We've flagged it for the vet team."
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const Interactive = {
|
|
153
|
+
name: "Interactive (allergens)",
|
|
154
|
+
render: () => {
|
|
155
|
+
// Ordered list of selected ids — insertion order drives the LIFO stack.
|
|
156
|
+
const [selected, setSelected] = React.useState<string[]>([])
|
|
157
|
+
|
|
158
|
+
const toggle = (id: string) =>
|
|
159
|
+
setSelected((prev) =>
|
|
160
|
+
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
const items: StackedNotificationItem[] = selected.map((id) => {
|
|
164
|
+
const allergen = ALLERGENS.find((a) => a.id === id)
|
|
165
|
+
return {
|
|
166
|
+
id,
|
|
167
|
+
type: allergen?.type ?? "info",
|
|
168
|
+
message: MESSAGES[id]
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<Backdrop label="Tap an allergen to select it (adds a notification). Tap again to deselect (removes it and re-raises on reselect). Use the ✕ to dismiss and reveal the next.">
|
|
174
|
+
<View style={styles.chips}>
|
|
175
|
+
{ALLERGENS.map((allergen) => (
|
|
176
|
+
<Button
|
|
177
|
+
key={allergen.id}
|
|
178
|
+
size="sm"
|
|
179
|
+
variant={selected.includes(allergen.id) ? "filled" : "outlined"}
|
|
180
|
+
onPress={() => toggle(allergen.id)}
|
|
181
|
+
>
|
|
182
|
+
{allergen.label}
|
|
183
|
+
</Button>
|
|
184
|
+
))}
|
|
185
|
+
</View>
|
|
186
|
+
<View style={styles.chips}>
|
|
187
|
+
{selected.map((id) => (
|
|
188
|
+
<Tag key={id}>{id}</Tag>
|
|
189
|
+
))}
|
|
190
|
+
</View>
|
|
191
|
+
<StackedNotifications
|
|
192
|
+
items={items}
|
|
193
|
+
onDismiss={(id) => console.warn("dismissed", id)}
|
|
194
|
+
/>
|
|
195
|
+
</Backdrop>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const styles = StyleSheet.create({
|
|
201
|
+
backdrop: {
|
|
202
|
+
position: "relative",
|
|
203
|
+
height: 360,
|
|
204
|
+
padding: 16,
|
|
205
|
+
gap: 16,
|
|
206
|
+
backgroundColor: "#f4f2ee",
|
|
207
|
+
borderWidth: 1,
|
|
208
|
+
borderColor: "#dcd7cf",
|
|
209
|
+
borderRadius: 8,
|
|
210
|
+
overflow: "hidden"
|
|
211
|
+
},
|
|
212
|
+
chips: {
|
|
213
|
+
flexDirection: "row",
|
|
214
|
+
flexWrap: "wrap",
|
|
215
|
+
gap: 8
|
|
216
|
+
},
|
|
217
|
+
spacer: {
|
|
218
|
+
height: 24
|
|
219
|
+
}
|
|
220
|
+
})
|