@krapwoo/ds-viewer 0.1.1 → 0.3.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 +182 -78
- package/dist/cli/agentsFile.js +31 -0
- package/dist/cli/config.js +26 -1
- package/dist/cli/dev.js +50 -5
- package/dist/cli/doctor.js +492 -0
- package/dist/cli/explain.js +151 -0
- package/dist/cli/githubAction.js +68 -0
- package/dist/cli/glob.js +8 -0
- package/dist/cli/init.js +152 -18
- package/dist/cli/main.js +115 -4
- package/dist/cli/pageIndex.js +27 -1
- package/dist/cli/props.js +170 -21
- package/dist/cli/staticPage.js +356 -0
- package/dist/cli/sync.js +28 -5
- package/dist/cli/workspace.js +49 -21
- package/dist/native/catalog/comparison.js +283 -0
- package/dist/native/catalog/tokens.js +119 -0
- package/dist/native/catalog/types.js +1 -0
- package/native/catalog/CatalogShell.tsx +6 -1
- package/native/catalog/CatalogSidebar.tsx +98 -5
- package/native/catalog/ComparisonGrid.tsx +9 -7
- package/native/catalog/SpacingScaleGallery.tsx +1 -1
- package/native/catalog/TokenRow.tsx +3 -2
- package/native/catalog/catalogNavigation.ts +1 -1
- package/native/catalog/comparison.ts +63 -5
- package/native/catalog/index.ts +8 -6
- package/native/catalog/logoLayout.ts +12 -0
- package/native/catalog/pageApi.ts +21 -3
- package/native/catalog/tokens.ts +5 -0
- package/native/catalog/types.ts +9 -6
- package/package.json +12 -6
- package/starter-kit/WHEN_TO_USE.md +124 -0
- package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +23 -0
- package/starter-kit/components/AnimatedChevron/AnimatedChevron.tsx +46 -0
- package/starter-kit/components/AnimatedChevron/index.ts +2 -0
- package/starter-kit/components/Avatar/Avatar.catalog.tsx +54 -0
- package/starter-kit/components/Avatar/Avatar.tsx +74 -0
- package/starter-kit/components/Avatar/index.ts +2 -0
- package/starter-kit/components/Badge/Badge.catalog.tsx +88 -0
- package/starter-kit/components/Badge/Badge.tsx +56 -0
- package/starter-kit/components/Badge/Badge.types.ts +18 -0
- package/starter-kit/components/Badge/index.ts +2 -0
- package/starter-kit/components/Banner/Banner.catalog.tsx +87 -0
- package/starter-kit/components/Banner/Banner.tsx +257 -0
- package/starter-kit/components/Banner/Banner.types.ts +38 -0
- package/starter-kit/components/Banner/index.ts +2 -0
- package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +77 -0
- package/starter-kit/components/BottomSheet/BottomSheet.tsx +151 -0
- package/starter-kit/components/BottomSheet/index.ts +2 -0
- package/starter-kit/components/Button/Button.catalog.tsx +99 -0
- package/starter-kit/components/Button/Button.tsx +173 -0
- package/starter-kit/components/Button/Button.types.ts +52 -0
- package/starter-kit/components/Button/index.ts +2 -0
- package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +39 -0
- package/starter-kit/components/ButtonGroup/ButtonGroup.tsx +84 -0
- package/starter-kit/components/ButtonGroup/index.ts +2 -0
- package/starter-kit/components/Card/Card.catalog.tsx +68 -0
- package/starter-kit/components/Card/Card.tsx +133 -0
- package/starter-kit/components/Card/index.ts +2 -0
- package/starter-kit/components/Checkbox/Checkbox.catalog.tsx +28 -0
- package/starter-kit/components/Checkbox/Checkbox.tsx +104 -0
- package/starter-kit/components/Checkbox/index.ts +2 -0
- package/starter-kit/components/Dialog/Dialog.catalog.tsx +41 -0
- package/starter-kit/components/Dialog/Dialog.tsx +82 -0
- package/starter-kit/components/Dialog/index.ts +2 -0
- package/starter-kit/components/Divider/Divider.catalog.tsx +35 -0
- package/starter-kit/components/Divider/Divider.tsx +20 -0
- package/starter-kit/components/Divider/index.ts +2 -0
- package/starter-kit/components/Dock/Dock.catalog.tsx +71 -0
- package/starter-kit/components/Dock/Dock.tsx +58 -0
- package/starter-kit/components/Dock/index.ts +2 -0
- package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +69 -0
- package/starter-kit/components/Dropdown/Dropdown.tsx +162 -0
- package/starter-kit/components/Dropdown/index.ts +2 -0
- package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +67 -0
- package/starter-kit/components/EmptyState/EmptyState.tsx +88 -0
- package/starter-kit/components/EmptyState/index.ts +2 -0
- package/starter-kit/components/FieldContainer/FieldContainer.catalog.tsx +76 -0
- package/starter-kit/components/FieldContainer/FieldContainer.tsx +99 -0
- package/starter-kit/components/FieldContainer/FieldContainer.types.ts +27 -0
- package/starter-kit/components/FieldContainer/index.ts +2 -0
- package/starter-kit/components/InputClearButton/InputClearButton.catalog.tsx +18 -0
- package/starter-kit/components/InputClearButton/InputClearButton.tsx +63 -0
- package/starter-kit/components/InputClearButton/index.ts +2 -0
- package/starter-kit/components/InputField/InputField.catalog.tsx +76 -0
- package/starter-kit/components/InputField/InputField.tsx +418 -0
- package/starter-kit/components/InputField/index.ts +2 -0
- package/starter-kit/components/List/List.catalog.tsx +30 -0
- package/starter-kit/components/List/List.tsx +36 -0
- package/starter-kit/components/List/index.ts +2 -0
- package/starter-kit/components/ListItem/ListItem.catalog.tsx +89 -0
- package/starter-kit/components/ListItem/ListItem.tsx +165 -0
- package/starter-kit/components/ListItem/index.ts +2 -0
- package/starter-kit/components/Loading/Loading.catalog.tsx +34 -0
- package/starter-kit/components/Loading/Loading.tsx +121 -0
- package/starter-kit/components/Loading/index.ts +2 -0
- package/starter-kit/components/Pill/Pill.catalog.tsx +79 -0
- package/starter-kit/components/Pill/Pill.tsx +181 -0
- package/starter-kit/components/Pill/Pill.types.ts +30 -0
- package/starter-kit/components/Pill/index.ts +2 -0
- package/starter-kit/components/PillRow/PillRow.catalog.tsx +91 -0
- package/starter-kit/components/PillRow/PillRow.tsx +140 -0
- package/starter-kit/components/PillRow/PillRow.types.ts +24 -0
- package/starter-kit/components/PillRow/index.ts +2 -0
- package/starter-kit/components/ProgressDots/ProgressDots.catalog.tsx +22 -0
- package/starter-kit/components/ProgressDots/ProgressDots.tsx +42 -0
- package/starter-kit/components/ProgressDots/index.ts +2 -0
- package/starter-kit/components/Radio/Radio.catalog.tsx +38 -0
- package/starter-kit/components/Radio/Radio.tsx +114 -0
- package/starter-kit/components/Radio/index.ts +2 -0
- package/starter-kit/components/Radio/usePressScale.ts +33 -0
- package/starter-kit/components/SearchField/SearchField.catalog.tsx +28 -0
- package/starter-kit/components/SearchField/SearchField.tsx +98 -0
- package/starter-kit/components/SearchField/index.ts +2 -0
- package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +72 -0
- package/starter-kit/components/SectionHeader/SectionHeader.tsx +139 -0
- package/starter-kit/components/SectionHeader/index.ts +2 -0
- package/starter-kit/components/SegmentedToggle/SegmentedToggle.catalog.tsx +31 -0
- package/starter-kit/components/SegmentedToggle/SegmentedToggle.tsx +163 -0
- package/starter-kit/components/SegmentedToggle/index.ts +2 -0
- package/starter-kit/components/SegmentedToggle/useSlideAnim.ts +29 -0
- package/starter-kit/components/Shimmer/Breathing.catalog.tsx +29 -0
- package/starter-kit/components/Shimmer/Shimmer.catalog.tsx +89 -0
- package/starter-kit/components/Shimmer/Shimmer.tsx +158 -0
- package/starter-kit/components/Shimmer/Shimmer.types.ts +27 -0
- package/starter-kit/components/Shimmer/SkeletonGroup.catalog.tsx +39 -0
- package/starter-kit/components/Shimmer/index.ts +2 -0
- package/starter-kit/components/Surface/Surface.catalog.tsx +44 -0
- package/starter-kit/components/Surface/Surface.tsx +45 -0
- package/starter-kit/components/Surface/index.ts +2 -0
- package/starter-kit/components/Switch/Switch.catalog.tsx +33 -0
- package/starter-kit/components/Switch/Switch.tsx +129 -0
- package/starter-kit/components/Switch/index.ts +2 -0
- package/starter-kit/components/TextArea/TextArea.catalog.tsx +29 -0
- package/starter-kit/components/TextArea/TextArea.tsx +99 -0
- package/starter-kit/components/TextArea/index.ts +2 -0
- package/starter-kit/components/Toast/Toast.catalog.tsx +61 -0
- package/starter-kit/components/Toast/Toast.tsx +144 -0
- package/starter-kit/components/Toast/index.ts +2 -0
- package/starter-kit/components/Tooltip/Tooltip.catalog.tsx +121 -0
- package/starter-kit/components/Tooltip/Tooltip.tsx +195 -0
- package/starter-kit/components/Tooltip/index.ts +2 -0
- package/starter-kit/components/TopNav/TopNav.catalog.tsx +71 -0
- package/starter-kit/components/TopNav/TopNav.tsx +77 -0
- package/starter-kit/components/TopNav/index.ts +2 -0
- package/starter-kit/components/UnderlineTabs/UnderlineTabs.catalog.tsx +30 -0
- package/starter-kit/components/UnderlineTabs/UnderlineTabs.tsx +156 -0
- package/starter-kit/components/UnderlineTabs/index.ts +2 -0
- package/starter-kit/components/index.ts +36 -0
- package/starter-kit/components/statusColors.ts +27 -0
- package/starter-kit/icons/Icon.native.tsx +90 -0
- package/starter-kit/icons/index.ts +10 -0
- package/starter-kit/icons/paths.ts +623 -0
- package/starter-kit/icons/types.ts +69 -0
- package/starter-kit/pages/Colors.catalog.tsx +55 -0
- package/starter-kit/pages/Font.catalog.tsx +45 -0
- package/starter-kit/pages/Icons.catalog.tsx +51 -0
- package/starter-kit/pages/Manifest.catalog.tsx +24 -0
- package/starter-kit/pages/Motion.catalog.tsx +101 -0
- package/starter-kit/pages/Radius.catalog.tsx +35 -0
- package/starter-kit/pages/ReportIssue.catalog.tsx +148 -0
- package/starter-kit/pages/SavedTrips.catalog.tsx +247 -0
- package/starter-kit/pages/Shadow.catalog.tsx +34 -0
- package/starter-kit/pages/Spacing.catalog.tsx +12 -0
- package/starter-kit/pages/TripPlannerForm.catalog.tsx +48 -0
- package/starter-kit/pages/Typography.catalog.tsx +17 -0
- package/starter-kit/tokens/index.ts +50 -0
- package/starter-kit/tokens/motion.ts +90 -0
- package/starter-kit/tokens/palette.ts +111 -0
- package/starter-kit/tokens/scales.ts +106 -0
- package/starter-kit/tokens/semantic.ts +98 -0
- package/starter-kit/tokens/shadow.ts +36 -0
- package/starter-kit/tokens/typography.ts +72 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# When to use X vs Y
|
|
2
|
+
|
|
3
|
+
Disambiguation for components whose names or looks overlap, so a new screen reaches for the right
|
|
4
|
+
one on the first try instead of the closest-looking one. Each entry names the deciding question —
|
|
5
|
+
answer it and the choice falls out. For the full prop/variant/state list per component, read
|
|
6
|
+
`native/catalog/manifest.ts`'s `buildComponentManifest()` output (rendered live at the catalog's
|
|
7
|
+
"Manifest" page) rather than guessing from a component's name alone.
|
|
8
|
+
|
|
9
|
+
## InputField vs SearchField vs Dropdown
|
|
10
|
+
|
|
11
|
+
All three render on the same field chrome (`FieldContainer`) and can look identical at rest. The
|
|
12
|
+
deciding question: **what happens when the user taps in?**
|
|
13
|
+
|
|
14
|
+
- **InputField** — a labeled field with a *floating label* (the label shrinks to a caption once
|
|
15
|
+
active/filled). Use for a named piece of data with a fixed identity across the interaction — "To",
|
|
16
|
+
"From", "Arrive by", a person's name. Supports a live `editable` TextInput mode *or* a tappable
|
|
17
|
+
picker mode (`active` forced from outside, e.g. by a Dropdown/BottomSheet you're driving).
|
|
18
|
+
- **SearchField** — a single-purpose live text input with a permanent leading search icon and no
|
|
19
|
+
floating label (there's nothing to float to — "search" *is* the label, implied by the icon). Use
|
|
20
|
+
for free-text filtering/searching, never for a named field with a fixed value.
|
|
21
|
+
- **Dropdown** — not a text input at all. A label-above-value trigger that opens a `BottomSheet`
|
|
22
|
+
picker with a fixed option list. Use when the answer is one of a small, known set of choices
|
|
23
|
+
(a direction, a filter value) — never for free text.
|
|
24
|
+
|
|
25
|
+
Rule of thumb: known short list → **Dropdown**. Free-text filter/search → **SearchField**. Anything
|
|
26
|
+
else the user types or picks, with its own persistent label → **InputField**.
|
|
27
|
+
|
|
28
|
+
## Button vs Pill
|
|
29
|
+
|
|
30
|
+
Both are tappable, labeled, and can carry an icon.
|
|
31
|
+
|
|
32
|
+
- **Button** — an *action*. Something happens when you tap it (save, confirm, navigate, submit).
|
|
33
|
+
Five weights (`primary`/`secondary`/`tertiary`/`white`/`ghost`) express visual priority among
|
|
34
|
+
multiple actions on the same screen.
|
|
35
|
+
- **Pill** — a *selection toggle*, not an action. Tapping it flips a `selected`/`not_selected`
|
|
36
|
+
state that persists (a filter chip, a saved-place shortcut). If tapping it doesn't leave something
|
|
37
|
+
visibly "on," it should be a Button, not a Pill.
|
|
38
|
+
|
|
39
|
+
## Toast vs Banner
|
|
40
|
+
|
|
41
|
+
Both show a status message with an icon and optional action, and both support the same five
|
|
42
|
+
semantic variants.
|
|
43
|
+
|
|
44
|
+
- **Toast** — *transient* and *self-contained*. Appears on top of the current screen, unrelated to
|
|
45
|
+
the content beneath it, and is expected to go away on its own or via its own action ("Undo",
|
|
46
|
+
"Retry"). Owns no layout space — it floats.
|
|
47
|
+
- **Banner** — *persistent* and *in-flow*. Lives inside the screen's own layout (top of a list,
|
|
48
|
+
inside a card stack) for as long as the condition it describes is true ("Fare increase", "Line
|
|
49
|
+
suspended"). Never auto-dismisses; the condition or an explicit user action clears it.
|
|
50
|
+
|
|
51
|
+
If the message is about something happening *right now, because of something the user just did* →
|
|
52
|
+
Toast. If it's a standing fact about the current screen's data → Banner.
|
|
53
|
+
|
|
54
|
+
## Badge vs Pill vs a Toast/Banner variant
|
|
55
|
+
|
|
56
|
+
All three can render the same five semantic colors, which makes them easy to reach for
|
|
57
|
+
interchangeably. They aren't interchangeable:
|
|
58
|
+
|
|
59
|
+
- **Badge** — *read-only status label*. Not tappable. Use inline next to other content ("On time"
|
|
60
|
+
next to a train row, "Suspended" next to a line name).
|
|
61
|
+
- **Pill** — *tappable selection*. Use it when the same-looking chip needs to respond to a tap and
|
|
62
|
+
hold a selected state.
|
|
63
|
+
- **Toast/Banner variant** — the color describes a *message about the whole screen or a transient
|
|
64
|
+
event*, not a single inline data point. If you're tagging one row's status, that's a Badge; if
|
|
65
|
+
you're telling the user something happened, that's a Toast or Banner.
|
|
66
|
+
|
|
67
|
+
## Dialog vs BottomSheet
|
|
68
|
+
|
|
69
|
+
Both are overlay containers with a backdrop.
|
|
70
|
+
|
|
71
|
+
- **Dialog** — center-screen, fades and scales in. Use for a short, focused decision that
|
|
72
|
+
interrupts the flow (confirm/cancel, a single form). Named `Dialog` (not `Modal`) specifically to
|
|
73
|
+
avoid shadowing React Native's own built-in `Modal`.
|
|
74
|
+
- **BottomSheet** — bottom-anchored, slides up, height driven by content up to 90% of the screen.
|
|
75
|
+
Use for anything longer, browsable, or that benefits from a `TopNav`/`Dock` header-footer
|
|
76
|
+
structure (an option picker, a detail view, a multi-field form). If the content would need its
|
|
77
|
+
own internal scrolling, it's a BottomSheet, not a Dialog.
|
|
78
|
+
|
|
79
|
+
## Card vs ListItem (inside a List)
|
|
80
|
+
|
|
81
|
+
Both hold a leading element, a title, and secondary text.
|
|
82
|
+
|
|
83
|
+
- **ListItem** (inside **List**) — a *row in a set of peers* — settings, a menu, search results.
|
|
84
|
+
Rows are visually light (no individual shadow/border) because the *List* container supplies the
|
|
85
|
+
shared surface and the dividers between them.
|
|
86
|
+
- **Card** — a *standalone, self-contained unit* with its own shadow and rounded surface. Use when
|
|
87
|
+
the content doesn't belong to a homogeneous list — a single trip summary, a promo, one dashboard
|
|
88
|
+
tile sitting alongside other, differently-shaped content.
|
|
89
|
+
|
|
90
|
+
If you're about to render more than one of the same shape in a vertical stack with dividers between
|
|
91
|
+
them, that's `List`/`ListItem`, not a stack of `Card`s.
|
|
92
|
+
|
|
93
|
+
## SegmentedToggle vs UnderlineTabs
|
|
94
|
+
|
|
95
|
+
Same `options`/`value`/`onChange` API, same job (switch between a small set of views) — the
|
|
96
|
+
difference is purely visual weight.
|
|
97
|
+
|
|
98
|
+
- **SegmentedToggle** — a filled, recessed track with a sliding white thumb. Heavier visual weight;
|
|
99
|
+
use when the choice is a primary, prominent control on the screen (e.g. Map/List view).
|
|
100
|
+
- **UnderlineTabs** — left-aligned labels over a hairline rule with a sliding underline. Quieter;
|
|
101
|
+
use for secondary navigation within a screen that already has a clear primary focus.
|
|
102
|
+
|
|
103
|
+
## Checkbox vs Switch vs Radio
|
|
104
|
+
|
|
105
|
+
All three are binary-ish controls; the difference is what the value means and how many can be true
|
|
106
|
+
at once.
|
|
107
|
+
|
|
108
|
+
- **Checkbox** — an independent on/off fact about *this one item* ("Remember this trip"). Any number
|
|
109
|
+
of checkboxes on a screen can be checked at once, independently.
|
|
110
|
+
- **Switch** — an on/off *setting* that takes effect immediately (no separate "Save" step). Reach
|
|
111
|
+
for Switch specifically when toggling it *does something right away*; reach for Checkbox when it's
|
|
112
|
+
just recording a fact that a future action (a form submit) will act on.
|
|
113
|
+
- **Radio** — one selection from a *mutually exclusive* set (only one can be selected across a group
|
|
114
|
+
of Radios sharing one consumer-held value). If checking one should un-check another, it's Radio,
|
|
115
|
+
never Checkbox.
|
|
116
|
+
|
|
117
|
+
## EmptyState vs Banner
|
|
118
|
+
|
|
119
|
+
- **EmptyState** — fills the *entire* content area of a screen or section because there is
|
|
120
|
+
*nothing else to show* (no results, no saved items, first run). Centered, icon-led, optionally one
|
|
121
|
+
action.
|
|
122
|
+
- **Banner** — sits *alongside* other real content, describing a condition about that content or the
|
|
123
|
+
screen in general. If the screen would otherwise be blank, use EmptyState; if it has content and
|
|
124
|
+
you're adding a note above/around it, use Banner.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { AnimatedChevron } from './AnimatedChevron';
|
|
4
|
+
import { DS_SEMANTIC } from '../../tokens';
|
|
5
|
+
|
|
6
|
+
export default defineCatalogPage({
|
|
7
|
+
component: 'AnimatedChevron',
|
|
8
|
+
group: 'Sub-Parts',
|
|
9
|
+
description: 'A chevron that morphs between down (collapsed) and up (expanded) — flipping vertically in place instead of rotating through a sideways-pointing angle, and instead of swapping icons. The morph Banner\'s own collapsible header uses for its disclosure indicator. Not commonly used on its own; exported for building a custom disclosure/accordion toggle.',
|
|
10
|
+
a11y: 'Purely decorative — no accessibility role of its own. Wrap it in an accessible parent (the way Banner\'s own header Pressable does) if the toggle needs to be announced.',
|
|
11
|
+
variants: {
|
|
12
|
+
items: [
|
|
13
|
+
{ key: 'collapsed', name: 'Collapsed', props: { expanded: false }, node: <AnimatedChevron expanded={false} /> },
|
|
14
|
+
{ key: 'expanded', name: 'Expanded', props: { expanded: true }, node: <AnimatedChevron expanded={true} /> },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
states: {
|
|
18
|
+
items: [
|
|
19
|
+
{ key: 'size', name: 'Custom size', node: <AnimatedChevron expanded={false} size={28} /> },
|
|
20
|
+
{ key: 'color', name: 'Custom color', node: <AnimatedChevron expanded={true} color={DS_SEMANTIC.emphasis.info} /> },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, Easing } from 'react-native';
|
|
3
|
+
import { Icon } from '../../icons/Icon.native';
|
|
4
|
+
import { DS_MOTION_DURATION, DS_MOTION_EASING } from '../../tokens';
|
|
5
|
+
|
|
6
|
+
export interface AnimatedChevronProps {
|
|
7
|
+
/** When true the chevron points up; when false it points down. */
|
|
8
|
+
expanded: boolean;
|
|
9
|
+
/** Icon size in px. */
|
|
10
|
+
size?: number;
|
|
11
|
+
/** Icon color. */
|
|
12
|
+
color?: string;
|
|
13
|
+
/** Morph duration in ms. Defaults to DS_MOTION_DURATION.base (240). */
|
|
14
|
+
duration?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A chevron that morphs between down (collapsed) and up (expanded) by flipping vertically in place
|
|
19
|
+
* (`scaleY` 1 → 0 → -1) rather than rotating through a sideways-pointing intermediate angle — a
|
|
20
|
+
* plain chevron-down glyph mirrored on its own vertical centre (`scaleY: -1`) is exactly chevron-up,
|
|
21
|
+
* so no second icon or path swap is needed. Uses the core `Animated` API with `useNativeDriver:
|
|
22
|
+
* true` (transform-only), so it's smooth on the New Architecture in dev and release.
|
|
23
|
+
*/
|
|
24
|
+
export function AnimatedChevron({ expanded, size = 16, color, duration = DS_MOTION_DURATION.base }: AnimatedChevronProps) {
|
|
25
|
+
const progress = useRef(new Animated.Value(expanded ? 1 : 0)).current;
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const anim = Animated.timing(progress, {
|
|
29
|
+
toValue: expanded ? 1 : 0,
|
|
30
|
+
duration,
|
|
31
|
+
// `standard` — an in-place morph (a swap), per DS_MOTION_EASING_USE.
|
|
32
|
+
easing: Easing.bezier(...DS_MOTION_EASING.standard),
|
|
33
|
+
useNativeDriver: true,
|
|
34
|
+
});
|
|
35
|
+
anim.start();
|
|
36
|
+
return () => anim.stop();
|
|
37
|
+
}, [expanded, duration, progress]);
|
|
38
|
+
|
|
39
|
+
const scaleY = progress.interpolate({ inputRange: [0, 1], outputRange: [1, -1] });
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Animated.View style={{ transform: [{ scaleY }] }}>
|
|
43
|
+
<Icon name="chevron-down" size={size} color={color} />
|
|
44
|
+
</Animated.View>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineCatalogPage, grid } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { Avatar } from './Avatar';
|
|
4
|
+
import { DS_SEMANTIC } from '../../tokens';
|
|
5
|
+
|
|
6
|
+
// Avatar: content Kind × Size (Avatar's `size` is continuous; 24 / 40 default / 64 is the sweep).
|
|
7
|
+
// Neither axis names a real prop — "Kind" is which of 3 optional props is set, not one enum, and
|
|
8
|
+
// `size` has no fixed option set — so both stay unbound. An unbound axis is fully valid; `doctor`
|
|
9
|
+
// only ever reports how many examples are unbound as information, never as an error or a warning.
|
|
10
|
+
const AVATAR_COMPARISON = grid(
|
|
11
|
+
'Kind',
|
|
12
|
+
'Size',
|
|
13
|
+
[
|
|
14
|
+
{ key: 'image', label: 'Image' },
|
|
15
|
+
{ key: 'icon', label: 'Icon' },
|
|
16
|
+
{ key: 'initials', label: 'Initials fallback' },
|
|
17
|
+
],
|
|
18
|
+
[
|
|
19
|
+
{ key: 'small', label: 'Small · 24' },
|
|
20
|
+
{ key: 'medium', label: 'Medium · 40 (default)' },
|
|
21
|
+
{ key: 'large', label: 'Large · 64' },
|
|
22
|
+
],
|
|
23
|
+
(row, column) => {
|
|
24
|
+
const size = column === 'small' ? 24 : column === 'large' ? 64 : 40;
|
|
25
|
+
if (row === 'image') return <Avatar imageUrl="https://i.pravatar.cc/100" accessibilityLabel="Jordan Lee" size={size} />;
|
|
26
|
+
if (row === 'icon') return <Avatar iconName="users" accessibilityLabel="Guest" size={size} />;
|
|
27
|
+
return <Avatar initials="JL" accessibilityLabel="Jordan Lee" size={size} />;
|
|
28
|
+
},
|
|
29
|
+
'compact',
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export default defineCatalogPage({
|
|
33
|
+
component: 'Avatar',
|
|
34
|
+
group: 'Surfaces',
|
|
35
|
+
specimenSize: 'compact',
|
|
36
|
+
comparison: AVATAR_COMPARISON,
|
|
37
|
+
description: 'A circular image, or an initials fallback on a solid fill when there\'s no image (or it fails to load).',
|
|
38
|
+
a11y: 'Renders with accessibilityRole="image"; pass accessibilityLabel for a meaningful name, otherwise it falls back to the initials text.',
|
|
39
|
+
variants: {
|
|
40
|
+
items: [
|
|
41
|
+
{ key: 'image', name: 'Image', node: <Avatar imageUrl="https://i.pravatar.cc/100" accessibilityLabel="Jordan Lee" /> },
|
|
42
|
+
{ key: 'icon', name: 'Icon', node: <Avatar iconName="users" accessibilityLabel="Guest" /> },
|
|
43
|
+
{ key: 'initials', name: 'Initials fallback', node: <Avatar initials="JL" accessibilityLabel="Jordan Lee" /> },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
states: {
|
|
47
|
+
items: [
|
|
48
|
+
{ key: 'small', name: 'Small', node: <Avatar initials="JL" size={24} /> },
|
|
49
|
+
{ key: 'medium', name: 'Medium (default)', node: <Avatar initials="JL" size={40} /> },
|
|
50
|
+
{ key: 'large', name: 'Large', node: <Avatar initials="JL" size={64} /> },
|
|
51
|
+
{ key: 'custom-color', name: 'Custom colour', node: <Avatar initials="AC" backgroundColor={DS_SEMANTIC.emphasis.info} /> },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, Image, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { DS_SEMANTIC, DS_FONT_WEIGHT } from '../../tokens';
|
|
4
|
+
import { Icon } from '../../icons/Icon.native';
|
|
5
|
+
import type { IconName } from '../../icons';
|
|
6
|
+
|
|
7
|
+
export interface AvatarProps {
|
|
8
|
+
/** Remote image URL. Takes precedence over `iconName`/`initials`; falls back to them when omitted
|
|
9
|
+
* or the image fails to load. */
|
|
10
|
+
imageUrl?: string;
|
|
11
|
+
/** Icon shown instead of initials — e.g. for a generic/anonymous avatar. Takes precedence over
|
|
12
|
+
* `initials` when there's no image. */
|
|
13
|
+
iconName?: IconName;
|
|
14
|
+
/** Shown when there's no image or icon — the first 1-2 characters are used, uppercased. */
|
|
15
|
+
initials?: string;
|
|
16
|
+
/** Diameter in px. @default 40 */
|
|
17
|
+
size?: number;
|
|
18
|
+
/** Fill colour behind the icon/initials. Defaults to a neutral muted surface (with dark
|
|
19
|
+
* foreground); passing a custom colour switches the icon/initials to light (inverse) text,
|
|
20
|
+
* assuming a saturated/dark fill — match that pairing if you override this. */
|
|
21
|
+
backgroundColor?: string;
|
|
22
|
+
accessibilityLabel?: string;
|
|
23
|
+
style?: StyleProp<ViewStyle>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A circular image, icon, or initials fallback on a solid fill — in that order of precedence. */
|
|
27
|
+
export function Avatar({
|
|
28
|
+
imageUrl,
|
|
29
|
+
iconName,
|
|
30
|
+
initials,
|
|
31
|
+
size = 40,
|
|
32
|
+
backgroundColor,
|
|
33
|
+
accessibilityLabel,
|
|
34
|
+
style,
|
|
35
|
+
}: AvatarProps) {
|
|
36
|
+
const [imageFailed, setImageFailed] = React.useState(false);
|
|
37
|
+
const showImage = !!imageUrl && !imageFailed;
|
|
38
|
+
const showIcon = !showImage && !!iconName;
|
|
39
|
+
const dimension = { width: size, height: size, borderRadius: size / 2 };
|
|
40
|
+
// The default fill (surface.muted) is light, so its icon/initials need dark text; a caller-supplied
|
|
41
|
+
// backgroundColor is assumed saturated/dark (the existing convention — see the "Custom colour"
|
|
42
|
+
// catalog example), so it keeps the light inverse text that pairing needs.
|
|
43
|
+
const contentColor = backgroundColor ? DS_SEMANTIC.text.inverse : DS_SEMANTIC.text.regular;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<View
|
|
47
|
+
style={[styles.circle, dimension, !showImage && { backgroundColor: backgroundColor ?? DS_SEMANTIC.surface.muted }, style]}
|
|
48
|
+
accessible
|
|
49
|
+
accessibilityRole="image"
|
|
50
|
+
accessibilityLabel={accessibilityLabel ?? initials}
|
|
51
|
+
>
|
|
52
|
+
{showImage ? (
|
|
53
|
+
<Image source={{ uri: imageUrl }} style={dimension} onError={() => setImageFailed(true)} />
|
|
54
|
+
) : showIcon ? (
|
|
55
|
+
<Icon name={iconName} size={Math.round(size * 0.5)} color={contentColor} />
|
|
56
|
+
) : (
|
|
57
|
+
<Text style={[styles.initials, { fontSize: Math.round(size * 0.4), color: contentColor }]} numberOfLines={1}>
|
|
58
|
+
{(initials ?? '?').slice(0, 2).toUpperCase()}
|
|
59
|
+
</Text>
|
|
60
|
+
)}
|
|
61
|
+
</View>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const styles = StyleSheet.create({
|
|
66
|
+
circle: {
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
justifyContent: 'center',
|
|
69
|
+
overflow: 'hidden',
|
|
70
|
+
},
|
|
71
|
+
initials: {
|
|
72
|
+
fontWeight: DS_FONT_WEIGHT.semibold,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineCatalogPage, grid } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { Badge } from './Badge';
|
|
4
|
+
import type { BadgeVariant } from './Badge.types';
|
|
5
|
+
|
|
6
|
+
// Badge: Tone × Icon layout. One icon per tone, matching the Variants examples.
|
|
7
|
+
const BADGE_TONES: { key: BadgeVariant; label: string; text: string; icon: React.ComponentProps<typeof Badge>['leadingIcon'] }[] = [
|
|
8
|
+
{ key: 'neutral', label: 'Neutral', text: 'Local', icon: 'pin' },
|
|
9
|
+
{ key: 'info', label: 'Info', text: 'Notice', icon: 'info-circle' },
|
|
10
|
+
{ key: 'positive', label: 'Positive', text: 'On time', icon: 'circle-check' },
|
|
11
|
+
{ key: 'warning', label: 'Warning', text: 'Delayed', icon: 'triangle-alert' },
|
|
12
|
+
{ key: 'negative', label: 'Negative', text: 'Suspended', icon: 'circle-slash' },
|
|
13
|
+
];
|
|
14
|
+
// `rows` is bound to Badge's own `variant` prop (design §4) — written as its own literal array
|
|
15
|
+
// (not `BADGE_TONES.map(...)`) so `doctor`'s static reader can check it without evaluating a
|
|
16
|
+
// function call; `BADGE_TONES` above still drives the real rendering in `cell` below.
|
|
17
|
+
const BADGE_ROWS = {
|
|
18
|
+
prop: 'variant',
|
|
19
|
+
items: [
|
|
20
|
+
{ key: 'neutral', label: 'Neutral' },
|
|
21
|
+
{ key: 'info', label: 'Info' },
|
|
22
|
+
{ key: 'positive', label: 'Positive' },
|
|
23
|
+
{ key: 'warning', label: 'Warning' },
|
|
24
|
+
{ key: 'negative', label: 'Negative' },
|
|
25
|
+
],
|
|
26
|
+
} as const;
|
|
27
|
+
const BADGE_COMPARISON = grid(
|
|
28
|
+
'Tone',
|
|
29
|
+
'Icon',
|
|
30
|
+
BADGE_ROWS,
|
|
31
|
+
[
|
|
32
|
+
{ key: 'label-only', label: 'Label only' },
|
|
33
|
+
{ key: 'leading-icon', label: 'Leading icon' },
|
|
34
|
+
{ key: 'trailing-icon', label: 'Trailing icon' },
|
|
35
|
+
{ key: 'icon-only', label: 'Icon-only' },
|
|
36
|
+
],
|
|
37
|
+
(row, column) => {
|
|
38
|
+
const tone = BADGE_TONES.find((t) => t.key === row)!;
|
|
39
|
+
if (column === 'icon-only') return <Badge variant={tone.key} leadingIcon={tone.icon} accessibilityLabel={tone.text} />;
|
|
40
|
+
return (
|
|
41
|
+
<Badge
|
|
42
|
+
variant={tone.key}
|
|
43
|
+
label={tone.text}
|
|
44
|
+
leadingIcon={column === 'leading-icon' ? tone.icon : undefined}
|
|
45
|
+
trailingIcon={column === 'trailing-icon' ? tone.icon : undefined}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
'compact',
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export default defineCatalogPage({
|
|
53
|
+
component: 'Badge',
|
|
54
|
+
group: 'Surfaces',
|
|
55
|
+
specimenSize: 'compact',
|
|
56
|
+
comparison: BADGE_COMPARISON,
|
|
57
|
+
description: 'A small status chip — five semantic variants, with optional leading/trailing icons or icon-only.',
|
|
58
|
+
whenToUse: "Read-only and inline, not tappable — for one row's data point. For a tappable chip with a selected state, use Pill; for a message about the whole screen, use Toast or Banner.",
|
|
59
|
+
a11y: 'A plain View with text; the label carries the meaning, so avoid encoding status by color alone.',
|
|
60
|
+
variants: {
|
|
61
|
+
items: [
|
|
62
|
+
{ key: 'neutral', name: 'Neutral', props: { variant: 'neutral' }, node: <Badge variant="neutral" label="Local" /> },
|
|
63
|
+
{ key: 'info', name: 'Info', props: { variant: 'info' }, node: <Badge variant="info" label="Notice" leadingIcon="info-circle" /> },
|
|
64
|
+
{ key: 'positive', name: 'Positive', props: { variant: 'positive' }, node: <Badge variant="positive" label="On time" leadingIcon="circle-check" /> },
|
|
65
|
+
{ key: 'warning', name: 'Warning', props: { variant: 'warning' }, node: <Badge variant="warning" label="Delayed" leadingIcon="triangle-alert" /> },
|
|
66
|
+
{ key: 'negative', name: 'Negative', props: { variant: 'negative' }, node: <Badge variant="negative" label="Suspended" leadingIcon="circle-slash" /> },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
states: {
|
|
70
|
+
items: [
|
|
71
|
+
{
|
|
72
|
+
key: 'leading-icon',
|
|
73
|
+
name: 'Leading icon',
|
|
74
|
+
node: <Badge variant="positive" label="On time" leadingIcon="circle-check" />,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: 'trailing-icon',
|
|
78
|
+
name: 'Trailing icon',
|
|
79
|
+
node: <Badge variant="positive" label="On time" trailingIcon="circle-check" />,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: 'icon-only',
|
|
83
|
+
name: 'Icon-only',
|
|
84
|
+
node: <Badge variant="info" leadingIcon="info-circle" accessibilityLabel="Notice" />,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
3
|
+
import { DS_RADIUS, DS_SPACING, DS_TYPOGRAPHY, DS_ICON_SIZE } from '../../tokens';
|
|
4
|
+
import { Icon } from '../../icons/Icon.native';
|
|
5
|
+
import { STATUS_BG, STATUS_FG } from '../statusColors';
|
|
6
|
+
import type { BadgeProps } from './Badge.types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A small status pill. Composable: label-only, icon-only (a single icon, no label), or a label with
|
|
10
|
+
* a leading and/or trailing icon. Text + icons sit on the variant background in the variant's own
|
|
11
|
+
* emphasis colour.
|
|
12
|
+
*/
|
|
13
|
+
export function Badge({ variant = 'neutral', label, leadingIcon, trailingIcon, accessibilityLabel, style }: BadgeProps) {
|
|
14
|
+
const hasLabel = label != null && label !== '';
|
|
15
|
+
// Dev-console check (same policy as ButtonGroup's checkConsistency): the docs require an
|
|
16
|
+
// accessible name for an icon-only badge, but the type can't enforce it — warn instead of
|
|
17
|
+
// silently rendering an unnameable status glyph.
|
|
18
|
+
if (__DEV__ && !hasLabel && !accessibilityLabel) {
|
|
19
|
+
console.warn('[Badge] Icon-only badge (no `label`) needs an `accessibilityLabel` — without one it has no accessible name.');
|
|
20
|
+
}
|
|
21
|
+
const fg = STATUS_FG[variant];
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<View
|
|
25
|
+
style={[styles.badge, !hasLabel && styles.iconOnly, { backgroundColor: STATUS_BG[variant] }, style]}
|
|
26
|
+
accessibilityRole="text"
|
|
27
|
+
accessibilityLabel={accessibilityLabel}
|
|
28
|
+
>
|
|
29
|
+
{leadingIcon ? <Icon name={leadingIcon} size={DS_ICON_SIZE.xs} color={fg} /> : null}
|
|
30
|
+
{hasLabel ? (
|
|
31
|
+
<Text style={[styles.label, { color: fg }]} numberOfLines={1}>
|
|
32
|
+
{label}
|
|
33
|
+
</Text>
|
|
34
|
+
) : null}
|
|
35
|
+
{trailingIcon ? <Icon name={trailingIcon} size={DS_ICON_SIZE.xs} color={fg} /> : null}
|
|
36
|
+
</View>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const styles = StyleSheet.create({
|
|
41
|
+
badge: {
|
|
42
|
+
flexDirection: 'row',
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
gap: DS_SPACING[100],
|
|
45
|
+
paddingVertical: DS_SPACING[200],
|
|
46
|
+
paddingHorizontal: DS_SPACING[400],
|
|
47
|
+
borderRadius: DS_RADIUS.round,
|
|
48
|
+
alignSelf: 'flex-start',
|
|
49
|
+
},
|
|
50
|
+
iconOnly: {
|
|
51
|
+
paddingHorizontal: DS_SPACING[200],
|
|
52
|
+
},
|
|
53
|
+
label: {
|
|
54
|
+
...DS_TYPOGRAPHY.labelXs,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import type { IconName } from '../../icons';
|
|
3
|
+
|
|
4
|
+
export type BadgeVariant = 'neutral' | 'info' | 'positive' | 'warning' | 'negative';
|
|
5
|
+
|
|
6
|
+
export interface BadgeProps {
|
|
7
|
+
/** Semantic color scheme. */
|
|
8
|
+
variant?: BadgeVariant;
|
|
9
|
+
/** Label text. Omit (with a single icon) for an icon-only chip. */
|
|
10
|
+
label?: string;
|
|
11
|
+
/** Icon before the label. */
|
|
12
|
+
leadingIcon?: IconName;
|
|
13
|
+
/** Icon after the label. */
|
|
14
|
+
trailingIcon?: IconName;
|
|
15
|
+
/** Accessible name — required for an icon-only badge (no `label`) so it can be announced. */
|
|
16
|
+
accessibilityLabel?: string;
|
|
17
|
+
style?: StyleProp<ViewStyle>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { Banner } from './Banner';
|
|
4
|
+
|
|
5
|
+
export default defineCatalogPage({
|
|
6
|
+
component: 'Banner',
|
|
7
|
+
group: 'Surfaces',
|
|
8
|
+
description: 'An inline callout for status/announcements — five semantic variants, optional collapsible body, inline link, and action button.',
|
|
9
|
+
whenToUse: "Persistent and in-flow, describing a standing condition about the screen's content. For a transient, self-contained event notification, use Toast instead.",
|
|
10
|
+
a11y: 'When onPress/action is set the header/button are Pressables; the collapsible header toggles the description with a chevron affordance.',
|
|
11
|
+
variants: {
|
|
12
|
+
itemsFill: true,
|
|
13
|
+
items: [
|
|
14
|
+
{ key: 'neutral', name: 'Neutral', props: { variant: 'neutral' }, node: <Banner variant="neutral" title="Schedule notice" description="Holiday schedule in effect Monday." /> },
|
|
15
|
+
{ key: 'info', name: 'Info', props: { variant: 'info' }, node: <Banner variant="info" title="Weekend service change" description="The 6 runs express in both directions this weekend." /> },
|
|
16
|
+
{ key: 'positive', name: 'Positive', props: { variant: 'positive' }, node: <Banner variant="positive" title="Service restored" description="All lines are running on a normal schedule." /> },
|
|
17
|
+
{ key: 'warning', name: 'Warning', props: { variant: 'warning' }, node: <Banner variant="warning" title="Delays" description="Signal problems near 14 St." /> },
|
|
18
|
+
{ key: 'negative', name: 'Negative', props: { variant: 'negative' }, node: <Banner variant="negative" title="Line suspended" description="No service between 96 St and 137 St until further notice." /> },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
states: {
|
|
22
|
+
itemsFill: true,
|
|
23
|
+
items: [
|
|
24
|
+
{
|
|
25
|
+
key: 'tappable',
|
|
26
|
+
name: 'Tappable',
|
|
27
|
+
node: (
|
|
28
|
+
<Banner
|
|
29
|
+
variant="neutral"
|
|
30
|
+
title="Alert preferences"
|
|
31
|
+
description="Tap to manage which alerts you receive."
|
|
32
|
+
onPress={() => {}}
|
|
33
|
+
/>
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: 'collapsible-expanded',
|
|
38
|
+
name: 'Collapsible (expanded)',
|
|
39
|
+
node: (
|
|
40
|
+
<Banner
|
|
41
|
+
variant="neutral"
|
|
42
|
+
title="Trip details"
|
|
43
|
+
description="Board at the front car for a faster transfer at Union Sq."
|
|
44
|
+
collapsible
|
|
45
|
+
/>
|
|
46
|
+
),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: 'collapsible-collapsed',
|
|
50
|
+
name: 'Collapsible (collapsed)',
|
|
51
|
+
node: (
|
|
52
|
+
<Banner
|
|
53
|
+
variant="neutral"
|
|
54
|
+
title="Trip details"
|
|
55
|
+
description="Board at the front car for a faster transfer at Union Sq."
|
|
56
|
+
collapsible
|
|
57
|
+
defaultExpanded={false}
|
|
58
|
+
/>
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: 'action',
|
|
63
|
+
name: 'With action',
|
|
64
|
+
node: (
|
|
65
|
+
<Banner
|
|
66
|
+
variant="info"
|
|
67
|
+
title="App update available"
|
|
68
|
+
description="Version 4.2 adds live bus tracking."
|
|
69
|
+
action={{ label: 'Update', onPress: () => {} }}
|
|
70
|
+
/>
|
|
71
|
+
),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
key: 'link',
|
|
75
|
+
name: 'With link',
|
|
76
|
+
node: (
|
|
77
|
+
<Banner
|
|
78
|
+
variant="neutral"
|
|
79
|
+
title="Fare increase"
|
|
80
|
+
description="New fares start March 1."
|
|
81
|
+
link={{ label: 'Learn more', onPress: () => {} }}
|
|
82
|
+
/>
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
});
|