@admin-layout/gluestack-ui-mobile 12.2.4-alpha.86 → 12.2.5-alpha.13
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/CHANGELOG.md +9 -81
- package/lib/components/FlightCards/FlightCards.d.ts +40 -0
- package/lib/components/FlightCards/FlightCards.js +134 -0
- package/lib/components/FlightCards/FlightCards.js.map +1 -0
- package/lib/components/FlightCards/index.d.ts +2 -0
- package/lib/components/FlightCards/index.js +2 -0
- package/lib/components/FlightCards/index.js.map +1 -0
- package/lib/components/InputToolBar/InputToolBar.js +8 -4
- package/lib/components/InputToolBar/InputToolBar.js.map +1 -1
- package/lib/components/WithConfiguration.d.ts +1 -6
- package/lib/components/WithConfiguration.js +1 -4
- package/lib/components/WithConfiguration.js.map +1 -1
- package/lib/components/WithPermission.d.ts +1 -11
- package/lib/components/WithPermission.js +0 -8
- package/lib/components/WithPermission.js.map +1 -1
- package/lib/components/index.d.ts +1 -0
- package/lib/components/index.js +1 -0
- package/lib/components/index.js.map +1 -1
- package/lib/tailwind.config.js +3 -0
- package/lib/tailwind.config.js.map +1 -1
- package/lib/utils/generateMobileNavigations.js +37 -8
- package/lib/utils/generateMobileNavigations.js.map +1 -1
- package/package.json +6 -6
- package/src/components/FlightCards/FlightCards.tsx +266 -0
- package/src/components/FlightCards/index.ts +2 -0
- package/src/components/InputToolBar/InputToolBar.tsx +8 -4
- package/src/components/WithConfiguration.tsx +1 -5
- package/src/components/WithPermission.tsx +0 -9
- package/src/components/index.ts +1 -0
- package/src/tailwind.config.js +202 -198
- package/src/utils/generateMobileNavigations.ts +34 -6
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import React, { memo } from 'react';
|
|
2
|
+
import { Pressable, ScrollView, StyleSheet, Text, View, useColorScheme, useWindowDimensions } from 'react-native';
|
|
3
|
+
import Svg, { Line, Path } from 'react-native-svg';
|
|
4
|
+
import { toFlightCards } from '@admin-layout/client';
|
|
5
|
+
import type { FlightCardView, FlightOfferSetLike, FlightSliceView } from '@admin-layout/client';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Flight results for a mobile chat: the native twin of ai-chat-ext's web
|
|
9
|
+
* MessageFlights. Both render from @admin-layout/client's flight model, so a
|
|
10
|
+
* flight reads the same (times, +1, stops, price) on the web and in the app.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface FlightCardColors {
|
|
14
|
+
card: string;
|
|
15
|
+
border: string;
|
|
16
|
+
ink: string;
|
|
17
|
+
muted: string;
|
|
18
|
+
faint: string;
|
|
19
|
+
accent: string;
|
|
20
|
+
price: string;
|
|
21
|
+
nextDay: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const LIGHT: FlightCardColors = {
|
|
25
|
+
card: '#ffffff',
|
|
26
|
+
border: '#e5e7eb',
|
|
27
|
+
ink: '#111827',
|
|
28
|
+
muted: '#6b7280',
|
|
29
|
+
faint: '#9ca3af',
|
|
30
|
+
accent: '#3b82f6',
|
|
31
|
+
price: '#2563eb',
|
|
32
|
+
nextDay: '#d97706',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const DARK: FlightCardColors = {
|
|
36
|
+
card: '#1f2937',
|
|
37
|
+
border: '#374151',
|
|
38
|
+
ink: '#f9fafb',
|
|
39
|
+
muted: '#9ca3af',
|
|
40
|
+
faint: '#6b7280',
|
|
41
|
+
accent: '#60a5fa',
|
|
42
|
+
price: '#60a5fa',
|
|
43
|
+
nextDay: '#fbbf24',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const GAP = 12;
|
|
47
|
+
const SIDE = 8;
|
|
48
|
+
|
|
49
|
+
export interface FlightCardRowProps {
|
|
50
|
+
attachment: FlightOfferSetLike & { totalCount?: number; title?: string; actionLabel?: string };
|
|
51
|
+
/** Open a bookable offer. The host decides how: system browser, in-app browser. */
|
|
52
|
+
onOpenOffer?: (url: string) => void;
|
|
53
|
+
/** Open the full result set. Also used by a card that has no seller link. */
|
|
54
|
+
onViewAll?: () => void;
|
|
55
|
+
/** Override the palette; defaults follow the system light/dark scheme. */
|
|
56
|
+
colors?: Partial<FlightCardColors>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const Plane = ({ color }: { color: string }) => (
|
|
60
|
+
<Svg width={14} height={14} viewBox="0 0 24 24" style={styles.plane}>
|
|
61
|
+
<Path
|
|
62
|
+
fill={color}
|
|
63
|
+
d="M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"
|
|
64
|
+
/>
|
|
65
|
+
</Svg>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// An SVG line, not a dashed border: iOS does not draw borderStyle 'dashed'
|
|
69
|
+
// on a single side, so the route line would vanish on iPhones.
|
|
70
|
+
const Dashes = ({ color }: { color: string }) => (
|
|
71
|
+
<Svg height={2} width="100%" style={styles.dashes}>
|
|
72
|
+
<Line x1="0" y1="1" x2="100%" y2="1" stroke={color} strokeWidth={1} strokeDasharray="4 3" />
|
|
73
|
+
</Svg>
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const SliceRow = ({ slice, c }: { slice: FlightSliceView; c: FlightCardColors }) => {
|
|
77
|
+
return (
|
|
78
|
+
<View style={styles.slice}>
|
|
79
|
+
<View>
|
|
80
|
+
<Text style={[styles.time, { color: c.ink }]}>{slice.departTime ?? '--:--'}</Text>
|
|
81
|
+
<Text style={[styles.code, { color: c.muted }]}>{slice.origin}</Text>
|
|
82
|
+
</View>
|
|
83
|
+
<View style={styles.route} importantForAccessibility="no-hide-descendants" accessibilityElementsHidden>
|
|
84
|
+
<View style={styles.routeLine}>
|
|
85
|
+
<Dashes color={c.border} />
|
|
86
|
+
<Plane color={c.accent} />
|
|
87
|
+
<Dashes color={c.border} />
|
|
88
|
+
</View>
|
|
89
|
+
{slice.meta ? (
|
|
90
|
+
<Text style={[styles.meta, { color: c.faint }]} numberOfLines={1}>
|
|
91
|
+
{slice.meta}
|
|
92
|
+
</Text>
|
|
93
|
+
) : null}
|
|
94
|
+
</View>
|
|
95
|
+
<View style={styles.arrive}>
|
|
96
|
+
<View style={styles.arriveTime}>
|
|
97
|
+
<Text style={[styles.time, { color: c.ink }]}>{slice.arriveTime ?? '--:--'}</Text>
|
|
98
|
+
{slice.dayOffset ? (
|
|
99
|
+
<Text style={[styles.nextDay, { color: c.nextDay }]}>{slice.dayOffset}</Text>
|
|
100
|
+
) : null}
|
|
101
|
+
</View>
|
|
102
|
+
<Text style={[styles.code, styles.right, { color: c.muted }]}>{slice.destination}</Text>
|
|
103
|
+
</View>
|
|
104
|
+
</View>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const FlightCard = memo(function FlightCard({
|
|
109
|
+
card,
|
|
110
|
+
width,
|
|
111
|
+
colors: c,
|
|
112
|
+
onPress,
|
|
113
|
+
}: {
|
|
114
|
+
card: FlightCardView;
|
|
115
|
+
width: number | '100%';
|
|
116
|
+
colors: FlightCardColors;
|
|
117
|
+
onPress?: () => void;
|
|
118
|
+
}) {
|
|
119
|
+
const body = (
|
|
120
|
+
<>
|
|
121
|
+
<View style={styles.header}>
|
|
122
|
+
{card.sellerInitial ? (
|
|
123
|
+
<View style={[styles.badge, { backgroundColor: c.accent }]}>
|
|
124
|
+
<Text style={styles.badgeText}>{card.sellerInitial}</Text>
|
|
125
|
+
</View>
|
|
126
|
+
) : null}
|
|
127
|
+
<Text style={[styles.headerText, { color: c.muted }]} numberOfLines={1}>
|
|
128
|
+
{card.header}
|
|
129
|
+
</Text>
|
|
130
|
+
</View>
|
|
131
|
+
<View style={styles.slices}>
|
|
132
|
+
{card.slices.map((s, i) => (
|
|
133
|
+
<SliceRow key={i} slice={s} c={c} />
|
|
134
|
+
))}
|
|
135
|
+
</View>
|
|
136
|
+
<View style={[styles.footer, { borderTopColor: c.border }]}>
|
|
137
|
+
<Text style={[styles.footerText, { color: c.muted }]} numberOfLines={1}>
|
|
138
|
+
{card.footer}
|
|
139
|
+
</Text>
|
|
140
|
+
{card.price ? <Text style={[styles.price, { color: c.price }]}>{card.price}</Text> : null}
|
|
141
|
+
</View>
|
|
142
|
+
</>
|
|
143
|
+
);
|
|
144
|
+
const frame = [styles.card, { width, backgroundColor: c.card, borderColor: c.border }];
|
|
145
|
+
if (!onPress) {
|
|
146
|
+
return (
|
|
147
|
+
<View style={frame} accessible accessibilityLabel={card.accessibleLabel} testID="flight-card">
|
|
148
|
+
{body}
|
|
149
|
+
</View>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return (
|
|
153
|
+
<Pressable
|
|
154
|
+
onPress={onPress}
|
|
155
|
+
accessibilityRole={card.url ? 'link' : 'button'}
|
|
156
|
+
accessibilityLabel={card.accessibleLabel}
|
|
157
|
+
style={({ pressed }) => [...frame, pressed && styles.pressed]}
|
|
158
|
+
testID="flight-card"
|
|
159
|
+
>
|
|
160
|
+
{body}
|
|
161
|
+
</Pressable>
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const usePalette = (overrides?: Partial<FlightCardColors>): FlightCardColors => {
|
|
166
|
+
const dark = useColorScheme() === 'dark';
|
|
167
|
+
return { ...(dark ? DARK : LIGHT), ...overrides };
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** A swipeable row of flight cards; the next card peeks in so the row reads as scrollable. */
|
|
171
|
+
export function FlightCardRow({ attachment, onOpenOffer, onViewAll, colors }: FlightCardRowProps) {
|
|
172
|
+
const c = usePalette(colors);
|
|
173
|
+
const { width: screen } = useWindowDimensions();
|
|
174
|
+
const cards = toFlightCards(attachment);
|
|
175
|
+
if (cards.length === 0) return null;
|
|
176
|
+
|
|
177
|
+
const cardWidth = Math.round(Math.min(304, screen * 0.8));
|
|
178
|
+
const count = attachment.totalCount ?? attachment.properties?.length ?? cards.length;
|
|
179
|
+
const press = (card: FlightCardView) =>
|
|
180
|
+
card.url && onOpenOffer ? () => onOpenOffer(card.url as string) : onViewAll;
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<View style={styles.wrap} accessibilityLabel={attachment.title || 'Flight options'}>
|
|
184
|
+
<ScrollView
|
|
185
|
+
horizontal
|
|
186
|
+
nestedScrollEnabled
|
|
187
|
+
showsHorizontalScrollIndicator={false}
|
|
188
|
+
// One card per swipe, landing flush with the row's side inset.
|
|
189
|
+
snapToInterval={cardWidth + GAP}
|
|
190
|
+
snapToAlignment="start"
|
|
191
|
+
decelerationRate="fast"
|
|
192
|
+
contentContainerStyle={styles.row}
|
|
193
|
+
testID="flight-row"
|
|
194
|
+
>
|
|
195
|
+
{cards.map((card) => (
|
|
196
|
+
<FlightCard key={card.key} card={card} width={cardWidth} colors={c} onPress={press(card)} />
|
|
197
|
+
))}
|
|
198
|
+
</ScrollView>
|
|
199
|
+
{onViewAll && count > cards.length ? (
|
|
200
|
+
<Pressable onPress={onViewAll} accessibilityRole="button" hitSlop={8} style={styles.viewAll}>
|
|
201
|
+
<Text style={[styles.viewAllText, { color: c.price }]}>
|
|
202
|
+
{attachment.actionLabel || `View all ${count} flights`} ›
|
|
203
|
+
</Text>
|
|
204
|
+
</Pressable>
|
|
205
|
+
) : null}
|
|
206
|
+
</View>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Every flight as a full-width vertical list, for a results screen or sheet. */
|
|
211
|
+
export function FlightCardList({ attachment, onOpenOffer, onViewAll, colors }: FlightCardRowProps) {
|
|
212
|
+
const c = usePalette(colors);
|
|
213
|
+
const cards = toFlightCards(attachment);
|
|
214
|
+
if (cards.length === 0) return null;
|
|
215
|
+
return (
|
|
216
|
+
<View style={styles.list}>
|
|
217
|
+
{cards.map((card) => (
|
|
218
|
+
<FlightCard
|
|
219
|
+
key={card.key}
|
|
220
|
+
card={card}
|
|
221
|
+
width="100%"
|
|
222
|
+
colors={c}
|
|
223
|
+
onPress={card.url && onOpenOffer ? () => onOpenOffer(card.url as string) : onViewAll}
|
|
224
|
+
/>
|
|
225
|
+
))}
|
|
226
|
+
</View>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const styles = StyleSheet.create({
|
|
231
|
+
wrap: { marginVertical: 8 },
|
|
232
|
+
row: { paddingHorizontal: SIDE, paddingVertical: 4, gap: GAP },
|
|
233
|
+
list: { gap: GAP },
|
|
234
|
+
card: { borderWidth: 1, borderRadius: 16, padding: 16 },
|
|
235
|
+
pressed: { opacity: 0.85 },
|
|
236
|
+
header: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
|
237
|
+
badge: { width: 20, height: 20, borderRadius: 6, alignItems: 'center', justifyContent: 'center' },
|
|
238
|
+
badgeText: { color: '#ffffff', fontSize: 11, fontWeight: '700' },
|
|
239
|
+
headerText: { flex: 1, fontSize: 14 },
|
|
240
|
+
slices: { marginVertical: 16, gap: 16 },
|
|
241
|
+
slice: { flexDirection: 'row', alignItems: 'center', gap: 12 },
|
|
242
|
+
time: { fontSize: 24, fontWeight: '700', letterSpacing: -0.5, fontVariant: ['tabular-nums'] },
|
|
243
|
+
code: { marginTop: 4, fontSize: 14 },
|
|
244
|
+
right: { textAlign: 'right' },
|
|
245
|
+
route: { flex: 1, alignItems: 'center', minWidth: 0 },
|
|
246
|
+
routeLine: { flexDirection: 'row', alignItems: 'center', alignSelf: 'stretch', gap: 6 },
|
|
247
|
+
dashes: { flex: 1 },
|
|
248
|
+
plane: { transform: [{ rotate: '90deg' }] },
|
|
249
|
+
meta: { marginTop: 4, fontSize: 11 },
|
|
250
|
+
arrive: { alignItems: 'flex-end' },
|
|
251
|
+
arriveTime: { flexDirection: 'row', alignItems: 'flex-start' },
|
|
252
|
+
nextDay: { marginLeft: 2, fontSize: 11, fontWeight: '600' },
|
|
253
|
+
footer: {
|
|
254
|
+
marginTop: 'auto',
|
|
255
|
+
flexDirection: 'row',
|
|
256
|
+
alignItems: 'center',
|
|
257
|
+
justifyContent: 'space-between',
|
|
258
|
+
gap: 12,
|
|
259
|
+
borderTopWidth: 1,
|
|
260
|
+
paddingTop: 12,
|
|
261
|
+
},
|
|
262
|
+
footerText: { flex: 1, fontSize: 14 },
|
|
263
|
+
price: { fontSize: 18, fontWeight: '700', fontVariant: ['tabular-nums'] },
|
|
264
|
+
viewAll: { marginTop: 4, marginHorizontal: SIDE, alignSelf: 'flex-start' },
|
|
265
|
+
viewAllText: { fontSize: 14, fontWeight: '600' },
|
|
266
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useMemo, useState } from 'react';
|
|
2
|
-
import { TextInput, Modal, ScrollView, Pressable, Text as RNText, Dimensions } from 'react-native';
|
|
2
|
+
import { TextInput, Modal, ScrollView, Pressable, Text as RNText, Dimensions, useColorScheme } from 'react-native';
|
|
3
3
|
import { Box, HStack, VStack, Text, Pressable as GluestackPressable } from '@gluestack-ui/themed';
|
|
4
4
|
import { Ionicons, Feather } from '@expo/vector-icons';
|
|
5
5
|
import type {
|
|
@@ -541,6 +541,10 @@ export function InputToolBar({
|
|
|
541
541
|
const rightItems = rightItemsProp ?? [];
|
|
542
542
|
const hasInputContent = inputConfig != null || children != null;
|
|
543
543
|
const effectiveTopContent = inputConfig != null ? topContent : null;
|
|
544
|
+
const isDark = useColorScheme() === 'dark';
|
|
545
|
+
const composerBg = isDark ? '#1e293b' : '$white';
|
|
546
|
+
const composerText = isDark ? '#e2e8f0' : '#18181b';
|
|
547
|
+
const composerPlaceholder = isDark ? '#94a3b8' : '#71717a';
|
|
544
548
|
|
|
545
549
|
const templateModal =
|
|
546
550
|
templateModalConfig != null && templateModalConfig.isOpen
|
|
@@ -559,7 +563,7 @@ export function InputToolBar({
|
|
|
559
563
|
w="100%"
|
|
560
564
|
overflow="visible"
|
|
561
565
|
rounded="$3xl"
|
|
562
|
-
bg=
|
|
566
|
+
bg={composerBg}
|
|
563
567
|
shadowColor="$black"
|
|
564
568
|
shadowOffset={{ width: 0, height: 4 }}
|
|
565
569
|
shadowOpacity={0.08}
|
|
@@ -581,7 +585,7 @@ export function InputToolBar({
|
|
|
581
585
|
value={inputConfig.value}
|
|
582
586
|
onChange={inputConfig.onChange}
|
|
583
587
|
placeholder={inputConfig.placeholder ?? 'Ask anything...'}
|
|
584
|
-
placeholderTextColor=
|
|
588
|
+
placeholderTextColor={composerPlaceholder}
|
|
585
589
|
editable={!inputConfig.disabled}
|
|
586
590
|
multiline
|
|
587
591
|
numberOfLines={1}
|
|
@@ -589,7 +593,7 @@ export function InputToolBar({
|
|
|
589
593
|
width: '100%',
|
|
590
594
|
minHeight: 44,
|
|
591
595
|
fontSize: 16,
|
|
592
|
-
color:
|
|
596
|
+
color: composerText,
|
|
593
597
|
paddingVertical: 8,
|
|
594
598
|
paddingHorizontal: 0,
|
|
595
599
|
margin: 0,
|
|
@@ -16,7 +16,7 @@ export interface IWithConfigurationProps {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export const WithConfiguration = (props: IWithConfigurationProps) => {
|
|
19
|
-
const { configKey, children, permissionKeys, resourceName, permissionTypes } = props;
|
|
19
|
+
const { configKey, children, permissionKeys, resourceName, permissionTypes = [PermissionType.Allow] } = props;
|
|
20
20
|
|
|
21
21
|
const {
|
|
22
22
|
data,
|
|
@@ -65,8 +65,4 @@ export const WithConfiguration = (props: IWithConfigurationProps) => {
|
|
|
65
65
|
);
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
WithConfiguration.defaultProps = {
|
|
69
|
-
permissionTypes: [PermissionType.Allow],
|
|
70
|
-
};
|
|
71
|
-
|
|
72
68
|
export const WithConfigurationContainer = (props: IWithConfigurationProps) => <WithConfiguration {...props} />;
|
|
@@ -68,15 +68,6 @@ export const WithPermission = ({
|
|
|
68
68
|
return null;
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
WithPermission.defaultProps = {
|
|
72
|
-
permissionTypes: [PermissionType.Allow],
|
|
73
|
-
behaviour: WithPermissionBehaviour.hide,
|
|
74
|
-
message: "You don't have permission to access this resource, contact owner",
|
|
75
|
-
disabledProps: {
|
|
76
|
-
disabled: true,
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
|
|
80
71
|
export const WithPermissionContainer = React.memo((props: IWithPermissionProps) => <WithPermission {...props} />);
|
|
81
72
|
export default WithPermissionContainer;
|
|
82
73
|
// export const WithPermissionContainer = (props: IWithPermissionProps) => <WithPermission {...props} />;
|
package/src/components/index.ts
CHANGED