@alexisapp/leave-mobile 0.0.1-beta.1

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 (42) hide show
  1. package/README.md +133 -0
  2. package/package.json +43 -0
  3. package/src/components/ElementBoundary.tsx +45 -0
  4. package/src/components/ErrorFallback.tsx +70 -0
  5. package/src/components/cards/LeaveCard.tsx +119 -0
  6. package/src/components/cards/LeaveCardList.tsx +55 -0
  7. package/src/features/balance/components/BalanceTile.tsx +28 -0
  8. package/src/features/balance/components/BalanceTiles.tsx +44 -0
  9. package/src/features/balance/index.ts +1 -0
  10. package/src/features/history/components/LeaveHistory.tsx +10 -0
  11. package/src/features/history/index.ts +1 -0
  12. package/src/features/index.ts +3 -0
  13. package/src/features/upcoming/components/UpcomingLeaves.tsx +10 -0
  14. package/src/features/upcoming/index.ts +1 -0
  15. package/src/index.ts +8 -0
  16. package/src/providers/LeaveProvider.tsx +94 -0
  17. package/src/screens/LeaveScreen.tsx +58 -0
  18. package/src/screens/index.ts +2 -0
  19. package/src/theme/LeaveThemeContext.tsx +21 -0
  20. package/src/theme/index.ts +12 -0
  21. package/src/theme/tokens.ts +211 -0
  22. package/src/ui/components/Button.tsx +107 -0
  23. package/src/ui/components/Card.tsx +41 -0
  24. package/src/ui/components/DatePicker.tsx +220 -0
  25. package/src/ui/components/Dialog.tsx +186 -0
  26. package/src/ui/components/Drawer.tsx +71 -0
  27. package/src/ui/components/InlineItem.tsx +33 -0
  28. package/src/ui/components/Input.tsx +77 -0
  29. package/src/ui/components/RadioGroup.tsx +94 -0
  30. package/src/ui/components/ScrollArea.tsx +23 -0
  31. package/src/ui/components/Select.tsx +145 -0
  32. package/src/ui/components/Sheet.tsx +85 -0
  33. package/src/ui/components/State.tsx +110 -0
  34. package/src/ui/components/Tabs.tsx +115 -0
  35. package/src/ui/components/TextArea.tsx +80 -0
  36. package/src/ui/components/Tooltip.tsx +64 -0
  37. package/src/ui/components/Typography.tsx +30 -0
  38. package/src/ui/components/useToast.ts +20 -0
  39. package/src/ui/index.ts +17 -0
  40. package/src/ui/theme.ts +2 -0
  41. package/src/utils/__tests__/leaveStatusColorsUtils.test.ts +70 -0
  42. package/src/utils/leaveStatusColorsUtils.ts +34 -0
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # @leave/mobile
2
+
3
+ React Native package for the leave management feature. Consumed as an npm package by the `mobile-platform` host app.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ pnpm install
9
+ pnpm typecheck # tsc --noEmit
10
+ pnpm lint # eslint src/
11
+ pnpm test # vitest run
12
+ pnpm test:watch # vitest (watch mode)
13
+ ```
14
+
15
+ No build step — Metro resolves raw `.ts` via the `react-native` condition in `package.json`. The host compiles everything at bundle time.
16
+
17
+ ## Usage
18
+
19
+ The primary export is `LeaveScreen` — a self-contained screen that initializes the client, i18n, query cache, and theme, then renders balance tiles, upcoming leaves, and leave history.
20
+
21
+ ```tsx
22
+ import { LeaveScreen } from '@leave/mobile';
23
+
24
+ <LeaveScreen
25
+ endpoint="https://api-2.dev-alexishr.io/graphql"
26
+ gatewayEndpoint="https://api-2.dev-alexishr.io/v2/graphql"
27
+ locale="en-GB"
28
+ getToken={() => auth0.getAccessTokenSilently()}
29
+ onAuthError={() => navigation.navigate('Login')}
30
+ onDismiss={() => navigation.goBack()}
31
+ onError={(err) => analytics.track('leave_error', err)}
32
+ />;
33
+ ```
34
+
35
+ ### Props (`LeaveScreenProps`)
36
+
37
+ | Prop | Type | Required | Description |
38
+ | ----------------- | ---------------------------------------------------- | -------- | ----------------------------------------------------------- |
39
+ | `endpoint` | `string` | Yes | HR Core GraphQL endpoint URL |
40
+ | `gatewayEndpoint` | `string` | No | Gateway GraphQL endpoint URL |
41
+ | `locale` | `string` | No | Locale code (e.g. `en-GB`, `sv-SE`). Defaults to `en-GB` |
42
+ | `getToken` | `() => Promise<string \| null>` | No | Returns Auth0 JWT for authenticated requests |
43
+ | `onAuthError` | `() => void` | No | Called on 401/403 before throwing — host navigates to login |
44
+ | `devToken` | `string` | No | Hardcoded dev token. Takes precedence over `getToken` |
45
+ | `theme` | `LeaveTheme` | No | Custom theme tokens. Defaults to `defaultLeaveTheme` |
46
+ | `onDismiss` | `() => void` | Yes | Called when user taps close/dismiss |
47
+ | `onError` | `(error: { code: string; message: string }) => void` | No | Error callback for analytics/logging |
48
+
49
+ ## Architecture
50
+
51
+ ```
52
+ LeaveScreen
53
+ └── LeaveProvider # Initializes client, i18n, QueryClient, theme
54
+ └── LeaveScreenContent
55
+ ├── ElementBoundary + BalanceTiles
56
+ ├── ElementBoundary + UpcomingLeaves
57
+ └── ElementBoundary + LeaveHistory
58
+ ```
59
+
60
+ - **LeaveProvider** — wires up the `@leave/core` singletons (`initializeClient`, `initLeaveI18n`), TanStack Query `QueryClientProvider`, `react-i18next` `I18nextProvider`, and `LeaveThemeProvider`. Blocks rendering until singletons are ready. Invalidates leave queries on AppState foreground transition.
61
+ - **ElementBoundary** — per-section error/suspense boundary wrapping `AsyncBoundary` from `@leave/core/components`. Shows `ActivityIndicator` during loading, `ErrorFallback` on error.
62
+ - **ErrorFallback** — RN error view with i18n title/description, retry button (for transient errors), and dismiss button.
63
+
64
+ ## Source Layout
65
+
66
+ ```
67
+ src/
68
+ ├── components/ # ElementBoundary, ErrorFallback, cards (LeaveCard, LeaveCardList)
69
+ ├── features/ # Feature modules (balance, upcoming, history)
70
+ │ ├── balance/ # BalanceTiles, BalanceTile
71
+ │ ├── upcoming/ # UpcomingLeaves, useUpcomingLeaves
72
+ │ └── history/ # LeaveHistory, useHistoryLeaves
73
+ ├── hooks/ # useCurrentEmployeeId, useLeaveList
74
+ ├── providers/ # LeaveProvider
75
+ ├── screens/ # LeaveScreen (main entry point)
76
+ ├── theme/ # LeaveThemeProvider, tokens (defaultLeaveTheme, darkLeaveTheme)
77
+ ├── ui/ # Shared UI primitives (Button, Card, Input, Select, Dialog, etc.)
78
+ └── utils/ # formatDateRange, leaveStatus, splitLeaveSections
79
+ ```
80
+
81
+ ## Theming
82
+
83
+ The package ships with `defaultLeaveTheme` and `darkLeaveTheme`. Host apps can pass a custom `LeaveTheme` via the `theme` prop. Inside components, use `useLeaveTheme()` to access tokens:
84
+
85
+ ```tsx
86
+ import { useLeaveTheme } from '@leave/mobile';
87
+
88
+ const { colors, spacing, typography, radius, opacity } = useLeaveTheme();
89
+ ```
90
+
91
+ ## Exports
92
+
93
+ ```typescript
94
+ // Screen
95
+ export { LeaveScreen } from '@leave/mobile';
96
+ export type { LeaveScreenProps } from '@leave/mobile';
97
+
98
+ // Backwards compat alias
99
+ export { TimeOffScreen } from '@leave/mobile';
100
+
101
+ // Theme
102
+ export {
103
+ useLeaveTheme,
104
+ LeaveThemeProvider,
105
+ defaultLeaveTheme,
106
+ darkLeaveTheme,
107
+ } from '@leave/mobile';
108
+ export type { LeaveTheme } from '@leave/mobile';
109
+
110
+ // UI primitives
111
+ export { Button, Card, Typography, Input, Select, Dialog /* ... */ } from '@leave/mobile';
112
+ ```
113
+
114
+ ## Dependencies
115
+
116
+ | Package | Purpose |
117
+ | ----------------------- | ------------------------------------- |
118
+ | `@leave/core` | Client, queries, errors, i18n, domain |
119
+ | `@tanstack/react-query` | Server-state cache |
120
+ | `@tanstack/react-form` | Form state management |
121
+ | `zod` | Schema validation |
122
+ | `ky` | HTTP client |
123
+ | `i18next` | Internationalization |
124
+ | `react-i18next` | React bindings for i18next |
125
+ | `react-error-boundary` | Error boundary primitives |
126
+
127
+ ### Peer Dependencies
128
+
129
+ | Package | Range |
130
+ | -------------- | ---------------------- |
131
+ | `react` | `^18.0.0 \|\| ^19.0.0` |
132
+ | `react-native` | `>=0.73.0` |
133
+ | `expo` | `>=51.0.0` |
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@alexisapp/leave-mobile",
3
+ "version": "0.0.1-beta.1",
4
+ "main": "src/index.ts",
5
+ "react-native": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "files": [
8
+ "src/",
9
+ "package.json"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "react-native": "./src/index.ts",
14
+ "types": "./src/index.ts"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "typecheck": "tsc --noEmit",
19
+ "lint": "eslint src/",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest"
22
+ },
23
+ "peerDependencies": {
24
+ "react": "^18.0.0 || ^19.0.0",
25
+ "react-native": ">=0.73.0",
26
+ "expo": ">=51.0.0",
27
+ "@tanstack/react-query": "^5.0.0",
28
+ "react-error-boundary": "^5.0.0",
29
+ "zod": "^4.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "typescript": "~5.9.0",
33
+ "@types/react": "^19.1.0",
34
+ "react-native": ">=0.73.0",
35
+ "vitest": "^3.0.0",
36
+ "@tanstack/react-query": "^5.0.0",
37
+ "react-error-boundary": "^5.0.0"
38
+ },
39
+ "dependencies": {
40
+ "react-i18next": "^14.0.0",
41
+ "@alexisapp/leave-core": "0.0.1-beta.1"
42
+ }
43
+ }
@@ -0,0 +1,45 @@
1
+ import { ActivityIndicator, View } from 'react-native';
2
+ import type { PropsWithChildren, ReactNode } from 'react';
3
+ import type { LeaveError } from '@leave/core';
4
+ import { AsyncBoundary } from '@leave/core/components';
5
+ import { ErrorFallback } from './ErrorFallback';
6
+
7
+ interface ElementBoundaryProps {
8
+ suspenseFallback?: ReactNode;
9
+ onDismiss: () => void;
10
+ onError?: ((error: { code: string; message: string }) => void) | undefined;
11
+ }
12
+
13
+ export const ElementBoundary = ({
14
+ children,
15
+ suspenseFallback,
16
+ onDismiss,
17
+ onError,
18
+ }: PropsWithChildren<ElementBoundaryProps>) => (
19
+ <AsyncBoundary
20
+ suspenseFallback={
21
+ suspenseFallback || (
22
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
23
+ <ActivityIndicator size="large" />
24
+ </View>
25
+ )
26
+ }
27
+ errorBoundaryProps={{
28
+ fallbackRender: ({ error, resetErrorBoundary }) => (
29
+ <ErrorFallback
30
+ error={error}
31
+ resetErrorBoundary={resetErrorBoundary}
32
+ onDismiss={onDismiss}
33
+ />
34
+ ),
35
+ onError: (error) => {
36
+ onError?.({
37
+ code: (error as LeaveError).code ?? 'RENDER_ERROR',
38
+ message: error.message,
39
+ });
40
+ },
41
+ }}
42
+ >
43
+ {children}
44
+ </AsyncBoundary>
45
+ );
@@ -0,0 +1,70 @@
1
+ import { useMemo } from 'react';
2
+ import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
3
+ import { isRetryable, getErrorMessage } from '@leave/core';
4
+ import { useTranslation } from 'react-i18next';
5
+ import type { FallbackProps } from 'react-error-boundary';
6
+ import { useLeaveTheme } from '../theme';
7
+
8
+ interface ErrorFallbackProps extends FallbackProps {
9
+ onDismiss: () => void;
10
+ }
11
+
12
+ export function ErrorFallback({ error, resetErrorBoundary, onDismiss }: ErrorFallbackProps) {
13
+ const { colors, spacing, typography } = useLeaveTheme();
14
+ const retryable = isRetryable(error);
15
+ const { title, description } = getErrorMessage(error);
16
+ const { t } = useTranslation();
17
+
18
+ const styles = useMemo(
19
+ () =>
20
+ StyleSheet.create({
21
+ container: {
22
+ flex: 1,
23
+ justifyContent: 'center',
24
+ alignItems: 'center',
25
+ padding: spacing.xxl,
26
+ },
27
+ title: {
28
+ fontSize: typography.body.fontSize,
29
+ fontWeight: '600',
30
+ marginBottom: spacing.sm,
31
+ textAlign: 'center',
32
+ color: colors.textPrimary,
33
+ },
34
+ description: {
35
+ fontSize: typography.bodySmall.fontSize,
36
+ marginBottom: spacing.lg,
37
+ textAlign: 'center',
38
+ color: colors.textMuted,
39
+ },
40
+ retryText: { color: colors.primary, fontSize: typography.body.fontSize },
41
+ closeButton: { marginTop: spacing.md },
42
+ closeText: { color: colors.textMuted, fontSize: typography.bodySmall.fontSize },
43
+ }),
44
+ [colors, spacing, typography]
45
+ );
46
+
47
+ return (
48
+ <View style={styles.container} accessible accessibilityRole='alert'>
49
+ <Text style={styles.title}>{title}</Text>
50
+ <Text style={styles.description}>{description}</Text>
51
+ {retryable && (
52
+ <TouchableOpacity
53
+ onPress={resetErrorBoundary}
54
+ accessibilityLabel={t('retry')}
55
+ accessibilityRole='button'
56
+ >
57
+ <Text style={styles.retryText}>{t('retry')}</Text>
58
+ </TouchableOpacity>
59
+ )}
60
+ <TouchableOpacity
61
+ onPress={onDismiss}
62
+ style={styles.closeButton}
63
+ accessibilityLabel={t('close')}
64
+ accessibilityRole='button'
65
+ >
66
+ <Text style={styles.closeText}>{t('close')}</Text>
67
+ </TouchableOpacity>
68
+ </View>
69
+ );
70
+ }
@@ -0,0 +1,119 @@
1
+ import { useMemo } from 'react';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { useLeaveTheme } from '../../theme';
5
+ import { formatDateRange } from '@leave/core/utils';
6
+ import type { DisplayStatus } from '@leave/core/utils';
7
+ import { getStatusChipColors } from '../../utils/leaveStatusColorsUtils';
8
+
9
+ interface LeaveCardProps {
10
+ typeName: string;
11
+ displayStatus: DisplayStatus;
12
+ localStartDate: string;
13
+ localEndDate: string | null;
14
+ }
15
+
16
+ export function LeaveCard({
17
+ typeName,
18
+ displayStatus,
19
+ localStartDate,
20
+ localEndDate,
21
+ }: LeaveCardProps) {
22
+ const { colors, spacing, radius, typography, opacity } = useLeaveTheme();
23
+ const { t } = useTranslation();
24
+ const chipColors = getStatusChipColors(displayStatus, colors);
25
+
26
+ const statusLabel = t(`leave.status.${displayStatus}`);
27
+ const dateText = formatDateRange(localStartDate, localEndDate, t('leave.ongoing'));
28
+
29
+ const styles = useMemo(
30
+ () =>
31
+ StyleSheet.create({
32
+ card: {
33
+ backgroundColor: colors.surface,
34
+ borderRadius: radius.lg,
35
+ padding: spacing.md,
36
+ borderWidth: 1,
37
+ borderColor: colors.borderLight,
38
+ shadowColor: colors.shadow,
39
+ shadowOffset: { width: 0, height: 1 },
40
+ shadowOpacity: opacity.shadowLight,
41
+ shadowRadius: 2,
42
+ elevation: 1,
43
+ },
44
+ header: {
45
+ flexDirection: 'row',
46
+ justifyContent: 'space-between',
47
+ alignItems: 'center',
48
+ },
49
+ typeName: {
50
+ fontSize: typography.body.fontSize,
51
+ fontWeight: '600',
52
+ color: colors.textPrimary,
53
+ flex: 1,
54
+ marginRight: spacing.sm,
55
+ },
56
+ chip: {
57
+ flexDirection: 'row',
58
+ alignItems: 'center',
59
+ backgroundColor: chipColors.background,
60
+ borderWidth: 1,
61
+ borderColor: chipColors.border,
62
+ borderRadius: radius.xl,
63
+ paddingHorizontal: spacing.sm,
64
+ paddingVertical: spacing.xxs,
65
+ gap: spacing.xxs,
66
+ },
67
+ chipIcon: {
68
+ fontSize: typography.caption.fontSize,
69
+ color: chipColors.text,
70
+ },
71
+ chipText: {
72
+ fontSize: typography.caption.fontSize,
73
+ fontWeight: '500',
74
+ color: chipColors.text,
75
+ },
76
+ dates: {
77
+ fontSize: typography.bodySmall.fontSize,
78
+ color: colors.textMuted,
79
+ marginTop: spacing.xs,
80
+ },
81
+ }),
82
+ [colors, spacing, radius, typography, opacity, chipColors]
83
+ );
84
+
85
+ const chipIcon = getChipIcon(displayStatus);
86
+
87
+ return (
88
+ <View
89
+ style={styles.card}
90
+ accessibilityRole='summary'
91
+ accessibilityLabel={`${typeName}, ${statusLabel}, ${dateText}`}
92
+ >
93
+ <View style={styles.header}>
94
+ <Text style={styles.typeName} numberOfLines={1}>
95
+ {typeName}
96
+ </Text>
97
+ <View style={styles.chip}>
98
+ <Text style={styles.chipIcon}>{chipIcon}</Text>
99
+ <Text style={styles.chipText}>{statusLabel}</Text>
100
+ </View>
101
+ </View>
102
+ <Text style={styles.dates}>{dateText}</Text>
103
+ </View>
104
+ );
105
+ }
106
+
107
+ function getChipIcon(status: DisplayStatus): string {
108
+ switch (status) {
109
+ case 'APPROVED':
110
+ return '\u2713'; // ✓
111
+ case 'PENDING':
112
+ case 'PENDING_CHANGES':
113
+ case 'PENDING_REVOKE':
114
+ return '\u25F7'; // ◷
115
+ case 'DENIED':
116
+ case 'CANCELLED':
117
+ return '\u2717'; // ✗
118
+ }
119
+ }
@@ -0,0 +1,55 @@
1
+ import { useMemo } from 'react';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+ import type { LeaveListItem } from '@leave/core/hooks';
4
+ import { useLeaveTheme } from '../../theme';
5
+ import { LeaveCard } from './LeaveCard';
6
+
7
+ interface LeaveCardListProps {
8
+ title: string;
9
+ items: readonly LeaveListItem[];
10
+ }
11
+
12
+ export function LeaveCardList({ title, items }: LeaveCardListProps) {
13
+ const { colors, spacing, typography } = useLeaveTheme();
14
+
15
+ const styles = useMemo(
16
+ () =>
17
+ StyleSheet.create({
18
+ container: {
19
+ gap: spacing.sm,
20
+ },
21
+ header: {
22
+ fontSize: typography.caption.fontSize,
23
+ fontWeight: '600',
24
+ color: colors.textMuted,
25
+ textTransform: 'uppercase',
26
+ letterSpacing: 0.5,
27
+ },
28
+ list: {
29
+ gap: spacing.sm,
30
+ },
31
+ }),
32
+ [colors, spacing, typography]
33
+ );
34
+
35
+ if (items.length === 0) {
36
+ return null;
37
+ }
38
+
39
+ return (
40
+ <View style={styles.container}>
41
+ <Text style={styles.header}>{title}</Text>
42
+ <View style={styles.list}>
43
+ {items.map(item => (
44
+ <LeaveCard
45
+ key={item.id}
46
+ typeName={item.typeName}
47
+ displayStatus={item.displayStatus}
48
+ localStartDate={item.localStartDate}
49
+ localEndDate={item.localEndDate}
50
+ />
51
+ ))}
52
+ </View>
53
+ </View>
54
+ );
55
+ }
@@ -0,0 +1,28 @@
1
+ import { View } from 'react-native';
2
+ import { Card, Typography } from '../../../ui';
3
+ import { useLeaveTheme } from '../../../theme';
4
+
5
+ interface BalanceTileProps {
6
+ label: string;
7
+ value: number;
8
+ subtitle: string;
9
+ }
10
+
11
+ export function BalanceTile({ label, value, subtitle }: BalanceTileProps) {
12
+ const { spacing } = useLeaveTheme();
13
+ return (
14
+ <View style={{ flex: 1 }} accessibilityLabel={`${label}: ${value} ${subtitle}`}>
15
+ <Card>
16
+ <Typography variant="caption" style={{ textTransform: 'uppercase', letterSpacing: 1 }}>
17
+ {label}
18
+ </Typography>
19
+ <Typography variant="h2" style={{ marginTop: spacing.xs }}>
20
+ {String(value)}
21
+ </Typography>
22
+ <Typography variant="caption" style={{ marginTop: spacing.xxs }}>
23
+ {subtitle}
24
+ </Typography>
25
+ </Card>
26
+ </View>
27
+ );
28
+ }
@@ -0,0 +1,44 @@
1
+ import { useBalance } from '@leave/core/hooks';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { View } from 'react-native';
4
+ import { useLeaveTheme } from '../../../theme';
5
+ import { BalanceTile } from './BalanceTile';
6
+
7
+ export function BalanceTiles() {
8
+ const { spacing } = useLeaveTheme();
9
+ const { t } = useTranslation();
10
+ const balance = useBalance();
11
+
12
+ if (!balance) return null;
13
+
14
+ return (
15
+ <View style={{ gap: spacing.sm }}>
16
+ <View style={{ flexDirection: 'row', gap: spacing.sm }}>
17
+ <BalanceTile
18
+ label={t('balance.remaining.label')}
19
+ value={balance.remaining}
20
+ subtitle={t('balance.remaining.subtitle')}
21
+ />
22
+ <BalanceTile
23
+ label={t('balance.spent.label')}
24
+ value={balance.spent}
25
+ subtitle={t('balance.spent.subtitle')}
26
+ />
27
+ </View>
28
+ {balance.saved != null && balance.unpaid != null && (
29
+ <View style={{ flexDirection: 'row', gap: spacing.sm }}>
30
+ <BalanceTile
31
+ label={t('balance.saved.label')}
32
+ value={balance.saved}
33
+ subtitle={t('balance.saved.subtitle')}
34
+ />
35
+ <BalanceTile
36
+ label={t('balance.unpaid.label')}
37
+ value={balance.unpaid}
38
+ subtitle={t('balance.unpaid.subtitle')}
39
+ />
40
+ </View>
41
+ )}
42
+ </View>
43
+ );
44
+ }
@@ -0,0 +1 @@
1
+ export { BalanceTiles } from './components/BalanceTiles';
@@ -0,0 +1,10 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import { useLeaveList } from '@leave/core/hooks';
3
+ import { LeaveCardList } from '../../../components/cards/LeaveCardList';
4
+
5
+ export function LeaveHistory() {
6
+ const { history } = useLeaveList();
7
+ const { t } = useTranslation();
8
+
9
+ return <LeaveCardList title={t('leave.history')} items={history} />;
10
+ }
@@ -0,0 +1 @@
1
+ export { LeaveHistory } from './components/LeaveHistory';
@@ -0,0 +1,3 @@
1
+ export { BalanceTiles } from './balance';
2
+ export { UpcomingLeaves } from './upcoming';
3
+ export { LeaveHistory } from './history';
@@ -0,0 +1,10 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import { useLeaveList } from '@leave/core/hooks';
3
+ import { LeaveCardList } from '../../../components/cards/LeaveCardList';
4
+
5
+ export function UpcomingLeaves() {
6
+ const { upcoming } = useLeaveList();
7
+ const { t } = useTranslation();
8
+
9
+ return <LeaveCardList title={t('leave.upcoming')} items={upcoming} />;
10
+ }
@@ -0,0 +1 @@
1
+ export { UpcomingLeaves } from './components/UpcomingLeaves';
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export { LeaveScreen } from './screens/LeaveScreen';
2
+ export type { LeaveScreenProps } from './screens/LeaveScreen';
3
+ // Backwards compat
4
+ export { LeaveScreen as TimeOffScreen } from './screens/LeaveScreen';
5
+ export type { LeaveScreenProps as TimeOffScreenProps } from './screens/LeaveScreen';
6
+ export { useLeaveTheme, LeaveThemeProvider, defaultLeaveTheme, darkLeaveTheme } from './theme';
7
+ export type { LeaveTheme } from './theme';
8
+ export * from './ui';
@@ -0,0 +1,94 @@
1
+ import { type ReactNode, useEffect, useState } from 'react';
2
+ import { AppState, type AppStateStatus } from 'react-native';
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
+ import { I18nextProvider } from 'react-i18next';
5
+ import { initializeClient, resetClient } from '@leave/core';
6
+ import { initLeaveI18n, getLeaveI18n, resetLeaveI18n } from '@leave/core/i18n';
7
+ import { leaveQueries } from '@leave/core/queries';
8
+ import { LeaveThemeProvider, type LeaveTheme } from '../theme';
9
+
10
+ const queryClient = new QueryClient({
11
+ defaultOptions: {
12
+ queries: {
13
+ staleTime: 5 * 60 * 1000,
14
+ gcTime: 30 * 60 * 1000,
15
+ retry: 2,
16
+ refetchOnWindowFocus: false,
17
+ },
18
+ mutations: {
19
+ retry: 0,
20
+ },
21
+ },
22
+ });
23
+
24
+ export interface LeaveProviderProps {
25
+ children: ReactNode;
26
+ endpoint: string;
27
+ gatewayEndpoint?: string | undefined;
28
+ locale?: string | undefined;
29
+ getToken?: (() => Promise<string | null>) | undefined;
30
+ onAuthError?: (() => void) | undefined;
31
+ devToken?: string | undefined;
32
+ theme?: LeaveTheme | undefined;
33
+ }
34
+
35
+ export function LeaveProvider(props: LeaveProviderProps) {
36
+ const { children, endpoint, gatewayEndpoint, locale, getToken, onAuthError, devToken, theme } =
37
+ props;
38
+
39
+ const [initialized, setInitialized] = useState(false);
40
+ const [i18nReady, setI18nReady] = useState(false);
41
+
42
+ // Client initialization (ADR-011 singleton guard)
43
+ useEffect(() => {
44
+ initializeClient({ endpoint, gatewayEndpoint, getToken, onAuthError, devToken });
45
+ setInitialized(true);
46
+
47
+ return () => {
48
+ resetClient();
49
+ setInitialized(false);
50
+ };
51
+ }, [endpoint, gatewayEndpoint, getToken, onAuthError, devToken]);
52
+
53
+ // i18n initialization (ADR-011 singleton)
54
+ useEffect(() => {
55
+ let cancelled = false;
56
+
57
+ void initLeaveI18n(locale ?? 'en-GB').then(() => {
58
+ if (!cancelled) {
59
+ setI18nReady(true);
60
+ }
61
+ });
62
+
63
+ return () => {
64
+ cancelled = true;
65
+ resetLeaveI18n();
66
+ setI18nReady(false);
67
+ };
68
+ }, [locale]);
69
+
70
+ // AppState foreground refetch — replaces missing window.focus in RN
71
+ useEffect(() => {
72
+ let previousState: AppStateStatus = AppState.currentState;
73
+
74
+ const subscription = AppState.addEventListener('change', (nextState: AppStateStatus) => {
75
+ if (previousState.match(/inactive|background/) && nextState === 'active') {
76
+ void queryClient.invalidateQueries({ queryKey: leaveQueries.all });
77
+ }
78
+ previousState = nextState;
79
+ });
80
+
81
+ return () => subscription.remove();
82
+ }, []);
83
+
84
+ // State gate — prevent children from rendering before singletons are ready
85
+ if (!initialized || !i18nReady) return null;
86
+
87
+ return (
88
+ <QueryClientProvider client={queryClient}>
89
+ <I18nextProvider i18n={getLeaveI18n()}>
90
+ <LeaveThemeProvider theme={theme}>{children}</LeaveThemeProvider>
91
+ </I18nextProvider>
92
+ </QueryClientProvider>
93
+ );
94
+ }