@olympusoss/canvas 4.0.0 → 5.0.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 +108 -0
- package/package.json +14 -3
- package/src/atoms/avatar/avatar.md +185 -0
- package/src/atoms/avatar/avatar.styles.ts +48 -0
- package/src/atoms/avatar/avatar.tsx +99 -0
- package/src/atoms/badge/badge.md +237 -0
- package/src/atoms/badge/badge.styles.ts +79 -0
- package/src/atoms/badge/badge.tsx +86 -0
- package/src/atoms/breadcrumb/breadcrumb.md +233 -0
- package/src/atoms/breadcrumb/breadcrumb.styles.ts +40 -0
- package/src/atoms/breadcrumb/breadcrumb.tsx +130 -0
- package/src/atoms/button/button.android.tsx +6 -0
- package/src/atoms/button/button.ios.tsx +6 -0
- package/src/atoms/button/button.md +184 -0
- package/src/atoms/button/button.shared.tsx +79 -0
- package/src/atoms/button/button.styles.ts +152 -0
- package/src/atoms/button/button.tsx +6 -0
- package/src/atoms/button-group/button-group.android.tsx +6 -0
- package/src/atoms/button-group/button-group.ios.tsx +6 -0
- package/src/atoms/button-group/button-group.md +120 -0
- package/src/atoms/button-group/button-group.shared.tsx +398 -0
- package/src/atoms/button-group/button-group.styles.ts +483 -0
- package/src/atoms/button-group/button-group.tsx +6 -0
- package/src/atoms/checkbox/checkbox.android.tsx +6 -0
- package/src/atoms/checkbox/checkbox.ios.tsx +6 -0
- package/src/atoms/checkbox/checkbox.md +150 -0
- package/src/atoms/checkbox/checkbox.shared.tsx +103 -0
- package/src/atoms/checkbox/checkbox.styles.ts +106 -0
- package/src/atoms/checkbox/checkbox.tsx +6 -0
- package/src/atoms/combobox/combobox.android.tsx +6 -0
- package/src/atoms/combobox/combobox.ios.tsx +6 -0
- package/src/atoms/combobox/combobox.md +213 -0
- package/src/atoms/combobox/combobox.shared.tsx +160 -0
- package/src/atoms/combobox/combobox.styles.ts +270 -0
- package/src/atoms/combobox/combobox.tsx +6 -0
- package/src/atoms/divider/divider.md +140 -0
- package/src/atoms/divider/divider.styles.ts +35 -0
- package/src/atoms/divider/divider.tsx +67 -0
- package/src/atoms/dropdown/dropdown.android.tsx +6 -0
- package/src/atoms/dropdown/dropdown.ios.tsx +6 -0
- package/src/atoms/dropdown/dropdown.md +221 -0
- package/src/atoms/dropdown/dropdown.shared.tsx +190 -0
- package/src/atoms/dropdown/dropdown.styles.ts +233 -0
- package/src/atoms/dropdown/dropdown.tsx +6 -0
- package/src/atoms/icon/icon.md +131 -0
- package/src/atoms/icon/icon.styles.ts +30 -0
- package/src/atoms/icon/icon.tsx +328 -0
- package/src/atoms/index.ts +24 -0
- package/src/atoms/input/input.android.tsx +6 -0
- package/src/atoms/input/input.ios.tsx +6 -0
- package/src/atoms/input/input.md +118 -0
- package/src/atoms/input/input.shared.tsx +203 -0
- package/src/atoms/input/input.styles.ts +286 -0
- package/src/atoms/input/input.tsx +6 -0
- package/src/atoms/kbd/kbd.md +91 -0
- package/src/atoms/kbd/kbd.styles.ts +33 -0
- package/src/atoms/kbd/kbd.tsx +27 -0
- package/src/atoms/listbox/listbox.md +177 -0
- package/src/atoms/listbox/listbox.styles.ts +60 -0
- package/src/atoms/listbox/listbox.tsx +113 -0
- package/src/atoms/pagination/pagination.android.tsx +6 -0
- package/src/atoms/pagination/pagination.ios.tsx +6 -0
- package/src/atoms/pagination/pagination.md +133 -0
- package/src/atoms/pagination/pagination.shared.tsx +289 -0
- package/src/atoms/pagination/pagination.styles.ts +245 -0
- package/src/atoms/pagination/pagination.tsx +6 -0
- package/src/atoms/popover/popover.android.tsx +8 -0
- package/src/atoms/popover/popover.ios.tsx +6 -0
- package/src/atoms/popover/popover.md +87 -0
- package/src/atoms/popover/popover.shared.tsx +124 -0
- package/src/atoms/popover/popover.styles.ts +144 -0
- package/src/atoms/popover/popover.tsx +6 -0
- package/src/atoms/radio/radio.android.tsx +6 -0
- package/src/atoms/radio/radio.ios.tsx +6 -0
- package/src/atoms/radio/radio.md +173 -0
- package/src/atoms/radio/radio.shared.tsx +98 -0
- package/src/atoms/radio/radio.styles.ts +109 -0
- package/src/atoms/radio/radio.tsx +6 -0
- package/src/atoms/select/select.android.tsx +6 -0
- package/src/atoms/select/select.ios.tsx +6 -0
- package/src/atoms/select/select.md +156 -0
- package/src/atoms/select/select.shared.tsx +143 -0
- package/src/atoms/select/select.styles.ts +310 -0
- package/src/atoms/select/select.tsx +6 -0
- package/src/atoms/skeleton/skeleton.md +135 -0
- package/src/atoms/skeleton/skeleton.styles.ts +117 -0
- package/src/atoms/skeleton/skeleton.tsx +145 -0
- package/src/atoms/spinner/spinner.android.tsx +7 -0
- package/src/atoms/spinner/spinner.ios.tsx +7 -0
- package/src/atoms/spinner/spinner.md +94 -0
- package/src/atoms/spinner/spinner.shared.tsx +92 -0
- package/src/atoms/spinner/spinner.styles.tsx +115 -0
- package/src/atoms/spinner/spinner.tsx +7 -0
- package/src/atoms/switch/switch.android.tsx +6 -0
- package/src/atoms/switch/switch.ios.tsx +6 -0
- package/src/atoms/switch/switch.md +91 -0
- package/src/atoms/switch/switch.shared.tsx +97 -0
- package/src/atoms/switch/switch.styles.ts +79 -0
- package/src/atoms/switch/switch.tsx +6 -0
- package/src/atoms/textarea/textarea.android.tsx +6 -0
- package/src/atoms/textarea/textarea.ios.tsx +6 -0
- package/src/atoms/textarea/textarea.md +140 -0
- package/src/atoms/textarea/textarea.shared.tsx +74 -0
- package/src/atoms/textarea/textarea.styles.ts +116 -0
- package/src/atoms/textarea/textarea.tsx +6 -0
- package/src/atoms/tooltip/tooltip.android.tsx +6 -0
- package/src/atoms/tooltip/tooltip.ios.tsx +7 -0
- package/src/atoms/tooltip/tooltip.md +122 -0
- package/src/atoms/tooltip/tooltip.shared.tsx +113 -0
- package/src/atoms/tooltip/tooltip.styles.ts +113 -0
- package/src/atoms/tooltip/tooltip.tsx +6 -0
- package/src/atoms/typography/typography.md +330 -0
- package/src/atoms/typography/typography.styles.ts +95 -0
- package/src/atoms/typography/typography.tsx +76 -0
- package/src/index.ts +12 -2
- package/src/molecules/action-panels/action-panels.md +133 -0
- package/src/molecules/action-panels/action-panels.styles.ts +39 -0
- package/src/molecules/action-panels/action-panels.tsx +113 -0
- package/src/molecules/alert/alert.md +119 -0
- package/src/molecules/alert/alert.styles.ts +88 -0
- package/src/molecules/alert/alert.tsx +74 -0
- package/src/molecules/alert-dialog/alert-dialog.android.tsx +6 -0
- package/src/molecules/alert-dialog/alert-dialog.ios.tsx +6 -0
- package/src/molecules/alert-dialog/alert-dialog.md +177 -0
- package/src/molecules/alert-dialog/alert-dialog.shared.tsx +187 -0
- package/src/molecules/alert-dialog/alert-dialog.styles.ts +248 -0
- package/src/molecules/alert-dialog/alert-dialog.tsx +6 -0
- package/src/molecules/card/card.md +190 -0
- package/src/molecules/card/card.styles.ts +67 -0
- package/src/molecules/card/card.tsx +176 -0
- package/src/molecules/code-block/code-block.md +159 -0
- package/src/molecules/code-block/code-block.styles.ts +167 -0
- package/src/molecules/code-block/code-block.tsx +176 -0
- package/src/molecules/description-lists/description-lists.md +129 -0
- package/src/molecules/description-lists/description-lists.styles.ts +102 -0
- package/src/molecules/description-lists/description-lists.tsx +133 -0
- package/src/molecules/empty-state/empty-state.md +218 -0
- package/src/molecules/empty-state/empty-state.styles.ts +63 -0
- package/src/molecules/empty-state/empty-state.tsx +77 -0
- package/src/molecules/feeds/feeds.md +102 -0
- package/src/molecules/feeds/feeds.styles.ts +120 -0
- package/src/molecules/feeds/feeds.tsx +167 -0
- package/src/molecules/field/field.md +117 -0
- package/src/molecules/field/field.styles.ts +85 -0
- package/src/molecules/field/field.tsx +175 -0
- package/src/molecules/fieldset/fieldset.md +141 -0
- package/src/molecules/fieldset/fieldset.styles.ts +79 -0
- package/src/molecules/fieldset/fieldset.tsx +182 -0
- package/src/molecules/form/form.md +137 -0
- package/src/molecules/form/form.styles.ts +39 -0
- package/src/molecules/form/form.tsx +246 -0
- package/src/molecules/grid-lists/grid-lists.md +114 -0
- package/src/molecules/grid-lists/grid-lists.styles.ts +79 -0
- package/src/molecules/grid-lists/grid-lists.tsx +157 -0
- package/src/molecules/index.ts +16 -0
- package/src/molecules/media-objects/media-objects.md +87 -0
- package/src/molecules/media-objects/media-objects.styles.ts +94 -0
- package/src/molecules/media-objects/media-objects.tsx +128 -0
- package/src/molecules/stacked-lists/stacked-lists.md +116 -0
- package/src/molecules/stacked-lists/stacked-lists.styles.ts +111 -0
- package/src/molecules/stacked-lists/stacked-lists.tsx +195 -0
- package/src/molecules/stats/stats.md +166 -0
- package/src/molecules/stats/stats.styles.ts +91 -0
- package/src/molecules/stats/stats.tsx +88 -0
- package/src/organisms/calendar/calendar.android.tsx +6 -0
- package/src/organisms/calendar/calendar.ios.tsx +6 -0
- package/src/organisms/calendar/calendar.md +114 -0
- package/src/organisms/calendar/calendar.shared.tsx +146 -0
- package/src/organisms/calendar/calendar.styles.ts +315 -0
- package/src/organisms/calendar/calendar.tsx +6 -0
- package/src/organisms/charts/charts.md +326 -0
- package/src/organisms/charts/charts.styles.ts +135 -0
- package/src/organisms/charts/charts.tsx +124 -0
- package/src/organisms/command/command.md +117 -0
- package/src/organisms/command/command.styles.ts +179 -0
- package/src/organisms/command/command.tsx +164 -0
- package/src/organisms/data-table/data-table.md +182 -0
- package/src/organisms/data-table/data-table.styles.ts +103 -0
- package/src/organisms/data-table/data-table.tsx +105 -0
- package/src/organisms/dialog/dialog.android.tsx +6 -0
- package/src/organisms/dialog/dialog.ios.tsx +6 -0
- package/src/organisms/dialog/dialog.md +271 -0
- package/src/organisms/dialog/dialog.shared.tsx +230 -0
- package/src/organisms/dialog/dialog.styles.ts +272 -0
- package/src/organisms/dialog/dialog.tsx +6 -0
- package/src/organisms/filter-panel/filter-panel.md +116 -0
- package/src/organisms/filter-panel/filter-panel.styles.ts +83 -0
- package/src/organisms/filter-panel/filter-panel.tsx +91 -0
- package/src/organisms/index.ts +13 -0
- package/src/organisms/navbars/navbars.android.tsx +6 -0
- package/src/organisms/navbars/navbars.ios.tsx +6 -0
- package/src/organisms/navbars/navbars.md +144 -0
- package/src/organisms/navbars/navbars.shared.tsx +137 -0
- package/src/organisms/navbars/navbars.styles.ts +251 -0
- package/src/organisms/navbars/navbars.tsx +6 -0
- package/src/organisms/overlays/overlays.android.tsx +6 -0
- package/src/organisms/overlays/overlays.ios.tsx +6 -0
- package/src/organisms/overlays/overlays.md +123 -0
- package/src/organisms/overlays/overlays.shared.tsx +175 -0
- package/src/organisms/overlays/overlays.styles.ts +309 -0
- package/src/organisms/overlays/overlays.tsx +6 -0
- package/src/organisms/row-menu/row-menu.android.tsx +6 -0
- package/src/organisms/row-menu/row-menu.ios.tsx +6 -0
- package/src/organisms/row-menu/row-menu.md +102 -0
- package/src/organisms/row-menu/row-menu.shared.tsx +105 -0
- package/src/organisms/row-menu/row-menu.styles.ts +262 -0
- package/src/organisms/row-menu/row-menu.tsx +6 -0
- package/src/organisms/sidebar/sidebar.android.tsx +6 -0
- package/src/organisms/sidebar/sidebar.ios.tsx +6 -0
- package/src/organisms/sidebar/sidebar.md +188 -0
- package/src/organisms/sidebar/sidebar.shared.tsx +167 -0
- package/src/organisms/sidebar/sidebar.styles.ts +262 -0
- package/src/organisms/sidebar/sidebar.tsx +6 -0
- package/src/organisms/stepper/stepper.android.tsx +6 -0
- package/src/organisms/stepper/stepper.ios.tsx +6 -0
- package/src/organisms/stepper/stepper.md +150 -0
- package/src/organisms/stepper/stepper.shared.tsx +158 -0
- package/src/organisms/stepper/stepper.styles.ts +280 -0
- package/src/organisms/stepper/stepper.tsx +6 -0
- package/src/organisms/tabs/tabs.android.tsx +6 -0
- package/src/organisms/tabs/tabs.ios.tsx +6 -0
- package/src/organisms/tabs/tabs.md +127 -0
- package/src/organisms/tabs/tabs.shared.tsx +281 -0
- package/src/organisms/tabs/tabs.styles.ts +398 -0
- package/src/organisms/tabs/tabs.tsx +6 -0
- package/src/style/color.ts +17 -0
- package/src/style/index.ts +14 -0
- package/src/style/primitives.ts +26 -0
- package/src/style/responsive.ts +45 -0
- package/src/style/shadow.ts +21 -0
- package/src/style/theme.tsx +56 -0
- package/src/style/tokens.ts +487 -0
- package/src/theme.ts +21 -0
- package/styles/canvas.css +128 -67
- package/tsconfig.json +4 -2
- package/src/cn.ts +0 -3
- package/styles/base.css +0 -17
- package/styles/components/alert.css +0 -66
- package/styles/components/app-shell.css +0 -46
- package/styles/components/avatar.css +0 -15
- package/styles/components/badge.css +0 -83
- package/styles/components/breadcrumb.css +0 -35
- package/styles/components/button-group.css +0 -23
- package/styles/components/button.css +0 -107
- package/styles/components/calendar.css +0 -73
- package/styles/components/card.css +0 -58
- package/styles/components/checkbox.css +0 -55
- package/styles/components/code-block.css +0 -18
- package/styles/components/combobox.css +0 -75
- package/styles/components/command.css +0 -94
- package/styles/components/data-table.css +0 -142
- package/styles/components/dialog.css +0 -72
- package/styles/components/dropdown.css +0 -54
- package/styles/components/empty-state.css +0 -17
- package/styles/components/field.css +0 -27
- package/styles/components/filter-panel.css +0 -58
- package/styles/components/form.css +0 -27
- package/styles/components/icon.css +0 -8
- package/styles/components/input-group.css +0 -45
- package/styles/components/input.css +0 -56
- package/styles/components/kbd.css +0 -15
- package/styles/components/page-header.css +0 -52
- package/styles/components/pagination.css +0 -48
- package/styles/components/popover.css +0 -14
- package/styles/components/radio.css +0 -28
- package/styles/components/row-menu.css +0 -69
- package/styles/components/section-card.css +0 -49
- package/styles/components/select.css +0 -57
- package/styles/components/separator.css +0 -32
- package/styles/components/sheet.css +0 -70
- package/styles/components/sidebar.css +0 -146
- package/styles/components/skeleton.css +0 -32
- package/styles/components/spinner.css +0 -26
- package/styles/components/stat-card.css +0 -71
- package/styles/components/stepper.css +0 -63
- package/styles/components/switch.css +0 -45
- package/styles/components/tabs.css +0 -40
- package/styles/components/textarea.css +0 -31
- package/styles/components/toast.css +0 -95
- package/styles/components/tooltip.css +0 -53
- package/styles/components/topbar.css +0 -24
- package/styles/components/typography.css +0 -105
- package/styles/patterns/backdrops.css +0 -35
- package/styles/patterns/density.css +0 -66
- package/styles/patterns/focus.css +0 -38
- package/styles/patterns/glass.css +0 -85
- package/styles/patterns/high-contrast.css +0 -70
- package/styles/patterns/reduced-motion.css +0 -12
- package/styles/patterns/scrollbar.css +0 -10
- package/styles/reset.css +0 -89
- package/styles/tokens/colors.css +0 -106
- package/styles/tokens/motion.css +0 -33
- package/styles/tokens/radius.css +0 -10
- package/styles/tokens/shadows.css +0 -35
- package/styles/tokens/spacing.css +0 -19
- package/styles/tokens/typography.css +0 -6
- package/styles/tokens/z-index.css +0 -12
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { View, Image, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
|
|
3
|
+
import { Avatar } from "../../atoms/avatar/avatar.js";
|
|
4
|
+
import * as s from "./media-objects.styles.js";
|
|
5
|
+
import { type Align, type Direction } from "./media-objects.styles.js";
|
|
6
|
+
|
|
7
|
+
// A media object is a horizontal row: a leading media element (avatar, image, or
|
|
8
|
+
// icon glyph) sits beside a content column (a bold title, a muted description,
|
|
9
|
+
// and an optional longer supporting body), sometimes with a trailing action
|
|
10
|
+
// pinned to the right. It is the building block for list rows, notifications,
|
|
11
|
+
// and comment layouts.
|
|
12
|
+
//
|
|
13
|
+
// Boolean-prop API, grouped by axis with first-match precedence within an axis
|
|
14
|
+
// (mirrors Button's intentOf):
|
|
15
|
+
//
|
|
16
|
+
// - Alignment: `center` aligns the row's cross axis to the middle (the compact,
|
|
17
|
+
// single-line list/action row); the default top-aligns with items-start so the
|
|
18
|
+
// media anchors to the first line of a multi-line body (per the component's
|
|
19
|
+
// "Avatar" do/don't: items-start for multi-line, center for single-line rows).
|
|
20
|
+
// - Direction: `reversed` flips the media to the trailing edge; default leads
|
|
21
|
+
// with the media on the left.
|
|
22
|
+
// - Surface: `bordered` wraps the row in the card surface (border + padding) used
|
|
23
|
+
// when a media object stands alone as a card; omit for a bare row.
|
|
24
|
+
//
|
|
25
|
+
// State/layout booleans stack orthogonally: `truncate` clamps the title and
|
|
26
|
+
// description to one line each (the action pattern, so a long email never wraps
|
|
27
|
+
// and pushes the trailing action out of alignment).
|
|
28
|
+
|
|
29
|
+
export interface MediaObjectProps {
|
|
30
|
+
/** Primary line: the bold heading (e.g. a person's name). */
|
|
31
|
+
title?: string;
|
|
32
|
+
/** Secondary line: muted supporting text under the title (e.g. a role or email). */
|
|
33
|
+
description?: string;
|
|
34
|
+
/** Optional longer body paragraph rendered below the description. */
|
|
35
|
+
body?: ReactNode;
|
|
36
|
+
/** Trailing metadata text pinned to the right (e.g. "2h ago", "admin"). */
|
|
37
|
+
meta?: string;
|
|
38
|
+
/** Initials for the leading avatar (e.g. "RC"); rendered as <Avatar>{avatar}</Avatar>. */
|
|
39
|
+
avatar?: string;
|
|
40
|
+
/** Photo URL for the leading avatar; takes precedence over initials. */
|
|
41
|
+
src?: string;
|
|
42
|
+
/** A leading icon glyph rendered in a tinted square box (stands in for an SVG icon). */
|
|
43
|
+
icon?: ReactNode;
|
|
44
|
+
/** A trailing action node (e.g. a <Button>), pinned to the right edge. */
|
|
45
|
+
action?: ReactNode;
|
|
46
|
+
// Alignment (pick one; default top-aligns with items-start).
|
|
47
|
+
center?: boolean;
|
|
48
|
+
start?: boolean;
|
|
49
|
+
// Direction (pick one; default leads with the media on the left).
|
|
50
|
+
reversed?: boolean;
|
|
51
|
+
leading?: boolean;
|
|
52
|
+
// Surface.
|
|
53
|
+
bordered?: boolean;
|
|
54
|
+
// Layout.
|
|
55
|
+
truncate?: boolean;
|
|
56
|
+
/** Escape hatch for layout/positioning composition (width, margins). */
|
|
57
|
+
style?: StyleProp<ViewStyle>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Alignment precedence when more than one is passed: first match wins. Default is
|
|
61
|
+
// start (items-start) so the media anchors to the first line of a multi-line body.
|
|
62
|
+
function alignOf(p: MediaObjectProps): Align {
|
|
63
|
+
if (p.center) return "center";
|
|
64
|
+
if (p.start) return "start";
|
|
65
|
+
return "start";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Direction precedence when more than one is passed: first match wins.
|
|
69
|
+
function directionOf(p: MediaObjectProps): Direction {
|
|
70
|
+
if (p.reversed) return "reversed";
|
|
71
|
+
if (p.leading) return "leading";
|
|
72
|
+
return "leading";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function MediaObject(props: MediaObjectProps) {
|
|
76
|
+
const { title, description, body, meta, avatar, src, icon, action, truncate, style } = props;
|
|
77
|
+
const { tokens } = useTheme();
|
|
78
|
+
const align = alignOf(props);
|
|
79
|
+
const direction = directionOf(props);
|
|
80
|
+
|
|
81
|
+
const container: StyleProp<ViewStyle> = [
|
|
82
|
+
s.containerBase,
|
|
83
|
+
{ flexDirection: s.DIRECTION_ROW[direction], alignItems: s.ALIGN_ITEMS[align] },
|
|
84
|
+
props.bordered ? s.borderedSurface(tokens) : null,
|
|
85
|
+
style,
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// Leading media: photo > initials avatar > icon box. Only one renders.
|
|
89
|
+
let media: ReactNode = null;
|
|
90
|
+
if (src) {
|
|
91
|
+
media = (
|
|
92
|
+
<View style={s.photoBox(tokens)}>
|
|
93
|
+
<Image style={s.photoImage} source={{ uri: src }} accessibilityLabel={title} resizeMode="cover" />
|
|
94
|
+
</View>
|
|
95
|
+
);
|
|
96
|
+
} else if (avatar) {
|
|
97
|
+
media = <Avatar name={avatar}>{avatar}</Avatar>;
|
|
98
|
+
} else if (icon != null) {
|
|
99
|
+
media = (
|
|
100
|
+
<View style={s.iconBox(tokens)}>
|
|
101
|
+
{typeof icon === "string" ? <Text style={s.iconGlyph(tokens)}>{icon}</Text> : icon}
|
|
102
|
+
</View>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The engine has no truncate utility; RN clamps text via numberOfLines, which
|
|
107
|
+
// is the supported equivalent (single line with an ellipsis on overflow).
|
|
108
|
+
return (
|
|
109
|
+
<View style={container}>
|
|
110
|
+
{media}
|
|
111
|
+
<View style={s.content}>
|
|
112
|
+
{title != null ? (
|
|
113
|
+
<Text style={s.title(tokens)} numberOfLines={truncate ? 1 : undefined}>
|
|
114
|
+
{title}
|
|
115
|
+
</Text>
|
|
116
|
+
) : null}
|
|
117
|
+
{description != null ? (
|
|
118
|
+
<Text style={s.description(tokens)} numberOfLines={truncate ? 1 : undefined}>
|
|
119
|
+
{description}
|
|
120
|
+
</Text>
|
|
121
|
+
) : null}
|
|
122
|
+
{body != null ? <Text style={s.body(tokens)}>{body}</Text> : null}
|
|
123
|
+
</View>
|
|
124
|
+
{meta != null ? <Text style={s.meta(tokens)}>{meta}</Text> : null}
|
|
125
|
+
{action != null ? <View style={s.actionBox}>{action}</View> : null}
|
|
126
|
+
</View>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Stacked Lists
|
|
2
|
+
|
|
3
|
+
Vertical lists with avatar, two-line items, and trailing metadata. Used for contacts, activity feeds, and data previews.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<StackedList
|
|
9
|
+
items={[
|
|
10
|
+
{ name: "Rachel Chen", detail: "rachel.chen@example.com", meta: "admin" },
|
|
11
|
+
{ name: "Ada Lovelace", detail: "ada@example.com", meta: "editor" },
|
|
12
|
+
{ name: "Kevin Turner", detail: "kevin@example.com", meta: "viewer" }
|
|
13
|
+
]}
|
|
14
|
+
/>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Variants
|
|
18
|
+
|
|
19
|
+
### Variant - clickable
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<StackedList
|
|
23
|
+
items={[
|
|
24
|
+
{ name: "Rachel Chen", detail: "rachel.chen@example.com", meta: "2h ago" },
|
|
25
|
+
{ name: "Ada Lovelace", detail: "ada@example.com", meta: "5h ago" },
|
|
26
|
+
{ name: "Kevin Turner", detail: "kevin@example.com", meta: "1d ago" }
|
|
27
|
+
]}
|
|
28
|
+
clickable
|
|
29
|
+
/>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Variant - card
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
<StackedList
|
|
36
|
+
items={[
|
|
37
|
+
{ name: "Rachel Chen", detail: "Engineering Lead" },
|
|
38
|
+
{ name: "Ada Lovelace", detail: "Staff Engineer" }
|
|
39
|
+
]}
|
|
40
|
+
card
|
|
41
|
+
title="Team members"
|
|
42
|
+
addAction="Add"
|
|
43
|
+
rowMenu
|
|
44
|
+
/>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Do & Don't
|
|
48
|
+
|
|
49
|
+
### Two-line with avatar
|
|
50
|
+
|
|
51
|
+
**Do** — Primary line bold; secondary line smaller and muted, truncated so long values never wrap.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<StackedList items={[
|
|
55
|
+
{ name: "Rachel Chen", detail: "rachel.chen@example.com", initials: "RC" }
|
|
56
|
+
]} />
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Don't** — Equal weight on both lines flattens the hierarchy; the email competes with the name.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, overflow: "hidden", ...shadow("sm"), width: "100%", maxWidth: 560 }}>
|
|
63
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 12, paddingHorizontal: 20, paddingVertical: 12 }}>
|
|
64
|
+
<Avatar name="Rachel Chen">RC</Avatar>
|
|
65
|
+
<View style={{ minWidth: 0, flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
|
|
66
|
+
<Text style={{ fontSize: 13.5, fontWeight: "600", color: tokens.foreground }}>Rachel Chen</Text>
|
|
67
|
+
<Text style={{ fontSize: 13.5, fontWeight: "600", color: tokens.foreground }}>rachel.chen@example.com</Text>
|
|
68
|
+
</View>
|
|
69
|
+
</View>
|
|
70
|
+
</View>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Clickable
|
|
74
|
+
|
|
75
|
+
**Do** — Wrap the row in a link with a hover background and a trailing chevron to signal drilldown.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<StackedList clickable items={[
|
|
79
|
+
{ name: "Ada Lovelace", detail: "ada@example.com", meta: "5h ago", initials: "AL" }
|
|
80
|
+
]} />
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Don't** — A drilldown row with no hover state and no chevron gives no hint it is interactive.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
<StackedList items={[
|
|
87
|
+
{ name: "Ada Lovelace", detail: "ada@example.com", meta: "5h ago", initials: "AL" }
|
|
88
|
+
]} />
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Card surface group
|
|
92
|
+
|
|
93
|
+
**Do** — Separate the titled header with a rule and give each row a trailing action menu.
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
<StackedList card title="Team members" addAction="Add" rowMenu items={[
|
|
97
|
+
{ name: "Rachel Chen", detail: "Engineering Lead", initials: "RC" }
|
|
98
|
+
]} />
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Don't** — A header with no rule blends into the rows, and dropping the per-row action removes the affordance.
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
<View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, overflow: "hidden", ...shadow("sm"), width: "100%", maxWidth: 560 }}>
|
|
105
|
+
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 20, paddingVertical: 12 }}>
|
|
106
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "600", color: tokens.foreground }}>Team members</Text>
|
|
107
|
+
</View>
|
|
108
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 12, paddingHorizontal: 20, paddingVertical: 12 }}>
|
|
109
|
+
<Avatar name="Rachel Chen">RC</Avatar>
|
|
110
|
+
<View style={{ minWidth: 0, flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
|
|
111
|
+
<Text style={{ fontSize: 13.5, fontWeight: "600", color: tokens.foreground }}>Rachel Chen</Text>
|
|
112
|
+
<Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>Engineering Lead</Text>
|
|
113
|
+
</View>
|
|
114
|
+
</View>
|
|
115
|
+
</View>
|
|
116
|
+
```
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { type ViewStyle, type TextStyle } from "react-native";
|
|
2
|
+
import { type ColorTokens, shadow } from "../../style/index.js";
|
|
3
|
+
|
|
4
|
+
// Co-located StackedList styles. Layout-only fragments are static objects;
|
|
5
|
+
// anything that reads a color is a function of the active tokens (so the surface,
|
|
6
|
+
// dividers, and labels follow light/dark and read as glass when the
|
|
7
|
+
// ThemeProvider's surface is "glass", since tokens.card goes translucent there).
|
|
8
|
+
|
|
9
|
+
// --- outer frame ------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
// w-full max-w-[560px]: full width, capped at 560px.
|
|
12
|
+
export const outer: ViewStyle = { width: "100%", maxWidth: 560 };
|
|
13
|
+
|
|
14
|
+
// The card surface used by the `card` variant and as the optional frame when a
|
|
15
|
+
// title is supplied: rounded-lg border bg-card overflow-hidden shadow-sm.
|
|
16
|
+
export function cardSurface(tokens: ColorTokens): ViewStyle {
|
|
17
|
+
return {
|
|
18
|
+
borderRadius: 8,
|
|
19
|
+
borderWidth: 1,
|
|
20
|
+
borderColor: tokens.border,
|
|
21
|
+
backgroundColor: tokens.card,
|
|
22
|
+
overflow: "hidden",
|
|
23
|
+
...shadow("sm"),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- rows -------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
// flex-row items-center gap-3 px-5 py-3.
|
|
30
|
+
export const rowBase: ViewStyle = {
|
|
31
|
+
flexDirection: "row",
|
|
32
|
+
alignItems: "center",
|
|
33
|
+
gap: 12,
|
|
34
|
+
paddingHorizontal: 20,
|
|
35
|
+
paddingVertical: 12,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// border-b border-border: the hairline between ruled rows.
|
|
39
|
+
export function rowDivider(tokens: ColorTokens): ViewStyle {
|
|
40
|
+
return { borderBottomWidth: 1, borderColor: tokens.border };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// active:bg-accent press surface (clickable rows and the overflow menu button).
|
|
44
|
+
export function pressedSurface(tokens: ColorTokens): ViewStyle {
|
|
45
|
+
return { backgroundColor: tokens.accent };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// flex-1: the primary + secondary text column.
|
|
49
|
+
export const column: ViewStyle = { flexGrow: 1, flexShrink: 1, flexBasis: "0%" };
|
|
50
|
+
|
|
51
|
+
// --- text -------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
// text-sm font-medium text-foreground: the primary (name) line.
|
|
54
|
+
export function nameLabel(tokens: ColorTokens): TextStyle {
|
|
55
|
+
return { fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// text-xs text-muted-foreground: the secondary (detail) line, also the trailing
|
|
59
|
+
// meta text and the trailing chevron.
|
|
60
|
+
export function mutedLabel(tokens: ColorTokens): TextStyle {
|
|
61
|
+
return { fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --- header -----------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
// flex-row items-center justify-between border-b border-border px-5 py-3.
|
|
67
|
+
export function header(tokens: ColorTokens): ViewStyle {
|
|
68
|
+
return {
|
|
69
|
+
flexDirection: "row",
|
|
70
|
+
alignItems: "center",
|
|
71
|
+
justifyContent: "space-between",
|
|
72
|
+
borderBottomWidth: 1,
|
|
73
|
+
borderColor: tokens.border,
|
|
74
|
+
paddingHorizontal: 20,
|
|
75
|
+
paddingVertical: 12,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// text-sm font-semibold text-foreground: the header title.
|
|
80
|
+
export function headerTitle(tokens: ColorTokens): TextStyle {
|
|
81
|
+
return { fontSize: 14, lineHeight: 20, fontWeight: "600", color: tokens.foreground };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- add-action button ------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
// flex-row items-center gap-1.5: the leading-plus + label row inside addAction.
|
|
87
|
+
export const addActionRow: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 6 };
|
|
88
|
+
|
|
89
|
+
// text-xs font-medium text-foreground: the addAction button label.
|
|
90
|
+
export function addActionLabel(tokens: ColorTokens): TextStyle {
|
|
91
|
+
return { fontSize: 12, lineHeight: 16, fontWeight: "500", color: tokens.foreground };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- overflow ("...") menu --------------------------------------------------
|
|
95
|
+
|
|
96
|
+
// h-7 w-7 flex-row items-center justify-center gap-1 rounded-md bg-transparent.
|
|
97
|
+
export const menuButton: ViewStyle = {
|
|
98
|
+
height: 28,
|
|
99
|
+
width: 28,
|
|
100
|
+
flexDirection: "row",
|
|
101
|
+
alignItems: "center",
|
|
102
|
+
justifyContent: "center",
|
|
103
|
+
gap: 4,
|
|
104
|
+
borderRadius: 6,
|
|
105
|
+
backgroundColor: "transparent",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// h-1 w-1 rounded-full bg-foreground: one of the three overflow dots.
|
|
109
|
+
export function menuDot(tokens: ColorTokens): ViewStyle {
|
|
110
|
+
return { height: 4, width: 4, borderRadius: 9999, backgroundColor: tokens.foreground };
|
|
111
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type GestureResponderEvent } from "react-native";
|
|
3
|
+
import { View, Pressable, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
|
|
4
|
+
import { Avatar } from "../../atoms/avatar/avatar.js";
|
|
5
|
+
import { Badge } from "../../atoms/badge/badge.js";
|
|
6
|
+
import { Button } from "../../atoms/button/button.js";
|
|
7
|
+
import { Icon } from "../../atoms/icon/icon.js";
|
|
8
|
+
import * as s from "./stacked-lists.styles.js";
|
|
9
|
+
|
|
10
|
+
// A stacked list is a vertical list of rows separated by hairlines, each row a
|
|
11
|
+
// leading avatar, a primary + secondary text column, and trailing meta/badge/
|
|
12
|
+
// action. It is the building block for contact lists, activity feeds, and data
|
|
13
|
+
// previews.
|
|
14
|
+
//
|
|
15
|
+
// Boolean-prop API: one boolean per option, grouped by axis, first-match
|
|
16
|
+
// precedence within an axis (mirrors Button's intentOf).
|
|
17
|
+
//
|
|
18
|
+
// - Variant axis (pick one; default is the plain two-line list): `clickable`
|
|
19
|
+
// makes each row a pressable drilldown with a hover/press surface and a
|
|
20
|
+
// trailing chevron; `card` wraps the list in a titled card surface with an
|
|
21
|
+
// optional header. `clickable` wins over `card` when both are passed.
|
|
22
|
+
// - Divider (orthogonal boolean, on by default): draws a hairline between rows.
|
|
23
|
+
// Pass `flush` to drop the dividers. The card variant always keeps its rows
|
|
24
|
+
// ruled regardless of `flush`, matching the documented card surface group.
|
|
25
|
+
|
|
26
|
+
/** One row in the list. */
|
|
27
|
+
export interface StackedListItem {
|
|
28
|
+
/** Primary line, bold (e.g. a person's name). */
|
|
29
|
+
name: string;
|
|
30
|
+
/** Secondary line, smaller and muted (e.g. an email or role). */
|
|
31
|
+
detail: string;
|
|
32
|
+
/** Trailing metadata text (e.g. "2h ago"). Ignored when `badge` is set. */
|
|
33
|
+
meta?: string;
|
|
34
|
+
/** Trailing badge label; rendered as a <Badge> instead of plain meta text. */
|
|
35
|
+
badge?: string;
|
|
36
|
+
/** Photo URL for the avatar; falls back to initials when absent. */
|
|
37
|
+
avatar?: string;
|
|
38
|
+
/** Initials shown when there is no photo; derived from `name` when omitted. */
|
|
39
|
+
initials?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface StackedListProps {
|
|
43
|
+
/** Rows to render. */
|
|
44
|
+
items?: StackedListItem[];
|
|
45
|
+
/** Optional header title; shown above the rows, separated by a rule. */
|
|
46
|
+
title?: ReactNode;
|
|
47
|
+
/** Trailing header content (e.g. an action button); only shown with a title.
|
|
48
|
+
* Takes precedence over `addAction` when both are supplied. */
|
|
49
|
+
action?: ReactNode;
|
|
50
|
+
/** Convenience header action: renders a small outlined button with a leading
|
|
51
|
+
* plus icon and this label (e.g. "Add"). Only shown with a title and when
|
|
52
|
+
* `action` is not set. Serializable, so the playground can drive it. */
|
|
53
|
+
addAction?: string;
|
|
54
|
+
/** Appends a trailing ghost overflow ("...") action-menu button to every row.
|
|
55
|
+
* Press is reported through `onPressItemMenu`. */
|
|
56
|
+
rowMenu?: boolean;
|
|
57
|
+
/** Press handler for a row, by index. Only used in the `clickable` variant. */
|
|
58
|
+
onPressItem?: (index: number, event: GestureResponderEvent) => void;
|
|
59
|
+
/** Press handler for a row's overflow menu, by index. Used with `rowMenu`. */
|
|
60
|
+
onPressItemMenu?: (index: number, event: GestureResponderEvent) => void;
|
|
61
|
+
// Variant (pick one; default is the plain two-line list).
|
|
62
|
+
clickable?: boolean;
|
|
63
|
+
card?: boolean;
|
|
64
|
+
// Divider modifier: rows are ruled by default; `flush` removes the hairlines.
|
|
65
|
+
flush?: boolean;
|
|
66
|
+
/** Escape hatch for layout/positioning composition (mainly width). */
|
|
67
|
+
style?: StyleProp<ViewStyle>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type Variant = "two-line" | "clickable" | "card";
|
|
71
|
+
|
|
72
|
+
// Variant precedence when more than one is passed: first match wins.
|
|
73
|
+
function variantOf(p: StackedListProps): Variant {
|
|
74
|
+
if (p.clickable) return "clickable";
|
|
75
|
+
if (p.card) return "card";
|
|
76
|
+
return "two-line";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Two initials from a name, used when an item supplies no explicit initials and
|
|
80
|
+
// no photo (e.g. "Rachel Chen" -> "RC").
|
|
81
|
+
function initialsFrom(name: string): string {
|
|
82
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
83
|
+
if (parts.length === 0) return "";
|
|
84
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
85
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function StackedList(props: StackedListProps) {
|
|
89
|
+
const { items = [], title, action, addAction, rowMenu, onPressItem, onPressItemMenu, flush, style } = props;
|
|
90
|
+
const variant = variantOf(props);
|
|
91
|
+
const { tokens } = useTheme();
|
|
92
|
+
|
|
93
|
+
// The header action: an explicit ReactNode wins; otherwise a small outlined
|
|
94
|
+
// button with a leading plus icon when `addAction` supplies a label.
|
|
95
|
+
const headerAction =
|
|
96
|
+
action != null ? (
|
|
97
|
+
action
|
|
98
|
+
) : addAction != null ? (
|
|
99
|
+
<Button outline small>
|
|
100
|
+
<View style={s.addActionRow}>
|
|
101
|
+
<Icon plus size={13} />
|
|
102
|
+
<Text style={s.addActionLabel(tokens)}>{addAction}</Text>
|
|
103
|
+
</View>
|
|
104
|
+
</Button>
|
|
105
|
+
) : null;
|
|
106
|
+
|
|
107
|
+
// The card variant always rules its rows; the others rule unless `flush`.
|
|
108
|
+
const ruled = variant === "card" ? true : !flush;
|
|
109
|
+
const framed = variant === "card" || title != null;
|
|
110
|
+
|
|
111
|
+
const lastIndex = items.length - 1;
|
|
112
|
+
|
|
113
|
+
const renderColumn = (item: StackedListItem) => (
|
|
114
|
+
<View style={s.column}>
|
|
115
|
+
<Text style={s.nameLabel(tokens)} numberOfLines={1}>
|
|
116
|
+
{item.name}
|
|
117
|
+
</Text>
|
|
118
|
+
<Text style={s.mutedLabel(tokens)} numberOfLines={1}>
|
|
119
|
+
{item.detail}
|
|
120
|
+
</Text>
|
|
121
|
+
</View>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const renderTrailing = (item: StackedListItem) => {
|
|
125
|
+
if (item.badge != null) return <Badge secondary>{item.badge}</Badge>;
|
|
126
|
+
if (item.meta != null) return <Text style={s.mutedLabel(tokens)}>{item.meta}</Text>;
|
|
127
|
+
return null;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// A ghost overflow ("...") action-menu button drawn as three horizontal dots;
|
|
131
|
+
// the Icon set has no more-horizontal glyph, so the dots are primitives.
|
|
132
|
+
const renderMenu = (index: number) => (
|
|
133
|
+
<Pressable
|
|
134
|
+
style={({ pressed }) => [s.menuButton, pressed ? s.pressedSurface(tokens) : null]}
|
|
135
|
+
onPress={(event) => onPressItemMenu?.(index, event)}
|
|
136
|
+
accessibilityRole="button"
|
|
137
|
+
accessibilityLabel="Actions"
|
|
138
|
+
>
|
|
139
|
+
<View style={s.menuDot(tokens)} />
|
|
140
|
+
<View style={s.menuDot(tokens)} />
|
|
141
|
+
<View style={s.menuDot(tokens)} />
|
|
142
|
+
</Pressable>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const renderAvatar = (item: StackedListItem) => (
|
|
146
|
+
<Avatar src={item.avatar} name={item.name}>
|
|
147
|
+
{item.initials ?? initialsFrom(item.name)}
|
|
148
|
+
</Avatar>
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const rows = items.map((item, index) => {
|
|
152
|
+
const divider = ruled && index < lastIndex ? s.rowDivider(tokens) : null;
|
|
153
|
+
|
|
154
|
+
if (variant === "clickable") {
|
|
155
|
+
return (
|
|
156
|
+
<Pressable
|
|
157
|
+
key={index}
|
|
158
|
+
style={({ pressed }) => [s.rowBase, pressed ? s.pressedSurface(tokens) : null, divider]}
|
|
159
|
+
onPress={(event) => onPressItem?.(index, event)}
|
|
160
|
+
accessibilityRole="button"
|
|
161
|
+
>
|
|
162
|
+
{renderAvatar(item)}
|
|
163
|
+
{renderColumn(item)}
|
|
164
|
+
{renderTrailing(item)}
|
|
165
|
+
{rowMenu ? renderMenu(index) : null}
|
|
166
|
+
<Text style={s.mutedLabel(tokens)}>{"›"}</Text>
|
|
167
|
+
</Pressable>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<View key={index} style={[s.rowBase, divider]}>
|
|
173
|
+
{renderAvatar(item)}
|
|
174
|
+
{renderColumn(item)}
|
|
175
|
+
{renderTrailing(item)}
|
|
176
|
+
{rowMenu ? renderMenu(index) : null}
|
|
177
|
+
</View>
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const header =
|
|
182
|
+
title != null ? (
|
|
183
|
+
<View style={s.header(tokens)}>
|
|
184
|
+
<Text style={s.headerTitle(tokens)}>{title}</Text>
|
|
185
|
+
{headerAction != null ? <View>{headerAction}</View> : null}
|
|
186
|
+
</View>
|
|
187
|
+
) : null;
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<View style={[s.outer, framed ? s.cardSurface(tokens) : null, style]}>
|
|
191
|
+
{header}
|
|
192
|
+
{rows}
|
|
193
|
+
</View>
|
|
194
|
+
);
|
|
195
|
+
}
|