@nextsparkjs/mobile 0.1.0-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 (123) hide show
  1. package/README.md +339 -0
  2. package/dist/api/client.d.ts +102 -0
  3. package/dist/api/client.js +189 -0
  4. package/dist/api/client.js.map +1 -0
  5. package/dist/api/client.types.d.ts +39 -0
  6. package/dist/api/client.types.js +12 -0
  7. package/dist/api/client.types.js.map +1 -0
  8. package/dist/api/core/auth.d.ts +26 -0
  9. package/dist/api/core/auth.js +52 -0
  10. package/dist/api/core/auth.js.map +1 -0
  11. package/dist/api/core/index.d.ts +4 -0
  12. package/dist/api/core/index.js +5 -0
  13. package/dist/api/core/index.js.map +1 -0
  14. package/dist/api/core/teams.d.ts +20 -0
  15. package/dist/api/core/teams.js +19 -0
  16. package/dist/api/core/teams.js.map +1 -0
  17. package/dist/api/core/types.d.ts +58 -0
  18. package/dist/api/core/types.js +1 -0
  19. package/dist/api/core/types.js.map +1 -0
  20. package/dist/api/core/users.d.ts +43 -0
  21. package/dist/api/core/users.js +41 -0
  22. package/dist/api/core/users.js.map +1 -0
  23. package/dist/api/entities/factory.d.ts +43 -0
  24. package/dist/api/entities/factory.js +31 -0
  25. package/dist/api/entities/factory.js.map +1 -0
  26. package/dist/api/entities/index.d.ts +3 -0
  27. package/dist/api/entities/index.js +3 -0
  28. package/dist/api/entities/index.js.map +1 -0
  29. package/dist/api/entities/types.d.ts +32 -0
  30. package/dist/api/entities/types.js +1 -0
  31. package/dist/api/entities/types.js.map +1 -0
  32. package/dist/api/index.d.ts +7 -0
  33. package/dist/api/index.js +15 -0
  34. package/dist/api/index.js.map +1 -0
  35. package/dist/hooks/index.d.ts +4 -0
  36. package/dist/hooks/index.js +5 -0
  37. package/dist/hooks/index.js.map +1 -0
  38. package/dist/index.d.ts +14 -0
  39. package/dist/index.js +28 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/lib/alert.d.ts +34 -0
  42. package/dist/lib/alert.js +73 -0
  43. package/dist/lib/alert.js.map +1 -0
  44. package/dist/lib/index.d.ts +2 -0
  45. package/dist/lib/index.js +10 -0
  46. package/dist/lib/index.js.map +1 -0
  47. package/dist/lib/storage.d.ts +1 -0
  48. package/dist/lib/storage.js +29 -0
  49. package/dist/lib/storage.js.map +1 -0
  50. package/dist/providers/AuthProvider.d.ts +21 -0
  51. package/dist/providers/AuthProvider.js +113 -0
  52. package/dist/providers/AuthProvider.js.map +1 -0
  53. package/dist/providers/QueryProvider.d.ts +11 -0
  54. package/dist/providers/QueryProvider.js +23 -0
  55. package/dist/providers/QueryProvider.js.map +1 -0
  56. package/dist/providers/index.d.ts +6 -0
  57. package/dist/providers/index.js +9 -0
  58. package/dist/providers/index.js.map +1 -0
  59. package/dist/storage-BaRppHUz.d.ts +22 -0
  60. package/package.json +99 -0
  61. package/templates/app/(app)/_layout.tsx +216 -0
  62. package/templates/app/(app)/customer/[id].tsx +68 -0
  63. package/templates/app/(app)/customer/create.tsx +24 -0
  64. package/templates/app/(app)/customers.tsx +164 -0
  65. package/templates/app/(app)/index.tsx +310 -0
  66. package/templates/app/(app)/notifications.tsx +242 -0
  67. package/templates/app/(app)/profile.tsx +254 -0
  68. package/templates/app/(app)/settings.tsx +241 -0
  69. package/templates/app/(app)/task/[id].tsx +70 -0
  70. package/templates/app/(app)/task/create.tsx +24 -0
  71. package/templates/app/(app)/tasks.tsx +164 -0
  72. package/templates/app/_layout.tsx +54 -0
  73. package/templates/app/index.tsx +35 -0
  74. package/templates/app/login.tsx +179 -0
  75. package/templates/app.config.ts +39 -0
  76. package/templates/babel.config.js +9 -0
  77. package/templates/eas.json +18 -0
  78. package/templates/jest.config.js +12 -0
  79. package/templates/metro.config.js +23 -0
  80. package/templates/package.json.template +52 -0
  81. package/templates/src/components/entities/customers/CustomerCard.tsx +59 -0
  82. package/templates/src/components/entities/customers/CustomerForm.tsx +194 -0
  83. package/templates/src/components/entities/customers/index.ts +6 -0
  84. package/templates/src/components/entities/index.ts +9 -0
  85. package/templates/src/components/entities/tasks/TaskCard.tsx +89 -0
  86. package/templates/src/components/entities/tasks/TaskForm.tsx +231 -0
  87. package/templates/src/components/entities/tasks/index.ts +6 -0
  88. package/templates/src/components/features/index.ts +6 -0
  89. package/templates/src/components/navigation/BottomTabBar.tsx +80 -0
  90. package/templates/src/components/navigation/CreateSheet.tsx +108 -0
  91. package/templates/src/components/navigation/MoreSheet.tsx +403 -0
  92. package/templates/src/components/navigation/TopBar.tsx +74 -0
  93. package/templates/src/components/navigation/index.ts +8 -0
  94. package/templates/src/components/ui/index.ts +89 -0
  95. package/templates/src/components/ui/text.tsx +64 -0
  96. package/templates/src/config/api.config.ts +26 -0
  97. package/templates/src/config/app.config.ts +15 -0
  98. package/templates/src/config/hooks.ts +58 -0
  99. package/templates/src/config/permissions.config.ts +119 -0
  100. package/templates/src/constants/colors.ts +55 -0
  101. package/templates/src/data/notifications.mock.json +100 -0
  102. package/templates/src/entities/customers/api.ts +10 -0
  103. package/templates/src/entities/customers/constants.internal.ts +6 -0
  104. package/templates/src/entities/customers/constants.ts +14 -0
  105. package/templates/src/entities/customers/index.ts +9 -0
  106. package/templates/src/entities/customers/mutations.ts +58 -0
  107. package/templates/src/entities/customers/queries.ts +40 -0
  108. package/templates/src/entities/customers/types.ts +43 -0
  109. package/templates/src/entities/index.ts +8 -0
  110. package/templates/src/entities/tasks/api.ts +10 -0
  111. package/templates/src/entities/tasks/constants.internal.ts +6 -0
  112. package/templates/src/entities/tasks/constants.ts +39 -0
  113. package/templates/src/entities/tasks/index.ts +9 -0
  114. package/templates/src/entities/tasks/mutations.ts +108 -0
  115. package/templates/src/entities/tasks/queries.ts +42 -0
  116. package/templates/src/entities/tasks/types.ts +52 -0
  117. package/templates/src/hooks/useCustomers.ts +17 -0
  118. package/templates/src/hooks/useTasks.ts +18 -0
  119. package/templates/src/lib/utils.ts +10 -0
  120. package/templates/src/styles/globals.css +103 -0
  121. package/templates/src/types/index.ts +45 -0
  122. package/templates/tailwind.config.js +108 -0
  123. package/templates/tsconfig.json +15 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * TanStack Query mutations for Tasks
3
+ */
4
+
5
+ import { useMutation, useQueryClient } from '@tanstack/react-query'
6
+ import { tasksApi } from './api'
7
+ import { TASKS_QUERY_KEY } from './constants.internal'
8
+ import type { Task, CreateTaskInput, UpdateTaskInput } from './types'
9
+ import type { PaginatedResponse } from '@nextsparkjs/mobile'
10
+
11
+ /**
12
+ * Hook to create a new task
13
+ */
14
+ export function useCreateTask() {
15
+ const queryClient = useQueryClient()
16
+
17
+ return useMutation({
18
+ mutationFn: (data: CreateTaskInput) => tasksApi.create(data),
19
+ onSuccess: () => {
20
+ // Invalidate tasks list to refetch
21
+ queryClient.invalidateQueries({ queryKey: TASKS_QUERY_KEY })
22
+ },
23
+ })
24
+ }
25
+
26
+ /**
27
+ * Hook to update an existing task
28
+ */
29
+ export function useUpdateTask() {
30
+ const queryClient = useQueryClient()
31
+
32
+ return useMutation({
33
+ mutationFn: ({ id, data }: { id: string; data: UpdateTaskInput }) =>
34
+ tasksApi.update(id, data),
35
+ onSuccess: (response) => {
36
+ // Update the specific task in cache
37
+ queryClient.setQueryData([...TASKS_QUERY_KEY, response.data.id], response)
38
+ // Invalidate list to refetch
39
+ queryClient.invalidateQueries({ queryKey: TASKS_QUERY_KEY })
40
+ },
41
+ })
42
+ }
43
+
44
+ /**
45
+ * Hook to delete a task
46
+ */
47
+ export function useDeleteTask() {
48
+ const queryClient = useQueryClient()
49
+
50
+ return useMutation({
51
+ mutationFn: (id: string) => tasksApi.delete(id),
52
+ onSuccess: (_, deletedId) => {
53
+ // Remove from cache
54
+ queryClient.removeQueries({ queryKey: [...TASKS_QUERY_KEY, deletedId] })
55
+ // Invalidate list to refetch
56
+ queryClient.invalidateQueries({ queryKey: TASKS_QUERY_KEY })
57
+ },
58
+ })
59
+ }
60
+
61
+ /**
62
+ * Hook for optimistic task status update
63
+ */
64
+ export function useUpdateTaskStatus() {
65
+ const queryClient = useQueryClient()
66
+
67
+ return useMutation({
68
+ mutationFn: ({ id, status }: { id: string; status: Task['status'] }) =>
69
+ tasksApi.update(id, { status }),
70
+ onMutate: async ({ id, status }) => {
71
+ // Cancel any outgoing refetches
72
+ await queryClient.cancelQueries({ queryKey: TASKS_QUERY_KEY })
73
+
74
+ // Snapshot the previous value
75
+ const previousTasks = queryClient.getQueriesData<PaginatedResponse<Task>>({
76
+ queryKey: TASKS_QUERY_KEY,
77
+ })
78
+
79
+ // Optimistically update all task lists
80
+ queryClient.setQueriesData<PaginatedResponse<Task>>(
81
+ { queryKey: TASKS_QUERY_KEY },
82
+ (old) => {
83
+ if (!old) return old
84
+ return {
85
+ ...old,
86
+ data: old.data.map((task) =>
87
+ task.id === id ? { ...task, status } : task
88
+ ),
89
+ }
90
+ }
91
+ )
92
+
93
+ return { previousTasks }
94
+ },
95
+ onError: (_err, _variables, context) => {
96
+ // Rollback on error
97
+ if (context?.previousTasks) {
98
+ context.previousTasks.forEach(([queryKey, data]) => {
99
+ queryClient.setQueryData(queryKey, data)
100
+ })
101
+ }
102
+ },
103
+ onSettled: () => {
104
+ // Refetch after error or success
105
+ queryClient.invalidateQueries({ queryKey: TASKS_QUERY_KEY })
106
+ },
107
+ })
108
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * TanStack Query hooks for Tasks
3
+ */
4
+
5
+ import { useQuery } from '@tanstack/react-query'
6
+ import { tasksApi } from './api'
7
+ import { TASKS_QUERY_KEY } from './constants.internal'
8
+
9
+ export { TASKS_QUERY_KEY }
10
+
11
+ interface UseTasksOptions {
12
+ page?: number
13
+ limit?: number
14
+ status?: string
15
+ priority?: string
16
+ search?: string
17
+ enabled?: boolean
18
+ }
19
+
20
+ /**
21
+ * Hook to fetch paginated tasks list
22
+ */
23
+ export function useTasks(options: UseTasksOptions = {}) {
24
+ const { page = 1, limit = 20, status, priority, search, enabled = true } = options
25
+
26
+ return useQuery({
27
+ queryKey: [...TASKS_QUERY_KEY, { page, limit, status, priority, search }],
28
+ queryFn: () => tasksApi.list({ page, limit, status, priority, search }),
29
+ enabled,
30
+ })
31
+ }
32
+
33
+ /**
34
+ * Hook to fetch a single task by ID
35
+ */
36
+ export function useTask(id: string | undefined) {
37
+ return useQuery({
38
+ queryKey: [...TASKS_QUERY_KEY, id],
39
+ queryFn: () => tasksApi.get(id!),
40
+ enabled: !!id,
41
+ })
42
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Task Entity Types
3
+ */
4
+
5
+ // Task status options (matching backend - uses hyphens, not underscores)
6
+ export type TaskStatus = 'todo' | 'in-progress' | 'review' | 'done' | 'blocked'
7
+
8
+ // Task priority options
9
+ export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent'
10
+
11
+ // Task entity
12
+ export interface Task {
13
+ id: string
14
+ title: string
15
+ description?: string | null
16
+ projectId?: string | null
17
+ status: TaskStatus
18
+ priority: TaskPriority
19
+ dueDate?: string | null
20
+ assigneeId?: string | null
21
+ estimatedHours?: number | null
22
+ actualHours?: number | null
23
+ teamId: string
24
+ userId: string
25
+ createdAt: string
26
+ updatedAt: string
27
+ }
28
+
29
+ // Create task payload
30
+ export interface CreateTaskInput {
31
+ title: string
32
+ description?: string
33
+ projectId?: string
34
+ status?: TaskStatus
35
+ priority?: TaskPriority
36
+ dueDate?: string
37
+ assigneeId?: string
38
+ estimatedHours?: number
39
+ }
40
+
41
+ // Update task payload
42
+ export interface UpdateTaskInput {
43
+ title?: string
44
+ description?: string | null
45
+ projectId?: string | null
46
+ status?: TaskStatus
47
+ priority?: TaskPriority
48
+ dueDate?: string | null
49
+ assigneeId?: string | null
50
+ estimatedHours?: number | null
51
+ actualHours?: number | null
52
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * TanStack Query hooks for Customers CRUD
3
+ *
4
+ * @deprecated Import from '../entities/customers' instead
5
+ */
6
+
7
+ export {
8
+ useCustomers,
9
+ useCustomer,
10
+ CUSTOMERS_QUERY_KEY,
11
+ } from '../entities/customers/queries'
12
+
13
+ export {
14
+ useCreateCustomer,
15
+ useUpdateCustomer,
16
+ useDeleteCustomer,
17
+ } from '../entities/customers/mutations'
@@ -0,0 +1,18 @@
1
+ /**
2
+ * TanStack Query hooks for Tasks CRUD
3
+ *
4
+ * @deprecated Import from '../entities/tasks' instead
5
+ */
6
+
7
+ export {
8
+ useTasks,
9
+ useTask,
10
+ TASKS_QUERY_KEY,
11
+ } from '../entities/tasks/queries'
12
+
13
+ export {
14
+ useCreateTask,
15
+ useUpdateTask,
16
+ useDeleteTask,
17
+ useUpdateTaskStatus,
18
+ } from '../entities/tasks/mutations'
@@ -0,0 +1,10 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ /**
5
+ * Utility function to merge Tailwind CSS classes
6
+ * Same pattern as shadcn/ui on web
7
+ */
8
+ export function cn(...inputs: ClassValue[]) {
9
+ return twMerge(clsx(inputs));
10
+ }
@@ -0,0 +1,103 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /*
6
+ * NextSpark Mobile Theme
7
+ * Converted from OKLCH to hex (React Native doesn't support OKLCH)
8
+ * Matching the black/white NextSpark default theme
9
+ */
10
+
11
+ :root {
12
+ /* Base */
13
+ --background: #FFFFFF;
14
+ --foreground: #1a1a1a;
15
+
16
+ /* Card */
17
+ --card: #FFFFFF;
18
+ --card-foreground: #1a1a1a;
19
+
20
+ /* Primary (black) */
21
+ --primary: #171717;
22
+ --primary-foreground: #fafafa;
23
+
24
+ /* Secondary */
25
+ --secondary: #f5f5f5;
26
+ --secondary-foreground: #1a1a1a;
27
+
28
+ /* Muted */
29
+ --muted: #f5f5f5;
30
+ --muted-foreground: #737373;
31
+
32
+ /* Accent */
33
+ --accent: #f5f5f5;
34
+ --accent-foreground: #1a1a1a;
35
+
36
+ /* Destructive */
37
+ --destructive: #ef4444;
38
+ --destructive-foreground: #FFFFFF;
39
+
40
+ /* Success */
41
+ --success: #22c55e;
42
+ --success-foreground: #FFFFFF;
43
+
44
+ /* Border, Input, Ring */
45
+ --border: #e5e5e5;
46
+ --input: #e5e5e5;
47
+ --ring: #a3a3a3;
48
+
49
+ /* Border Radius */
50
+ --radius: 10px;
51
+
52
+ /* Status Colors */
53
+ --status-todo: #6B7280;
54
+ --status-in-progress: #3B82F6;
55
+ --status-review: #F59E0B;
56
+ --status-done: #22c55e;
57
+ --status-blocked: #ef4444;
58
+
59
+ /* Priority Colors */
60
+ --priority-low: #6B7280;
61
+ --priority-medium: #3B82F6;
62
+ --priority-high: #F59E0B;
63
+ --priority-urgent: #ef4444;
64
+ }
65
+
66
+ .dark {
67
+ /* Base */
68
+ --background: #0a0a0a;
69
+ --foreground: #fafafa;
70
+
71
+ /* Card */
72
+ --card: #171717;
73
+ --card-foreground: #fafafa;
74
+
75
+ /* Primary (white) */
76
+ --primary: #fafafa;
77
+ --primary-foreground: #171717;
78
+
79
+ /* Secondary */
80
+ --secondary: #262626;
81
+ --secondary-foreground: #fafafa;
82
+
83
+ /* Muted */
84
+ --muted: #262626;
85
+ --muted-foreground: #a3a3a3;
86
+
87
+ /* Accent */
88
+ --accent: #262626;
89
+ --accent-foreground: #fafafa;
90
+
91
+ /* Destructive */
92
+ --destructive: #dc2626;
93
+ --destructive-foreground: #FFFFFF;
94
+
95
+ /* Success */
96
+ --success: #16a34a;
97
+ --success-foreground: #FFFFFF;
98
+
99
+ /* Border, Input, Ring */
100
+ --border: #262626;
101
+ --input: #262626;
102
+ --ring: #d4d4d4;
103
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Type definitions for the NextSpark Mobile App
3
+ *
4
+ * Re-exports all types from their canonical locations for backward compatibility.
5
+ */
6
+
7
+ // Re-export API types from package
8
+ export type { PaginatedResponse, SingleResponse } from '@nextsparkjs/mobile'
9
+ export { ApiError } from '@nextsparkjs/mobile'
10
+
11
+ // Re-export core types (auth, user, team) from package
12
+ export type {
13
+ User,
14
+ Team,
15
+ AuthSession,
16
+ LoginResponse,
17
+ SessionResponse,
18
+ TeamsResponse,
19
+ } from '@nextsparkjs/mobile'
20
+
21
+ // Re-export entity types
22
+ export type {
23
+ Task,
24
+ TaskStatus,
25
+ TaskPriority,
26
+ CreateTaskInput,
27
+ UpdateTaskInput,
28
+ } from '../entities/tasks/types'
29
+
30
+ export type {
31
+ Customer,
32
+ DayOption,
33
+ CreateCustomerInput,
34
+ UpdateCustomerInput,
35
+ } from '../entities/customers/types'
36
+
37
+ // Re-export entity constants
38
+ export {
39
+ STATUS_LABELS,
40
+ PRIORITY_LABELS,
41
+ STATUS_COLORS,
42
+ PRIORITY_COLORS,
43
+ } from '../entities/tasks/constants'
44
+
45
+ export { DAY_LABELS } from '../entities/customers/constants'
@@ -0,0 +1,108 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: [
4
+ "./app/**/*.{js,jsx,ts,tsx}",
5
+ "./src/**/*.{js,jsx,ts,tsx}",
6
+ // Include shared UI package for NativeWind to scan
7
+ "./node_modules/@nextsparkjs/ui/dist/**/*.{js,jsx,ts,tsx}",
8
+ ],
9
+ presets: [require("nativewind/preset")],
10
+ theme: {
11
+ extend: {
12
+ colors: {
13
+ background: "var(--background)",
14
+ foreground: "var(--foreground)",
15
+ primary: {
16
+ DEFAULT: "var(--primary)",
17
+ foreground: "var(--primary-foreground)",
18
+ },
19
+ secondary: {
20
+ DEFAULT: "var(--secondary)",
21
+ foreground: "var(--secondary-foreground)",
22
+ },
23
+ muted: {
24
+ DEFAULT: "var(--muted)",
25
+ foreground: "var(--muted-foreground)",
26
+ },
27
+ accent: {
28
+ DEFAULT: "var(--accent)",
29
+ foreground: "var(--accent-foreground)",
30
+ },
31
+ destructive: {
32
+ DEFAULT: "var(--destructive)",
33
+ foreground: "var(--destructive-foreground)",
34
+ },
35
+ success: {
36
+ DEFAULT: "var(--success)",
37
+ foreground: "var(--success-foreground)",
38
+ },
39
+ border: "var(--border)",
40
+ input: "var(--input)",
41
+ ring: "var(--ring)",
42
+ card: {
43
+ DEFAULT: "var(--card)",
44
+ foreground: "var(--card-foreground)",
45
+ },
46
+ // Status colors
47
+ status: {
48
+ todo: "var(--status-todo)",
49
+ inProgress: "var(--status-in-progress)",
50
+ review: "var(--status-review)",
51
+ done: "var(--status-done)",
52
+ blocked: "var(--status-blocked)",
53
+ },
54
+ // Priority colors
55
+ priority: {
56
+ low: "var(--priority-low)",
57
+ medium: "var(--priority-medium)",
58
+ high: "var(--priority-high)",
59
+ urgent: "var(--priority-urgent)",
60
+ },
61
+ },
62
+ borderRadius: {
63
+ lg: "var(--radius)",
64
+ md: "calc(var(--radius) - 2px)",
65
+ sm: "calc(var(--radius) - 4px)",
66
+ },
67
+ },
68
+ },
69
+ plugins: [],
70
+ // Safelist common utility classes for dynamic usage
71
+ safelist: [
72
+ // Container variants
73
+ "flex-row",
74
+ "items-center",
75
+ "justify-center",
76
+ "rounded-lg",
77
+ "font-semibold",
78
+ "active:opacity-80",
79
+ // Primary
80
+ "bg-primary",
81
+ "text-primary-foreground",
82
+ // Secondary
83
+ "bg-secondary",
84
+ "text-secondary-foreground",
85
+ // Outline
86
+ "border",
87
+ "border-input",
88
+ "bg-background",
89
+ "text-foreground",
90
+ // Ghost (no bg)
91
+ // Destructive
92
+ "bg-destructive",
93
+ "text-destructive-foreground",
94
+ // Sizes
95
+ "h-9",
96
+ "h-11",
97
+ "h-12",
98
+ "px-3",
99
+ "px-4",
100
+ "px-6",
101
+ "text-sm",
102
+ "text-base",
103
+ "text-lg",
104
+ // Avatar colors
105
+ "bg-amber-500",
106
+ "text-amber-500",
107
+ ],
108
+ };
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "expo/tsconfig.base",
3
+ "compilerOptions": {
4
+ "strict": true,
5
+ "paths": {
6
+ "@/*": ["./*"]
7
+ }
8
+ },
9
+ "include": [
10
+ "**/*.ts",
11
+ "**/*.tsx",
12
+ ".expo/types/**/*.ts",
13
+ "expo-env.d.ts"
14
+ ]
15
+ }