@wasto/cms-components 1.5.10

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.
Files changed (94) hide show
  1. package/README.md +47 -0
  2. package/dist/index.d.mts +465 -0
  3. package/dist/index.d.ts +465 -0
  4. package/dist/index.js +10445 -0
  5. package/dist/index.mjs +10553 -0
  6. package/package.json +90 -0
  7. package/src/components/announcement-modal/AnnouncementModal.tsx +183 -0
  8. package/src/components/article/Article.tsx +22 -0
  9. package/src/components/article/ArticleCard.tsx +81 -0
  10. package/src/components/article/ArticleExcerpts.tsx +80 -0
  11. package/src/components/article/ArticleList.tsx +114 -0
  12. package/src/components/article/ArticleMarkdown.tsx +170 -0
  13. package/src/components/article/ArticleReaderModal.tsx +93 -0
  14. package/src/components/article/ArticleSingle.tsx +57 -0
  15. package/src/components/bottom-tab/BottomTab.tsx +392 -0
  16. package/src/components/button/Button.tsx +154 -0
  17. package/src/components/challenge/Badges.tsx +84 -0
  18. package/src/components/challenge/BadgesModal.tsx +166 -0
  19. package/src/components/challenge/Challenge.tsx +380 -0
  20. package/src/components/challenge/ChallengeModal.tsx +231 -0
  21. package/src/components/challenge/ChallengeVignette.tsx +249 -0
  22. package/src/components/challenge/ChallengesModal.tsx +243 -0
  23. package/src/components/challenge/QuizModal.tsx +381 -0
  24. package/src/components/challenge/ResultModal.tsx +144 -0
  25. package/src/components/chat/Chat.tsx +928 -0
  26. package/src/components/collection-calendar/CollectionCalendar.tsx +274 -0
  27. package/src/components/columns/Columns.tsx +34 -0
  28. package/src/components/consignes-modal/ConsignesModal.tsx +267 -0
  29. package/src/components/content-inset/ContentInset.tsx +33 -0
  30. package/src/components/header/Header.tsx +391 -0
  31. package/src/components/image/Image.tsx +16 -0
  32. package/src/components/image/ImageFrame.tsx +102 -0
  33. package/src/components/image/ImageMedia.tsx +31 -0
  34. package/src/components/image/ImageUrl.tsx +30 -0
  35. package/src/components/locations-map/LocationSheet.tsx +417 -0
  36. package/src/components/locations-map/LocationsMap.native.tsx +260 -0
  37. package/src/components/locations-map/LocationsMap.tsx +13 -0
  38. package/src/components/locations-map/LocationsMap.web.tsx +10 -0
  39. package/src/components/locations-map/LocationsMapShell.tsx +527 -0
  40. package/src/components/locations-map/LocationsMapView.tsx +108 -0
  41. package/src/components/locations-map/LocationsMapView.web.tsx +331 -0
  42. package/src/components/nav-button/NavButton.tsx +59 -0
  43. package/src/components/next-collection/NextCollection.tsx +621 -0
  44. package/src/components/notifications/NotificationsScreen.tsx +247 -0
  45. package/src/components/page/Page.tsx +64 -0
  46. package/src/components/parapraph/Paragraph.tsx +67 -0
  47. package/src/components/profile/ProfileScreen.tsx +236 -0
  48. package/src/components/recycling-info/RecyclingInfo.tsx +189 -0
  49. package/src/components/safe-area/OptionalSafeAreaProvider.tsx +17 -0
  50. package/src/components/search/Search.tsx +320 -0
  51. package/src/components/select-street-modal/SelectStreetModal.tsx +475 -0
  52. package/src/components/title/Title.tsx +100 -0
  53. package/src/components/vision/Vision.tsx +447 -0
  54. package/src/components/vision/VisionActionCard.tsx +117 -0
  55. package/src/components/vision/VisionCapture.tsx +30 -0
  56. package/src/components/vision/VisionTapOverlay.tsx +122 -0
  57. package/src/contexts/AnnouncementsContext.tsx +171 -0
  58. package/src/contexts/ChallengesContext.tsx +182 -0
  59. package/src/contexts/LocalContext.tsx +227 -0
  60. package/src/contexts/ThemeContext.tsx +37 -0
  61. package/src/contexts/WastoContext.tsx +127 -0
  62. package/src/helpers/blogPost.ts +72 -0
  63. package/src/helpers/generateQuizQuestions.ts +247 -0
  64. package/src/helpers/getContrastColor.ts +19 -0
  65. package/src/helpers/haptics.ts +20 -0
  66. package/src/helpers/hoursSchedule.ts +230 -0
  67. package/src/helpers/htmlContentToMarkdown.ts +114 -0
  68. package/src/helpers/layout.ts +54 -0
  69. package/src/helpers/loadLinearGradient.ts +22 -0
  70. package/src/helpers/maquetteTheme.ts +76 -0
  71. package/src/helpers/markdownUnderlinePlugin.ts +32 -0
  72. package/src/helpers/normalizeWasteTypeRecyclingInfo.ts +80 -0
  73. package/src/helpers/publicMediaUrl.ts +8 -0
  74. package/src/helpers/resolveApiAssetUrl.ts +21 -0
  75. package/src/helpers/systemRoutes.ts +7 -0
  76. package/src/helpers/themeStyles.ts +114 -0
  77. package/src/helpers/useSafeAreaPadding.ts +14 -0
  78. package/src/hooks/useBadges.ts +100 -0
  79. package/src/hooks/useBlogPosts.ts +105 -0
  80. package/src/hooks/useChallenges.ts +110 -0
  81. package/src/hooks/useCities.ts +56 -0
  82. package/src/hooks/useCityAnnouncements.ts +118 -0
  83. package/src/hooks/useCollectionCalendar.ts +421 -0
  84. package/src/hooks/useColumns.ts +20 -0
  85. package/src/hooks/useConsignes.ts +62 -0
  86. package/src/hooks/useLocations.ts +248 -0
  87. package/src/hooks/useNextCollection.ts +238 -0
  88. package/src/hooks/useRecyclingInfo.ts +90 -0
  89. package/src/hooks/useWastoClient.ts +16 -0
  90. package/src/index.ts +142 -0
  91. package/src/navigation/NavigationContext.tsx +56 -0
  92. package/src/sentry/initSentry.ts +120 -0
  93. package/src/types/Theme.ts +72 -0
  94. package/src/types/css-modules.d.ts +1 -0
@@ -0,0 +1,274 @@
1
+ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react-native";
2
+ import { useCollectionCalendar, type WasteTypeInfoForDay } from "../../hooks/useCollectionCalendar";
3
+ import { StyleSheet, Text, View } from "react-native";
4
+ import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
5
+ import { NavButton } from "../nav-button/NavButton";
6
+ import { ContentInset } from "../content-inset/ContentInset";
7
+ import { Theme } from "../../types/Theme";
8
+ import { useTheme } from "../../contexts/ThemeContext";
9
+ import { resolveTheme } from "../../helpers/themeStyles";
10
+ import { NextCollection } from "../next-collection/NextCollection";
11
+ import { hapticSelection } from "../../helpers/haptics";
12
+ import { SPACE } from "../../helpers/layout";
13
+ import { useState, type ReactNode } from "react";
14
+
15
+ export type CollectionCalendarProps = {};
16
+
17
+ const PIN_H = 6;
18
+ const CELL_INSET = 4;
19
+
20
+ function colorAlpha(hex: string, alphaHex: string): string {
21
+ const h = hex.replace("#", "").trim();
22
+ if (h.length === 3) {
23
+ return `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}${alphaHex}`;
24
+ }
25
+ if (h.length === 6) return `#${h}${alphaHex}`;
26
+ if (h.length === 8) return `#${h.slice(0, 6)}${alphaHex}`;
27
+ return hex;
28
+ }
29
+
30
+ function WasteTypePins({ types }: { types: WasteTypeInfoForDay[] }) {
31
+ if (types.length === 0) return null;
32
+
33
+ return (
34
+ <View style={styles.wasteTypes}>
35
+ {types.map((wasteType) => (
36
+ <View
37
+ key={wasteType.wasteTypeId}
38
+ style={[styles.wasteType, { backgroundColor: wasteType.color }]}
39
+ />
40
+ ))}
41
+ </View>
42
+ );
43
+ }
44
+
45
+ export function CollectionCalendar({}: CollectionCalendarProps): ReactNode {
46
+ const {
47
+ weekDays,
48
+ weeks,
49
+ monthLabel,
50
+ goToNextMonth,
51
+ goToPrevMonth,
52
+ } = useCollectionCalendar();
53
+ const { theme } = useTheme();
54
+ const t = resolveTheme(theme);
55
+ const themedStyles = createThemedStyles(theme);
56
+ const [monthKey, setMonthKey] = useState(0);
57
+ const today = new Date();
58
+ const todayDay = today.getDate();
59
+ const todayMonth = today.getMonth();
60
+ const todayYear = today.getFullYear();
61
+ const exceptionFill = colorAlpha(theme.colors.error, "33");
62
+
63
+ const prev = async () => {
64
+ await hapticSelection();
65
+ goToPrevMonth();
66
+ setMonthKey((k) => k - 1);
67
+ };
68
+ const next = async () => {
69
+ await hapticSelection();
70
+ goToNextMonth();
71
+ setMonthKey((k) => k + 1);
72
+ };
73
+
74
+ return (
75
+ <View style={styles.collectionCalendar}>
76
+ <ContentInset>
77
+ <View style={styles.header}>
78
+ <Text style={[styles.monthLabel, { color: t.colors.muted }]}>
79
+ {monthLabel.toUpperCase()}
80
+ </Text>
81
+ <View style={styles.navRow}>
82
+ <NavButton
83
+ icon={ChevronLeftIcon}
84
+ onPress={prev}
85
+ style={styles.navCircle}
86
+ />
87
+ <NavButton
88
+ icon={ChevronRightIcon}
89
+ onPress={next}
90
+ style={styles.navCircle}
91
+ />
92
+ </View>
93
+ </View>
94
+ </ContentInset>
95
+
96
+ <Animated.View
97
+ key={monthKey}
98
+ entering={FadeIn.duration(220)}
99
+ exiting={FadeOut.duration(160)}
100
+ style={styles.grid}
101
+ >
102
+ <View style={[styles.calendarHeader, { backgroundColor: theme.colors.background }]}>
103
+ {weekDays.map((weekDay) => (
104
+ <View key={weekDay} style={styles.calendarHeaderCell}>
105
+ <Text style={themedStyles.mutedText}>{weekDay}</Text>
106
+ </View>
107
+ ))}
108
+ </View>
109
+ <View style={styles.calendarBody}>
110
+ {weeks.map((week, index) => (
111
+ <View
112
+ key={index}
113
+ style={[
114
+ styles.calendarBodyRow,
115
+ {
116
+ backgroundColor:
117
+ index % 2 === 0
118
+ ? theme.colors.surface
119
+ : theme.colors.background,
120
+ },
121
+ ]}
122
+ >
123
+ {week.map((day, cellIndex) => {
124
+ const isToday =
125
+ day.inCurrentMonth &&
126
+ day.day === todayDay &&
127
+ day.month === todayMonth &&
128
+ day.year === todayYear;
129
+ const isException = day.exceptions.length > 0;
130
+
131
+ return (
132
+ <View
133
+ key={cellIndex}
134
+ style={[
135
+ styles.calendarBodyCell,
136
+ !day.inCurrentMonth && styles.notCurrentMonth,
137
+ ]}
138
+ >
139
+ {isToday || isException ? (
140
+ <View
141
+ pointerEvents="none"
142
+ style={[
143
+ styles.dayHighlight,
144
+ {
145
+ backgroundColor: isToday
146
+ ? theme.colors.primary
147
+ : exceptionFill,
148
+ borderRadius: t.radii.sm,
149
+ },
150
+ ]}
151
+ />
152
+ ) : null}
153
+ <Text
154
+ style={[
155
+ styles.dayNumber,
156
+ isToday
157
+ ? {
158
+ color: theme.colors.onPrimary,
159
+ fontWeight: "700",
160
+ }
161
+ : isException
162
+ ? {
163
+ color: theme.colors.error,
164
+ fontWeight: "700",
165
+ }
166
+ : themedStyles.dayNumber,
167
+ ]}
168
+ >
169
+ {day.day}
170
+ </Text>
171
+ <WasteTypePins types={day.wasteTypesForDay} />
172
+ </View>
173
+ );
174
+ })}
175
+ </View>
176
+ ))}
177
+ </View>
178
+ </Animated.View>
179
+
180
+ <NextCollection showCalendarCta={false} />
181
+ </View>
182
+ );
183
+ }
184
+
185
+ const createThemedStyles = (theme: Theme) =>
186
+ StyleSheet.create({
187
+ dayNumber: {
188
+ color: theme.colors.muted ?? theme.colors.onSurface,
189
+ },
190
+ mutedText: {
191
+ color: theme.colors.muted ?? theme.colors.onSurface,
192
+ fontSize: 12,
193
+ fontWeight: "600",
194
+ },
195
+ });
196
+
197
+ const styles = StyleSheet.create({
198
+ collectionCalendar: {
199
+ gap: SPACE.sectionY,
200
+ width: "100%",
201
+ },
202
+ header: {
203
+ flexDirection: "row",
204
+ justifyContent: "space-between",
205
+ alignItems: "center",
206
+ paddingVertical: 12,
207
+ },
208
+ monthLabel: {
209
+ fontSize: 13,
210
+ fontWeight: "700",
211
+ letterSpacing: 0.6,
212
+ },
213
+ navRow: {
214
+ flexDirection: "row",
215
+ gap: 8,
216
+ },
217
+ navCircle: {
218
+ width: SPACE.iconHit,
219
+ height: SPACE.iconHit,
220
+ },
221
+ grid: {
222
+ width: "100%",
223
+ overflow: "hidden",
224
+ },
225
+ calendarHeader: {
226
+ flexDirection: "row",
227
+ },
228
+ calendarHeaderCell: {
229
+ flex: 1,
230
+ paddingVertical: 10,
231
+ alignItems: "center",
232
+ },
233
+ calendarBody: {
234
+ width: "100%",
235
+ },
236
+ calendarBodyRow: {
237
+ flexDirection: "row",
238
+ },
239
+ calendarBodyCell: {
240
+ flex: 1,
241
+ aspectRatio: 1,
242
+ position: "relative",
243
+ alignItems: "center",
244
+ justifyContent: "center",
245
+ },
246
+ dayHighlight: {
247
+ ...StyleSheet.absoluteFillObject,
248
+ margin: CELL_INSET,
249
+ zIndex: 0,
250
+ },
251
+ dayNumber: {
252
+ fontSize: 14,
253
+ fontWeight: "600",
254
+ zIndex: 1,
255
+ },
256
+ notCurrentMonth: {
257
+ opacity: 0.32,
258
+ },
259
+ wasteTypes: {
260
+ position: "absolute",
261
+ left: CELL_INSET,
262
+ right: CELL_INSET,
263
+ bottom: CELL_INSET + 2,
264
+ height: PIN_H,
265
+ flexDirection: "row",
266
+ borderRadius: PIN_H / 2,
267
+ overflow: "hidden",
268
+ zIndex: 2,
269
+ },
270
+ wasteType: {
271
+ flex: 1,
272
+ height: PIN_H,
273
+ },
274
+ });
@@ -0,0 +1,34 @@
1
+ import { Children, PropsWithChildren, type ReactNode } from "react"
2
+ import { StyleSheet, View } from "react-native";
3
+
4
+ export type ColumnsColumnCount = 2 | 3;
5
+
6
+ export type ColumnsProps = PropsWithChildren & {
7
+ columnCount: ColumnsColumnCount
8
+ }
9
+
10
+ export function Columns({
11
+ columnCount,
12
+ children
13
+ }: ColumnsProps): ReactNode {
14
+ if (Children.count(children) > columnCount) {
15
+ throw new Error(`Columns must have ${columnCount} (columnCount) children.`);
16
+ }
17
+
18
+ return <View style={styles.columns}>
19
+ {Children.map(children, (child) => (
20
+ <View style={styles.column}>{child}</View>
21
+ ))}
22
+ </View>
23
+ }
24
+
25
+ const styles = StyleSheet.create({
26
+ columns: {
27
+ flexDirection: "row",
28
+ width: "100%"
29
+ },
30
+ column: {
31
+ padding: 5,
32
+ flex: 1
33
+ },
34
+ });
@@ -0,0 +1,267 @@
1
+ import type { ReactNode } from "react";
2
+ import { Dimensions, Modal, ScrollView, StyleSheet, Text, View } from "react-native";
3
+ import { Image } from "expo-image";
4
+ import { useConsignes } from "../../hooks/useConsignes";
5
+ import { Title } from "../title/Title";
6
+ import { Theme } from "../../types/Theme";
7
+ import { useTheme } from "../../contexts/ThemeContext";
8
+ import { XIcon } from "lucide-react-native";
9
+ import { NavButton } from "../nav-button/NavButton";
10
+ import { getContrastColor } from "../../helpers/getContrastColor";
11
+ import { useSafeAreaPadding } from "../../helpers/useSafeAreaPadding";
12
+ import { groupedSectionStyle, resolveTheme } from "../../helpers/themeStyles";
13
+ import { resolveApiAssetUrl } from "../../helpers/resolveApiAssetUrl";
14
+ import { useWasto } from "../../contexts/WastoContext";
15
+
16
+ export type ConsignesModalProps = {
17
+ visible: boolean;
18
+ wasteTypeId: string;
19
+ onClose: () => void;
20
+ };
21
+
22
+ export function ConsignesModal({
23
+ visible,
24
+ wasteTypeId,
25
+ onClose
26
+ }: ConsignesModalProps): ReactNode {
27
+ const {
28
+ wasteType,
29
+ } = useConsignes(wasteTypeId);
30
+ const { theme } = useTheme();
31
+ const { apiUrl } = useWasto();
32
+ const t = resolveTheme(theme);
33
+ const themedStyles = createThemedStyles(theme);
34
+ const topInset = useSafeAreaPadding(["top"]);
35
+ const bottomInset = useSafeAreaPadding(["bottom"]);
36
+ const headerColor = wasteType?.color ?? theme.colors.primary;
37
+ const contrast = getContrastColor(headerColor);
38
+ const details = wasteType?.details ?? [];
39
+ const rules = wasteType?.rules ?? [];
40
+ const headerImageUrl = resolveApiAssetUrl(apiUrl, wasteType?.imageUrl);
41
+
42
+ return <Modal
43
+ visible={visible}
44
+ animationType="slide"
45
+ onRequestClose={onClose}
46
+ >
47
+ <View style={[styles.modal, themedStyles.modal]}>
48
+ <View
49
+ style={[
50
+ themedStyles.headerSafeArea,
51
+ { backgroundColor: headerColor, borderBottomLeftRadius: theme.radii.lg, borderBottomRightRadius: theme.radii.lg },
52
+ topInset,
53
+ ]}
54
+ >
55
+ <View style={styles.header}>
56
+ <View style={styles.headerTitleRow}>
57
+ {headerImageUrl ? (
58
+ <Image
59
+ source={headerImageUrl}
60
+ style={styles.headerImage}
61
+ contentFit="contain"
62
+ />
63
+ ) : null}
64
+ <Title
65
+ text={wasteType?.name ?? ""}
66
+ level="2"
67
+ style={{ color: contrast, flex: 1 }}
68
+ numberOfLines={2}
69
+ />
70
+ </View>
71
+ <NavButton
72
+ icon={XIcon}
73
+ variant="close"
74
+ onPress={onClose}
75
+ />
76
+ </View>
77
+ </View>
78
+ <View style={[styles.bodyWrap, bottomInset]}>
79
+ <ScrollView bounces={false} contentContainerStyle={styles.body}>
80
+ {details.length > 0 ? (
81
+ <View style={styles.section}>
82
+ <Text style={[styles.sectionLabel, { color: t.colors.muted }]}>
83
+ Matériaux
84
+ </Text>
85
+ <View style={groupedSectionStyle(theme)}>
86
+ {details.map((detail, index) => {
87
+ const detailImageUrl = resolveApiAssetUrl(
88
+ apiUrl,
89
+ detail.imageUrl,
90
+ );
91
+ return (
92
+ <View key={detail.id}>
93
+ <View style={styles.detailRow}>
94
+ {detailImageUrl ? (
95
+ <Image
96
+ source={detailImageUrl}
97
+ style={styles.detailImage}
98
+ contentFit="contain"
99
+ />
100
+ ) : (
101
+ <View
102
+ style={[
103
+ styles.detailDot,
104
+ { backgroundColor: headerColor },
105
+ ]}
106
+ />
107
+ )}
108
+ <View style={styles.detailText}>
109
+ {detail.material ? (
110
+ <Text style={themedStyles.detailTitle}>
111
+ {detail.material}
112
+ </Text>
113
+ ) : null}
114
+ {detail.description ? (
115
+ <Text style={themedStyles.detailBody}>
116
+ {detail.description}
117
+ </Text>
118
+ ) : null}
119
+ </View>
120
+ </View>
121
+ {index < details.length - 1 ? (
122
+ <View
123
+ style={[
124
+ styles.rowDivider,
125
+ {
126
+ backgroundColor: theme.colors.border,
127
+ marginLeft: detailImageUrl ? 64 : 42,
128
+ },
129
+ ]}
130
+ />
131
+ ) : null}
132
+ </View>
133
+ );
134
+ })}
135
+ </View>
136
+ </View>
137
+ ) : null}
138
+
139
+ {rules.length > 0 ? (
140
+ <View style={styles.section}>
141
+ <Text style={[styles.sectionLabel, { color: t.colors.muted }]}>
142
+ Règles
143
+ </Text>
144
+ <View style={[groupedSectionStyle(theme), styles.rulesGroup]}>
145
+ {rules.map((rule, index) => (
146
+ <View key={index} style={styles.ruleRow}>
147
+ <View
148
+ style={[
149
+ styles.ruleBullet,
150
+ { backgroundColor: headerColor },
151
+ ]}
152
+ />
153
+ <Text style={themedStyles.detailBody}>{rule}</Text>
154
+ </View>
155
+ ))}
156
+ </View>
157
+ </View>
158
+ ) : null}
159
+ </ScrollView>
160
+ </View>
161
+ </View>
162
+ </Modal>
163
+ }
164
+
165
+ const createThemedStyles = (theme: Theme) => StyleSheet.create({
166
+ modal: {
167
+ backgroundColor: theme.colors.background,
168
+ },
169
+ headerSafeArea: {},
170
+ detailTitle: {
171
+ color: theme.colors.onSurface,
172
+ fontSize: 15,
173
+ fontWeight: "600",
174
+ },
175
+ detailBody: {
176
+ color: theme.colors.onSurface,
177
+ fontSize: 14,
178
+ lineHeight: 20,
179
+ opacity: 0.85,
180
+ flex: 1,
181
+ },
182
+ });
183
+
184
+ const styles = StyleSheet.create({
185
+ modal: {
186
+ height: Dimensions.get("screen").height
187
+ },
188
+ header: {
189
+ paddingHorizontal: 16,
190
+ paddingVertical: 16,
191
+ width: "100%",
192
+ flexDirection: "row",
193
+ justifyContent: "space-between",
194
+ alignItems: "center",
195
+ gap: 12,
196
+ },
197
+ headerTitleRow: {
198
+ flex: 1,
199
+ flexDirection: "row",
200
+ alignItems: "center",
201
+ gap: 12,
202
+ },
203
+ headerImage: {
204
+ width: 56,
205
+ height: 56,
206
+ },
207
+ bodyWrap: {
208
+ flex: 1,
209
+ },
210
+ body: {
211
+ padding: 16,
212
+ flexGrow: 1,
213
+ gap: 20,
214
+ paddingBottom: 28,
215
+ },
216
+ section: {
217
+ gap: 8,
218
+ },
219
+ sectionLabel: {
220
+ fontSize: 12,
221
+ fontWeight: "600",
222
+ textTransform: "uppercase",
223
+ letterSpacing: 0.6,
224
+ paddingHorizontal: 4,
225
+ },
226
+ detailRow: {
227
+ flexDirection: "row",
228
+ alignItems: "flex-start",
229
+ gap: 12,
230
+ paddingHorizontal: 14,
231
+ paddingVertical: 14,
232
+ },
233
+ detailImage: {
234
+ width: 40,
235
+ height: 40,
236
+ },
237
+ detailDot: {
238
+ width: 10,
239
+ height: 10,
240
+ borderRadius: 5,
241
+ marginTop: 5,
242
+ },
243
+ detailText: {
244
+ flex: 1,
245
+ gap: 4,
246
+ },
247
+ rowDivider: {
248
+ height: StyleSheet.hairlineWidth,
249
+ },
250
+ rulesGroup: {
251
+ paddingVertical: 8,
252
+ paddingHorizontal: 14,
253
+ gap: 10,
254
+ },
255
+ ruleRow: {
256
+ flexDirection: "row",
257
+ alignItems: "flex-start",
258
+ gap: 10,
259
+ paddingVertical: 4,
260
+ },
261
+ ruleBullet: {
262
+ width: 6,
263
+ height: 6,
264
+ borderRadius: 3,
265
+ marginTop: 7,
266
+ },
267
+ });
@@ -0,0 +1,33 @@
1
+ import { PropsWithChildren, type ReactNode } from "react";
2
+ import { StyleSheet, View, type StyleProp, type ViewStyle } from "react-native";
3
+ import { SPACE } from "../../helpers/layout";
4
+
5
+ export type ContentInsetProps = PropsWithChildren & {
6
+ /** Use 12px horizontal instead of 16px */
7
+ tight?: boolean;
8
+ style?: StyleProp<ViewStyle>;
9
+ };
10
+
11
+ /**
12
+ * Horizontal page inset applied per block (not on Page).
13
+ * Full-bleed components (map canvas, calendar grid) skip this wrapper.
14
+ */
15
+ export function ContentInset({ children, tight = false, style }: ContentInsetProps): ReactNode {
16
+ return (
17
+ <View
18
+ style={[
19
+ styles.base,
20
+ { paddingHorizontal: tight ? SPACE.pageXTight : SPACE.pageX },
21
+ style,
22
+ ]}
23
+ >
24
+ {children}
25
+ </View>
26
+ );
27
+ }
28
+
29
+ const styles = StyleSheet.create({
30
+ base: {
31
+ width: "100%",
32
+ },
33
+ });