@mr.dj2u/library-registry 0.0.0 → 0.2.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.
@@ -0,0 +1,198 @@
1
+ import { useState } from 'react';
2
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
3
+
4
+ import { useAppTheme } from '../../theme/provider';
5
+ import { LegalDocumentModal } from './legal-document-modal';
6
+ import { getLegalDocument, type LegalDocumentId } from './legal-documents';
7
+ import { useLegalAcceptance } from './use-legal-acceptance';
8
+
9
+ interface LegalAgreementScreenProps {
10
+ continueLabel?: string;
11
+ onComplete?: () => void;
12
+ }
13
+
14
+ function AgreementRow({
15
+ documentId,
16
+ accepted,
17
+ onOpen,
18
+ }: {
19
+ documentId: LegalDocumentId;
20
+ accepted: boolean;
21
+ onOpen: () => void;
22
+ }) {
23
+ const theme = useAppTheme();
24
+ const colors = theme.activeColors;
25
+ const document = getLegalDocument(documentId);
26
+
27
+ return (
28
+ <View
29
+ style={[
30
+ styles.card,
31
+ {
32
+ backgroundColor: colors.surface,
33
+ borderColor: accepted ? colors.success : colors.primary,
34
+ borderRadius: theme.layout.radius,
35
+ },
36
+ ]}>
37
+ <View style={styles.cardText}>
38
+ <Text style={[styles.cardTitle, { color: colors.text }]}>{document.title}</Text>
39
+ <Text style={[styles.cardSummary, { color: colors.text }]}>
40
+ Last updated {document.lastUpdated}
41
+ </Text>
42
+ </View>
43
+ <Pressable
44
+ accessibilityRole="button"
45
+ onPress={onOpen}
46
+ style={[
47
+ styles.reviewButton,
48
+ {
49
+ backgroundColor: accepted ? colors.success : colors.primary,
50
+ borderRadius: Math.max(8, theme.layout.radius - 4),
51
+ },
52
+ ]}>
53
+ <Text style={styles.reviewButtonText}>{accepted ? 'Accepted' : 'Review'}</Text>
54
+ </Pressable>
55
+ </View>
56
+ );
57
+ }
58
+
59
+ export function LegalAgreementScreen({
60
+ continueLabel = 'Continue',
61
+ onComplete,
62
+ }: LegalAgreementScreenProps) {
63
+ const theme = useAppTheme();
64
+ const colors = theme.activeColors;
65
+ const [activeDocument, setActiveDocument] = useState<LegalDocumentId | null>(null);
66
+ const {
67
+ acceptedDocuments,
68
+ acceptDocument,
69
+ hasAcceptedRequiredDocuments,
70
+ } = useLegalAcceptance();
71
+ const canContinue = hasAcceptedRequiredDocuments && Boolean(onComplete);
72
+
73
+ const closeModal = () => setActiveDocument(null);
74
+ const acceptActiveDocument = () => {
75
+ if (activeDocument) {
76
+ acceptDocument(activeDocument);
77
+ }
78
+ closeModal();
79
+ };
80
+
81
+ return (
82
+ <View style={[styles.screen, { backgroundColor: colors.background }]}>
83
+ <View style={styles.header}>
84
+ <Text style={[styles.title, { color: colors.text }]}>Legal agreements</Text>
85
+ <Text style={[styles.body, { color: colors.text }]}>
86
+ Review and accept the Terms of Service and Privacy Policy before continuing.
87
+ </Text>
88
+ </View>
89
+
90
+ <View style={styles.stack}>
91
+ <AgreementRow
92
+ accepted={acceptedDocuments.terms}
93
+ documentId="terms"
94
+ onOpen={() => setActiveDocument('terms')}
95
+ />
96
+ <AgreementRow
97
+ accepted={acceptedDocuments.privacy}
98
+ documentId="privacy"
99
+ onOpen={() => setActiveDocument('privacy')}
100
+ />
101
+ </View>
102
+
103
+ <Pressable
104
+ accessibilityRole="button"
105
+ disabled={!canContinue}
106
+ onPress={onComplete}
107
+ style={[
108
+ styles.continueButton,
109
+ {
110
+ backgroundColor: canContinue ? colors.primary : colors.surface,
111
+ borderRadius: theme.layout.radius,
112
+ },
113
+ ]}>
114
+ <Text
115
+ style={[
116
+ styles.continueButtonText,
117
+ { color: canContinue ? '#ffffff' : colors.text },
118
+ ]}>
119
+ {continueLabel}
120
+ </Text>
121
+ </Pressable>
122
+
123
+ {activeDocument ? (
124
+ <LegalDocumentModal
125
+ documentId={activeDocument}
126
+ onClose={closeModal}
127
+ onPrimaryAction={acceptActiveDocument}
128
+ primaryActionLabel={`Accept ${getLegalDocument(activeDocument).title}`}
129
+ visible
130
+ />
131
+ ) : null}
132
+ </View>
133
+ );
134
+ }
135
+
136
+ export default LegalAgreementScreen;
137
+
138
+ const styles = StyleSheet.create({
139
+ screen: {
140
+ flex: 1,
141
+ gap: 18,
142
+ padding: 20,
143
+ paddingTop: 72,
144
+ },
145
+ header: {
146
+ gap: 8,
147
+ },
148
+ title: {
149
+ fontSize: 28,
150
+ fontWeight: '900',
151
+ },
152
+ body: {
153
+ fontSize: 15,
154
+ lineHeight: 22,
155
+ },
156
+ stack: {
157
+ gap: 12,
158
+ },
159
+ card: {
160
+ alignItems: 'center',
161
+ borderWidth: 1,
162
+ flexDirection: 'row',
163
+ gap: 12,
164
+ padding: 14,
165
+ },
166
+ cardText: {
167
+ flex: 1,
168
+ gap: 4,
169
+ },
170
+ cardTitle: {
171
+ fontSize: 17,
172
+ fontWeight: '900',
173
+ },
174
+ cardSummary: {
175
+ fontSize: 13,
176
+ lineHeight: 18,
177
+ },
178
+ reviewButton: {
179
+ alignItems: 'center',
180
+ minWidth: 94,
181
+ paddingHorizontal: 12,
182
+ paddingVertical: 10,
183
+ },
184
+ reviewButtonText: {
185
+ color: '#ffffff',
186
+ fontSize: 13,
187
+ fontWeight: '900',
188
+ },
189
+ continueButton: {
190
+ alignItems: 'center',
191
+ marginTop: 'auto',
192
+ paddingVertical: 15,
193
+ },
194
+ continueButtonText: {
195
+ fontSize: 15,
196
+ fontWeight: '900',
197
+ },
198
+ });
@@ -0,0 +1,85 @@
1
+ import { Link } from 'expo-router';
2
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
3
+
4
+ import { useAppTheme } from '../../theme/provider';
5
+
6
+ interface LegalDocumentLinksProps {
7
+ orientation?: 'horizontal' | 'vertical';
8
+ showDescription?: boolean;
9
+ }
10
+
11
+ const legalLinks = [
12
+ {
13
+ href: '/terms',
14
+ label: 'Terms of Service',
15
+ description: 'Review app usage, account, and service terms.',
16
+ },
17
+ {
18
+ href: '/privacy',
19
+ label: 'Privacy Policy',
20
+ description: 'Review data processing, retention, and user choices.',
21
+ },
22
+ ] as const;
23
+
24
+ export function LegalDocumentLinks({
25
+ orientation = 'vertical',
26
+ showDescription = true,
27
+ }: LegalDocumentLinksProps) {
28
+ const theme = useAppTheme();
29
+ const colors = theme.activeColors;
30
+
31
+ return (
32
+ <View style={[styles.group, orientation === 'horizontal' && styles.horizontal]}>
33
+ {legalLinks.map((item) => (
34
+ <Link key={item.href} href={item.href} asChild>
35
+ <Pressable
36
+ accessibilityRole="link"
37
+ style={[
38
+ styles.linkCard,
39
+ {
40
+ backgroundColor: colors.surface,
41
+ borderColor: colors.primary,
42
+ borderRadius: theme.layout.radius,
43
+ },
44
+ orientation === 'horizontal' && styles.horizontalCard,
45
+ ]}>
46
+ <Text style={[styles.linkLabel, { color: colors.text }]}>{item.label}</Text>
47
+ {showDescription ? (
48
+ <Text style={[styles.linkDescription, { color: colors.text }]}>
49
+ {item.description}
50
+ </Text>
51
+ ) : null}
52
+ </Pressable>
53
+ </Link>
54
+ ))}
55
+ </View>
56
+ );
57
+ }
58
+
59
+ const styles = StyleSheet.create({
60
+ group: {
61
+ gap: 10,
62
+ },
63
+ horizontal: {
64
+ flexDirection: 'row',
65
+ flexWrap: 'wrap',
66
+ },
67
+ linkCard: {
68
+ borderWidth: 1,
69
+ gap: 4,
70
+ paddingHorizontal: 14,
71
+ paddingVertical: 12,
72
+ },
73
+ horizontalCard: {
74
+ flex: 1,
75
+ minWidth: 180,
76
+ },
77
+ linkLabel: {
78
+ fontSize: 15,
79
+ fontWeight: '900',
80
+ },
81
+ linkDescription: {
82
+ fontSize: 13,
83
+ lineHeight: 18,
84
+ },
85
+ });
@@ -0,0 +1,117 @@
1
+ import { Modal, Pressable, SafeAreaView, StyleSheet, Text, View } from 'react-native';
2
+
3
+ import { useAppTheme } from '../../theme/provider';
4
+ import { LegalDocumentView } from './legal-document-view';
5
+ import { getLegalDocument, type LegalDocumentId } from './legal-documents';
6
+
7
+ interface LegalDocumentModalProps {
8
+ documentId: LegalDocumentId;
9
+ visible: boolean;
10
+ onClose: () => void;
11
+ onPrimaryAction?: () => void;
12
+ primaryActionLabel?: string;
13
+ }
14
+
15
+ export function LegalDocumentModal({
16
+ documentId,
17
+ visible,
18
+ onClose,
19
+ onPrimaryAction,
20
+ primaryActionLabel = 'Done',
21
+ }: LegalDocumentModalProps) {
22
+ const theme = useAppTheme();
23
+ const colors = theme.activeColors;
24
+ const document = getLegalDocument(documentId);
25
+
26
+ return (
27
+ <Modal animationType="slide" presentationStyle="pageSheet" visible={visible}>
28
+ <SafeAreaView style={[styles.screen, { backgroundColor: colors.background }]}>
29
+ <View style={[styles.header, { borderBottomColor: colors.surface }]}>
30
+ <View style={styles.headerText}>
31
+ <Text style={[styles.title, { color: colors.text }]}>{document.title}</Text>
32
+ <Text style={[styles.updated, { color: colors.text }]}>
33
+ Last updated: {document.lastUpdated}
34
+ </Text>
35
+ </View>
36
+ <Pressable
37
+ accessibilityLabel={`Close ${document.title}`}
38
+ accessibilityRole="button"
39
+ onPress={onClose}
40
+ style={[
41
+ styles.closeButton,
42
+ {
43
+ backgroundColor: colors.surface,
44
+ borderRadius: Math.max(8, theme.layout.radius - 4),
45
+ },
46
+ ]}>
47
+ <Text style={[styles.closeText, { color: colors.text }]}>X</Text>
48
+ </Pressable>
49
+ </View>
50
+ <LegalDocumentView document={document} showHeader={false} />
51
+ <View style={[styles.footer, { borderTopColor: colors.surface }]}>
52
+ <Pressable
53
+ accessibilityRole="button"
54
+ onPress={onPrimaryAction ?? onClose}
55
+ style={[
56
+ styles.primaryButton,
57
+ {
58
+ backgroundColor: colors.primary,
59
+ borderRadius: theme.layout.radius,
60
+ },
61
+ ]}>
62
+ <Text style={styles.primaryButtonText}>{primaryActionLabel}</Text>
63
+ </Pressable>
64
+ </View>
65
+ </SafeAreaView>
66
+ </Modal>
67
+ );
68
+ }
69
+
70
+ const styles = StyleSheet.create({
71
+ screen: {
72
+ flex: 1,
73
+ },
74
+ header: {
75
+ alignItems: 'center',
76
+ borderBottomWidth: 1,
77
+ flexDirection: 'row',
78
+ gap: 12,
79
+ paddingHorizontal: 20,
80
+ paddingVertical: 16,
81
+ },
82
+ headerText: {
83
+ flex: 1,
84
+ gap: 3,
85
+ },
86
+ title: {
87
+ fontSize: 22,
88
+ fontWeight: '900',
89
+ },
90
+ updated: {
91
+ fontSize: 13,
92
+ lineHeight: 18,
93
+ },
94
+ closeButton: {
95
+ alignItems: 'center',
96
+ height: 40,
97
+ justifyContent: 'center',
98
+ width: 40,
99
+ },
100
+ closeText: {
101
+ fontSize: 15,
102
+ fontWeight: '900',
103
+ },
104
+ footer: {
105
+ borderTopWidth: 1,
106
+ padding: 20,
107
+ },
108
+ primaryButton: {
109
+ alignItems: 'center',
110
+ paddingVertical: 14,
111
+ },
112
+ primaryButtonText: {
113
+ color: '#ffffff',
114
+ fontSize: 15,
115
+ fontWeight: '900',
116
+ },
117
+ });
@@ -0,0 +1,230 @@
1
+ import type { ReactNode } from 'react';
2
+ import { ScrollView, StyleSheet, Text, View } from 'react-native';
3
+
4
+ import { useAppTheme } from '../../theme/provider';
5
+ import {
6
+ legalDocumentReplacementWarning,
7
+ type LegalDocument,
8
+ } from './legal-documents';
9
+
10
+ interface LegalDocumentViewProps {
11
+ document: LegalDocument;
12
+ footer?: ReactNode;
13
+ showHeader?: boolean;
14
+ }
15
+
16
+ function LegalDocumentMeta({ label, value }: { label: string; value: string }) {
17
+ const theme = useAppTheme();
18
+ const colors = theme.activeColors;
19
+
20
+ return (
21
+ <View
22
+ style={[
23
+ styles.metaItem,
24
+ {
25
+ backgroundColor: colors.surface,
26
+ borderColor: colors.surface,
27
+ borderRadius: theme.layout.radius,
28
+ },
29
+ ]}>
30
+ <Text style={[styles.metaLabel, { color: colors.text }]}>{label}</Text>
31
+ <Text style={[styles.metaValue, { color: colors.text }]}>{value}</Text>
32
+ </View>
33
+ );
34
+ }
35
+
36
+ function LegalSectionItem({ title, body }: { title: string; body: string[] }) {
37
+ const theme = useAppTheme();
38
+ const colors = theme.activeColors;
39
+
40
+ return (
41
+ <View
42
+ style={[
43
+ styles.section,
44
+ {
45
+ backgroundColor: colors.surface,
46
+ borderColor: colors.primary,
47
+ borderRadius: theme.layout.radius,
48
+ },
49
+ ]}>
50
+ <Text
51
+ style={[
52
+ styles.sectionTitle,
53
+ {
54
+ color: colors.text,
55
+ fontFamily: theme.typography.fontFamily,
56
+ },
57
+ ]}>
58
+ {title}
59
+ </Text>
60
+ <View style={styles.paragraphs}>
61
+ {body.map((paragraph, index) => (
62
+ <Text
63
+ key={`${title}-${index}`}
64
+ style={[
65
+ styles.sectionBody,
66
+ {
67
+ color: colors.text,
68
+ fontFamily: theme.typography.fontBody,
69
+ fontSize: theme.typography.bodySize,
70
+ },
71
+ ]}>
72
+ {paragraph}
73
+ </Text>
74
+ ))}
75
+ </View>
76
+ </View>
77
+ );
78
+ }
79
+
80
+ export function LegalDocumentView({
81
+ document,
82
+ footer,
83
+ showHeader = true,
84
+ }: LegalDocumentViewProps) {
85
+ const theme = useAppTheme();
86
+ const colors = theme.activeColors;
87
+
88
+ return (
89
+ <ScrollView
90
+ contentContainerStyle={styles.content}
91
+ style={[styles.screen, { backgroundColor: colors.background }]}>
92
+ <View style={styles.document}>
93
+ {showHeader ? (
94
+ <View style={styles.header}>
95
+ <Text
96
+ style={[
97
+ styles.title,
98
+ {
99
+ color: colors.text,
100
+ fontFamily: theme.typography.fontFamily,
101
+ },
102
+ ]}>
103
+ {document.title}
104
+ </Text>
105
+ <Text
106
+ style={[
107
+ styles.summary,
108
+ {
109
+ color: colors.text,
110
+ fontFamily: theme.typography.fontBody,
111
+ fontSize: theme.typography.bodySize,
112
+ },
113
+ ]}>
114
+ {document.summary}
115
+ </Text>
116
+ <View style={styles.metaRow}>
117
+ <LegalDocumentMeta label="Effective" value={document.effectiveDate} />
118
+ <LegalDocumentMeta label="Last updated" value={document.lastUpdated} />
119
+ </View>
120
+ </View>
121
+ ) : null}
122
+
123
+ <View
124
+ style={[
125
+ styles.warning,
126
+ {
127
+ backgroundColor: colors.surface,
128
+ borderColor: colors.warning,
129
+ borderRadius: theme.layout.radius,
130
+ },
131
+ ]}>
132
+ <Text style={[styles.warningTitle, { color: colors.text }]}>Replacement required</Text>
133
+ <Text
134
+ style={[
135
+ styles.warningBody,
136
+ {
137
+ color: colors.text,
138
+ fontFamily: theme.typography.fontBody,
139
+ },
140
+ ]}>
141
+ {legalDocumentReplacementWarning}
142
+ </Text>
143
+ </View>
144
+
145
+ {document.sections.map((section) => (
146
+ <LegalSectionItem key={section.id} title={section.title} body={section.body} />
147
+ ))}
148
+ {footer}
149
+ </View>
150
+ </ScrollView>
151
+ );
152
+ }
153
+
154
+ const styles = StyleSheet.create({
155
+ screen: {
156
+ flex: 1,
157
+ },
158
+ content: {
159
+ alignItems: 'center',
160
+ flexGrow: 1,
161
+ padding: 24,
162
+ paddingBottom: 48,
163
+ paddingTop: 48,
164
+ },
165
+ document: {
166
+ gap: 16,
167
+ maxWidth: 920,
168
+ width: '100%',
169
+ },
170
+ header: {
171
+ gap: 12,
172
+ },
173
+ title: {
174
+ fontSize: 30,
175
+ fontWeight: '900',
176
+ },
177
+ summary: {
178
+ fontSize: 15,
179
+ lineHeight: 22,
180
+ },
181
+ metaRow: {
182
+ flexDirection: 'row',
183
+ flexWrap: 'wrap',
184
+ gap: 10,
185
+ },
186
+ metaItem: {
187
+ borderWidth: 1,
188
+ gap: 2,
189
+ paddingHorizontal: 12,
190
+ paddingVertical: 9,
191
+ },
192
+ metaLabel: {
193
+ fontSize: 11,
194
+ fontWeight: '800',
195
+ textTransform: 'uppercase',
196
+ },
197
+ metaValue: {
198
+ fontSize: 13,
199
+ fontWeight: '800',
200
+ },
201
+ warning: {
202
+ borderWidth: 1,
203
+ gap: 6,
204
+ padding: 14,
205
+ },
206
+ warningTitle: {
207
+ fontSize: 14,
208
+ fontWeight: '900',
209
+ },
210
+ warningBody: {
211
+ fontSize: 14,
212
+ lineHeight: 20,
213
+ },
214
+ section: {
215
+ borderWidth: 1,
216
+ gap: 10,
217
+ padding: 16,
218
+ },
219
+ sectionTitle: {
220
+ fontSize: 18,
221
+ fontWeight: '900',
222
+ },
223
+ paragraphs: {
224
+ gap: 10,
225
+ },
226
+ sectionBody: {
227
+ fontSize: 15,
228
+ lineHeight: 23,
229
+ },
230
+ });