@organcli/composed-cli 0.1.0 → 0.1.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.
@@ -928,6 +928,338 @@
928
928
  "controller": "FileDropzoneController"
929
929
  }
930
930
  ],
931
+ "featureRegistry": [
932
+ {
933
+ "name": "login",
934
+ "description": "Email/password login form, posting to the auth API and redirecting into the protected area on success.",
935
+ "frameworks": {
936
+ "tanstack": {
937
+ "routeScope": "public",
938
+ "files": [
939
+ "src/routes/(public-pages)/(auth)/route.tsx",
940
+ "src/routes/(public-pages)/(auth)/login.tsx",
941
+ "src/components/screens/login/index.ts",
942
+ "src/components/screens/login/login-screen.tsx",
943
+ "src/components/screens/login/login-form.tsx",
944
+ "src/components/screens/login/login-schema.ts",
945
+ "src/components/layouts/index.ts",
946
+ "src/components/layouts/auth-layout.tsx",
947
+ "src/apis/auth/client/login.ts",
948
+ "src/apis/auth/client/register.ts",
949
+ "src/apis/auth/client/index.ts",
950
+ "src/apis/auth/client/get-profile.ts",
951
+ "src/apis/auth/hooks/index.ts",
952
+ "src/apis/auth/hooks/mutations/index.ts",
953
+ "src/apis/auth/hooks/mutations/use-login.mutation.ts",
954
+ "src/apis/auth/hooks/mutations/use-register.mutation.ts",
955
+ "src/apis/query-keys/auth.ts",
956
+ "src/store/auth-state.ts",
957
+ "src/lib/auth/token-cookie.ts",
958
+ "src/@types/user.ts",
959
+ "src/components/common/controllers/input-field-controller.tsx",
960
+ "src/components/ui/composed/button/loading-button.tsx",
961
+ "src/hooks/use-response-error.ts"
962
+ ],
963
+ "i18nKeys": [
964
+ "login_page",
965
+ "login_title",
966
+ "login_intro",
967
+ "login_email_label",
968
+ "login_email_placeholder",
969
+ "login_password_label",
970
+ "login_password_placeholder",
971
+ "login_submitting",
972
+ "login_submit",
973
+ "login_success",
974
+ "validation_email_required",
975
+ "validation_email_invalid",
976
+ "validation_password_required"
977
+ ]
978
+ },
979
+ "nextjs": {
980
+ "routeScope": "public",
981
+ "sourcePrefix": "cli/templates/nextjs/",
982
+ "files": [
983
+ "cli/templates/nextjs/src/app/(public)/(auth)/layout.tsx",
984
+ "cli/templates/nextjs/src/app/(public)/(auth)/login/page.tsx",
985
+ "src/components/screens/login/index.ts",
986
+ "src/components/screens/login/login-screen.tsx",
987
+ "src/components/screens/login/login-form.tsx",
988
+ "src/components/screens/login/login-schema.ts",
989
+ "src/components/layouts/index.ts",
990
+ "src/components/layouts/auth-layout.tsx",
991
+ "cli/templates/nextjs/src/apis/auth/hooks/mutations/use-login.mutation.ts",
992
+ "cli/templates/nextjs/src/apis/auth/hooks/mutations/use-register.mutation.ts",
993
+ "src/apis/auth/client/register.ts",
994
+ "src/apis/auth/client/login.ts",
995
+ "src/apis/auth/client/index.ts",
996
+ "src/apis/auth/client/get-profile.ts",
997
+ "src/apis/auth/hooks/index.ts",
998
+ "src/apis/auth/hooks/mutations/index.ts",
999
+ "src/apis/query-keys/auth.ts",
1000
+ "src/store/auth-state.ts",
1001
+ "src/lib/auth/token-cookie.ts",
1002
+ "src/lib/query-client.ts",
1003
+ "src/@types/user.ts",
1004
+ "src/components/common/controllers/input-field-controller.tsx",
1005
+ "src/components/ui/composed/button/loading-button.tsx",
1006
+ "src/hooks/use-response-error.ts"
1007
+ ],
1008
+ "i18nKeys": [
1009
+ "login_page",
1010
+ "login_title",
1011
+ "login_intro",
1012
+ "login_email_label",
1013
+ "login_email_placeholder",
1014
+ "login_password_label",
1015
+ "login_password_placeholder",
1016
+ "login_submitting",
1017
+ "login_submit",
1018
+ "login_success",
1019
+ "validation_email_required",
1020
+ "validation_email_invalid",
1021
+ "validation_password_required"
1022
+ ]
1023
+ }
1024
+ }
1025
+ },
1026
+ {
1027
+ "name": "signup",
1028
+ "description": "Name/email/password registration form, posting to the auth API and auto-logging in on success.",
1029
+ "frameworks": {
1030
+ "tanstack": {
1031
+ "routeScope": "public",
1032
+ "files": [
1033
+ "src/routes/(public-pages)/(auth)/route.tsx",
1034
+ "src/routes/(public-pages)/(auth)/signup.tsx",
1035
+ "src/components/screens/signup/index.ts",
1036
+ "src/components/screens/signup/signup-screen.tsx",
1037
+ "src/components/screens/signup/signup-form.tsx",
1038
+ "src/components/screens/signup/signup-schema.ts",
1039
+ "src/components/layouts/index.ts",
1040
+ "src/components/layouts/auth-layout.tsx",
1041
+ "src/apis/auth/client/register.ts",
1042
+ "src/apis/auth/client/login.ts",
1043
+ "src/apis/auth/client/index.ts",
1044
+ "src/apis/auth/client/get-profile.ts",
1045
+ "src/apis/auth/hooks/index.ts",
1046
+ "src/apis/auth/hooks/mutations/index.ts",
1047
+ "src/apis/auth/hooks/mutations/use-register.mutation.ts",
1048
+ "src/apis/auth/hooks/mutations/use-login.mutation.ts",
1049
+ "src/apis/query-keys/auth.ts",
1050
+ "src/store/auth-state.ts",
1051
+ "src/lib/auth/token-cookie.ts",
1052
+ "src/@types/user.ts",
1053
+ "src/components/common/controllers/input-field-controller.tsx",
1054
+ "src/components/ui/composed/button/loading-button.tsx",
1055
+ "src/hooks/use-response-error.ts"
1056
+ ],
1057
+ "i18nKeys": [
1058
+ "signup_page",
1059
+ "signup_title",
1060
+ "signup_intro",
1061
+ "signup_name_label",
1062
+ "signup_name_placeholder",
1063
+ "signup_email_label",
1064
+ "signup_email_placeholder",
1065
+ "signup_password_label",
1066
+ "signup_password_placeholder",
1067
+ "signup_confirm_password_label",
1068
+ "signup_confirm_password_placeholder",
1069
+ "signup_submitting",
1070
+ "signup_submit",
1071
+ "signup_success",
1072
+ "validation_name_required",
1073
+ "validation_email_required",
1074
+ "validation_email_invalid",
1075
+ "validation_password_required",
1076
+ "validation_confirm_password_required",
1077
+ "validation_password_mismatch"
1078
+ ]
1079
+ },
1080
+ "nextjs": {
1081
+ "routeScope": "public",
1082
+ "sourcePrefix": "cli/templates/nextjs/",
1083
+ "files": [
1084
+ "cli/templates/nextjs/src/app/(public)/(auth)/layout.tsx",
1085
+ "cli/templates/nextjs/src/app/(public)/(auth)/signup/page.tsx",
1086
+ "src/components/screens/signup/index.ts",
1087
+ "src/components/screens/signup/signup-screen.tsx",
1088
+ "src/components/screens/signup/signup-form.tsx",
1089
+ "src/components/screens/signup/signup-schema.ts",
1090
+ "src/components/layouts/index.ts",
1091
+ "src/components/layouts/auth-layout.tsx",
1092
+ "cli/templates/nextjs/src/apis/auth/hooks/mutations/use-login.mutation.ts",
1093
+ "cli/templates/nextjs/src/apis/auth/hooks/mutations/use-register.mutation.ts",
1094
+ "src/apis/auth/client/register.ts",
1095
+ "src/apis/auth/client/login.ts",
1096
+ "src/apis/auth/client/index.ts",
1097
+ "src/apis/auth/client/get-profile.ts",
1098
+ "src/apis/auth/hooks/index.ts",
1099
+ "src/apis/auth/hooks/mutations/index.ts",
1100
+ "src/apis/query-keys/auth.ts",
1101
+ "src/store/auth-state.ts",
1102
+ "src/lib/auth/token-cookie.ts",
1103
+ "src/lib/query-client.ts",
1104
+ "src/@types/user.ts",
1105
+ "src/components/common/controllers/input-field-controller.tsx",
1106
+ "src/components/ui/composed/button/loading-button.tsx",
1107
+ "src/hooks/use-response-error.ts"
1108
+ ],
1109
+ "i18nKeys": [
1110
+ "signup_page",
1111
+ "signup_title",
1112
+ "signup_intro",
1113
+ "signup_name_label",
1114
+ "signup_name_placeholder",
1115
+ "signup_email_label",
1116
+ "signup_email_placeholder",
1117
+ "signup_password_label",
1118
+ "signup_password_placeholder",
1119
+ "signup_confirm_password_label",
1120
+ "signup_confirm_password_placeholder",
1121
+ "signup_submitting",
1122
+ "signup_submit",
1123
+ "signup_success",
1124
+ "validation_name_required",
1125
+ "validation_email_required",
1126
+ "validation_email_invalid",
1127
+ "validation_password_required",
1128
+ "validation_confirm_password_required",
1129
+ "validation_password_mismatch"
1130
+ ]
1131
+ }
1132
+ }
1133
+ },
1134
+ {
1135
+ "name": "dashboard",
1136
+ "description": "Protected landing screen behind the auth guard, with the current profile fetched via React Query into the auth store.",
1137
+ "frameworks": {
1138
+ "tanstack": {
1139
+ "routeScope": "protected",
1140
+ "files": [
1141
+ "src/routes/(protected-pages)/route.tsx",
1142
+ "src/routes/(protected-pages)/dashboard.tsx",
1143
+ "src/components/screens/dashboard/index.ts",
1144
+ "src/components/screens/dashboard/dashboard-screen.tsx",
1145
+ "src/components/layouts/index.ts",
1146
+ "src/components/layouts/dashboard-layout.tsx",
1147
+ "src/components/layouts/dashboard-sidebar.tsx",
1148
+ "src/apis/auth/client/get-profile.ts",
1149
+ "src/apis/auth/client/login.ts",
1150
+ "src/apis/auth/client/register.ts",
1151
+ "src/apis/auth/client/index.ts",
1152
+ "src/apis/query-keys/auth.ts",
1153
+ "src/store/auth-state.ts",
1154
+ "src/lib/auth/token-cookie.ts",
1155
+ "src/@types/user.ts"
1156
+ ],
1157
+ "i18nKeys": [
1158
+ "dashboard_page",
1159
+ "dashboard_title",
1160
+ "dashboard_intro",
1161
+ "dashboard_signed_in_as",
1162
+ "dashboard_logout"
1163
+ ]
1164
+ },
1165
+ "nextjs": {
1166
+ "routeScope": "protected",
1167
+ "sourcePrefix": "cli/templates/nextjs/",
1168
+ "files": [
1169
+ "cli/templates/nextjs/src/app/(protected)/layout.tsx",
1170
+ "cli/templates/nextjs/src/app/(protected)/dashboard/page.tsx",
1171
+ "cli/templates/nextjs/src/components/layouts/dashboard-sidebar.tsx",
1172
+ "src/components/screens/dashboard/index.ts",
1173
+ "src/components/screens/dashboard/dashboard-screen.tsx",
1174
+ "src/components/layouts/index.ts",
1175
+ "src/components/layouts/dashboard-layout.tsx",
1176
+ "src/apis/auth/client/get-profile.ts",
1177
+ "src/apis/auth/client/login.ts",
1178
+ "src/apis/auth/client/register.ts",
1179
+ "src/apis/auth/client/index.ts",
1180
+ "src/apis/query-keys/auth.ts",
1181
+ "src/store/auth-state.ts",
1182
+ "src/lib/auth/token-cookie.ts",
1183
+ "src/lib/query-client.ts",
1184
+ "src/@types/user.ts"
1185
+ ],
1186
+ "i18nKeys": [
1187
+ "dashboard_page",
1188
+ "dashboard_title",
1189
+ "dashboard_intro",
1190
+ "dashboard_signed_in_as",
1191
+ "dashboard_logout"
1192
+ ]
1193
+ }
1194
+ }
1195
+ },
1196
+ {
1197
+ "name": "about",
1198
+ "description": "Static public \"About\" page.",
1199
+ "frameworks": {
1200
+ "tanstack": {
1201
+ "routeScope": "public",
1202
+ "files": [
1203
+ "src/routes/(public-pages)/about.tsx",
1204
+ "src/components/screens/about/index.ts",
1205
+ "src/components/screens/about/about-screen.tsx"
1206
+ ],
1207
+ "i18nKeys": [
1208
+ "about_page",
1209
+ "about_title",
1210
+ "about_intro"
1211
+ ]
1212
+ },
1213
+ "nextjs": {
1214
+ "routeScope": "public",
1215
+ "sourcePrefix": "cli/templates/nextjs/",
1216
+ "files": [
1217
+ "cli/templates/nextjs/src/app/(public)/about/page.tsx",
1218
+ "src/components/screens/about/index.ts",
1219
+ "src/components/screens/about/about-screen.tsx"
1220
+ ],
1221
+ "i18nKeys": [
1222
+ "about_page",
1223
+ "about_title",
1224
+ "about_intro"
1225
+ ]
1226
+ }
1227
+ }
1228
+ },
1229
+ {
1230
+ "name": "contact",
1231
+ "description": "Static public \"Contact\" page.",
1232
+ "frameworks": {
1233
+ "tanstack": {
1234
+ "routeScope": "public",
1235
+ "files": [
1236
+ "src/routes/(public-pages)/contact.tsx",
1237
+ "src/components/screens/contact/index.ts",
1238
+ "src/components/screens/contact/contact-screen.tsx"
1239
+ ],
1240
+ "i18nKeys": [
1241
+ "contact_page",
1242
+ "contact_title",
1243
+ "contact_intro"
1244
+ ]
1245
+ },
1246
+ "nextjs": {
1247
+ "routeScope": "public",
1248
+ "sourcePrefix": "cli/templates/nextjs/",
1249
+ "files": [
1250
+ "cli/templates/nextjs/src/app/(public)/contact/page.tsx",
1251
+ "src/components/screens/contact/index.ts",
1252
+ "src/components/screens/contact/contact-screen.tsx"
1253
+ ],
1254
+ "i18nKeys": [
1255
+ "contact_page",
1256
+ "contact_title",
1257
+ "contact_intro"
1258
+ ]
1259
+ }
1260
+ }
1261
+ }
1262
+ ],
931
1263
  "files": {
932
1264
  "src/components/ui/composed/accordion/faq-accordion.tsx": "import * as React from 'react';\r\nimport {\r\n Accordion,\r\n AccordionContent,\r\n AccordionItem,\r\n AccordionTrigger,\r\n} from '@/components/ui/accordion';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype FaqItem = {\r\n id?: string;\r\n question: string;\r\n answer: React.ReactNode;\r\n};\r\n\r\ntype FaqAccordionProps = {\r\n items: FaqItem[];\r\n defaultValue?: string;\r\n className?: string;\r\n};\r\n\r\nexport function FaqAccordion({\r\n items,\r\n defaultValue,\r\n className,\r\n}: FaqAccordionProps) {\r\n return (\r\n <Accordion\r\n type='single'\r\n collapsible\r\n defaultValue={\r\n defaultValue ??\r\n (items.length > 0 ? (items[0].id ?? 'faq-0') : undefined)\r\n }\r\n className={cn('w-full', className)}\r\n >\r\n {items.map((item, index) => {\r\n const value = item.id ?? `faq-${index}`;\r\n return (\r\n <AccordionItem key={value} value={value}>\r\n <AccordionTrigger>{item.question}</AccordionTrigger>\r\n <AccordionContent>{item.answer}</AccordionContent>\r\n </AccordionItem>\r\n );\r\n })}\r\n </Accordion>\r\n );\r\n}\r\n",
933
1265
  "src/components/ui/composed/alert/error-alert.tsx": "import type { ReactNode } from 'react';\r\nimport { CircleAlertIcon } from 'lucide-react';\r\n\r\nimport {\r\n Alert,\r\n AlertAction,\r\n AlertDescription,\r\n AlertTitle,\r\n} from '@/components/ui/alert';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype ErrorAlertProps = {\r\n title: string;\r\n description?: string;\r\n action?: ReactNode;\r\n className?: string;\r\n};\r\n\r\nexport function ErrorAlert({\r\n title,\r\n description,\r\n action,\r\n className,\r\n}: ErrorAlertProps) {\r\n return (\r\n <Alert variant='destructive' className={cn('w-full', className)}>\r\n <CircleAlertIcon />\r\n <AlertTitle>{title}</AlertTitle>\r\n {description ? <AlertDescription>{description}</AlertDescription> : null}\r\n {action ? <AlertAction>{action}</AlertAction> : null}\r\n </Alert>\r\n );\r\n}\r\n",
@@ -961,6 +1293,67 @@
961
1293
  "src/components/ui/composed/textarea/labeled-textarea.tsx": "import * as React from 'react';\r\nimport { Textarea } from '@/components/ui/textarea';\r\nimport { Label } from '@/components/ui/label';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype LabeledTextareaProps = React.ComponentProps<typeof Textarea> & {\r\n label?: string;\r\n description?: string;\r\n error?: string;\r\n required?: boolean;\r\n maxLength?: number;\r\n showCharCount?: boolean;\r\n};\r\n\r\nexport function LabeledTextarea({\r\n label,\r\n description,\r\n error,\r\n required,\r\n maxLength,\r\n showCharCount = false,\r\n value,\r\n id: externalId,\r\n className,\r\n ...props\r\n}: LabeledTextareaProps) {\r\n const generatedId = React.useId();\r\n const id = externalId ?? generatedId;\r\n const descId = description ? `${id}-desc` : undefined;\r\n const errId = error ? `${id}-err` : undefined;\r\n const charCount = typeof value === 'string' ? value.length : 0;\r\n\r\n return (\r\n <div className={cn('flex flex-col gap-1.5', className)}>\r\n <div className='flex items-baseline justify-between gap-2'>\r\n {label && (\r\n <Label htmlFor={id}>\r\n {label}\r\n {required && <span className='text-destructive ms-0.5'>*</span>}\r\n </Label>\r\n )}\r\n {showCharCount && maxLength && (\r\n <span className='text-muted-foreground text-xs tabular-nums'>\r\n {charCount}/{maxLength}\r\n </span>\r\n )}\r\n </div>\r\n {description && (\r\n <p id={descId} className='text-muted-foreground text-sm'>\r\n {description}\r\n </p>\r\n )}\r\n <Textarea\r\n id={id}\r\n value={value}\r\n maxLength={maxLength}\r\n aria-describedby={\r\n [descId, errId].filter(Boolean).join(' ') || undefined\r\n }\r\n aria-invalid={!!error}\r\n {...props}\r\n />\r\n {error && (\r\n <p id={errId} role='alert' className='text-destructive text-sm'>\r\n {error}\r\n </p>\r\n )}\r\n </div>\r\n );\r\n}\r\n",
962
1294
  "src/components/ui/composed/tooltip/icon-tooltip.tsx": "import * as React from 'react';\r\nimport {\r\n Tooltip,\r\n TooltipContent,\r\n TooltipTrigger,\r\n} from '@/components/ui/tooltip';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype IconTooltipProps = {\r\n content: React.ReactNode;\r\n children: React.ReactNode;\r\n side?: 'top' | 'right' | 'bottom' | 'left';\r\n align?: 'start' | 'center' | 'end';\r\n className?: string;\r\n};\r\n\r\nexport function IconTooltip({\r\n content,\r\n children,\r\n side = 'top',\r\n align = 'center',\r\n className,\r\n}: IconTooltipProps) {\r\n return (\r\n <Tooltip>\r\n <TooltipTrigger asChild>{children}</TooltipTrigger>\r\n <TooltipContent side={side} align={align} className={className}>\r\n {content}\r\n </TooltipContent>\r\n </Tooltip>\r\n );\r\n}\r\n",
963
1295
  "src/components/ui/composed/upload/file-dropzone.tsx": "import * as React from 'react';\r\nimport { UploadIcon, XIcon } from 'lucide-react';\r\n\r\nimport { Button } from '@/components/ui/button';\r\nimport { Input } from '@/components/ui/input';\r\nimport { Label } from '@/components/ui/label';\r\nimport { cn } from '@/lib/utils';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\ntype FileDropzoneProps = {\r\n value?: File[];\r\n onValueChange?: (files: File[]) => void;\r\n label?: string;\r\n description?: string;\r\n error?: string;\r\n required?: boolean;\r\n disabled?: boolean;\r\n multiple?: boolean;\r\n accept?: string;\r\n maxSize?: number;\r\n id?: string;\r\n className?: string;\r\n};\r\n\r\nconst formatBytes = (bytes: number) => {\r\n if (bytes < 1024) return `${bytes} B`;\r\n if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;\r\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\r\n};\r\n\r\nconst fileKey = (file: File) =>\r\n `${file.name}-${file.size}-${file.lastModified}`;\r\n\r\nexport function FileDropzone({\r\n value = [],\r\n onValueChange,\r\n label,\r\n description,\r\n error,\r\n required,\r\n disabled,\r\n multiple = false,\r\n accept,\r\n maxSize,\r\n id: externalId,\r\n className,\r\n}: FileDropzoneProps) {\r\n const generatedId = React.useId();\r\n const id = externalId ?? generatedId;\r\n const descId = description ? `${id}-desc` : undefined;\r\n const errId = error ? `${id}-err` : undefined;\r\n const [isDragging, setIsDragging] = React.useState(false);\r\n\r\n const addFiles = (incoming: File[]) => {\r\n const allowed = maxSize\r\n ? incoming.filter(file => file.size <= maxSize)\r\n : incoming;\r\n if (allowed.length === 0) return;\r\n\r\n if (!multiple) {\r\n onValueChange?.(allowed.slice(0, 1));\r\n return;\r\n }\r\n\r\n const existing = new Set(value.map(fileKey));\r\n onValueChange?.([\r\n ...value,\r\n ...allowed.filter(file => !existing.has(fileKey(file))),\r\n ]);\r\n };\r\n\r\n return (\r\n <div className={cn('flex flex-col gap-1.5', className)}>\r\n {label ? (\r\n <Label htmlFor={id}>\r\n {label}\r\n {required ? <span className='text-destructive ms-0.5'>*</span> : null}\r\n </Label>\r\n ) : null}\r\n {description ? (\r\n <p id={descId} className='text-muted-foreground text-sm'>\r\n {description}\r\n </p>\r\n ) : null}\r\n <label\r\n htmlFor={id}\r\n onDragOver={event => {\r\n event.preventDefault();\r\n if (!disabled) setIsDragging(true);\r\n }}\r\n onDragLeave={() => setIsDragging(false)}\r\n onDrop={event => {\r\n event.preventDefault();\r\n setIsDragging(false);\r\n if (disabled) return;\r\n addFiles(Array.from(event.dataTransfer.files));\r\n }}\r\n className={cn(\r\n 'border-input bg-background flex min-h-32 cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border border-dashed px-4 py-6 text-center transition-colors',\r\n isDragging && 'border-ring bg-muted/50',\r\n error && 'border-destructive',\r\n disabled && 'pointer-events-none opacity-50'\r\n )}\r\n >\r\n <UploadIcon className='text-muted-foreground size-6' />\r\n <span className='text-sm'>{m.ui_dropzone_prompt()}</span>\r\n <span className='text-primary text-sm font-medium'>\r\n {m.ui_browse_files()}\r\n </span>\r\n {maxSize ? (\r\n <span className='text-muted-foreground text-xs'>\r\n {m.ui_max_size({ size: formatBytes(maxSize) })}\r\n </span>\r\n ) : null}\r\n <Input\r\n id={id}\r\n type='file'\r\n accept={accept}\r\n multiple={multiple}\r\n disabled={disabled}\r\n className='sr-only'\r\n aria-describedby={\r\n [descId, errId].filter(Boolean).join(' ') || undefined\r\n }\r\n aria-invalid={!!error}\r\n onChange={event => {\r\n addFiles(Array.from(event.target.files ?? []));\r\n event.target.value = '';\r\n }}\r\n />\r\n </label>\r\n {value.length > 0 ? (\r\n <ul className='flex flex-col gap-1'>\r\n {value.map(file => (\r\n <li\r\n key={fileKey(file)}\r\n className='bg-muted flex items-center gap-2 rounded-md px-2 py-1.5 text-sm'\r\n >\r\n <span className='min-w-0 flex-1 truncate text-start'>\r\n {file.name}\r\n </span>\r\n <span className='text-muted-foreground shrink-0 text-xs'>\r\n {formatBytes(file.size)}\r\n </span>\r\n <Button\r\n type='button'\r\n variant='ghost'\r\n size='icon-xs'\r\n disabled={disabled}\r\n aria-label={m.ui_remove_file()}\r\n onClick={() =>\r\n onValueChange?.(\r\n value.filter(item => fileKey(item) !== fileKey(file))\r\n )\r\n }\r\n >\r\n <XIcon />\r\n </Button>\r\n </li>\r\n ))}\r\n </ul>\r\n ) : null}\r\n {error ? (\r\n <p id={errId} role='alert' className='text-destructive text-sm'>\r\n {error}\r\n </p>\r\n ) : null}\r\n </div>\r\n );\r\n}\r\n",
1296
+ "src/routes/(public-pages)/(auth)/route.tsx": "import { Outlet, createFileRoute, redirect } from '@tanstack/react-router';\r\nimport { AuthLayout } from '@/components/layouts';\r\nimport { useAuthStore } from '@/store/auth-state';\r\n\r\n/**\r\n * Client-side guard for the auth pages under `(public-pages)/(auth)`\r\n * (login, etc.). It keeps already-authenticated users out of these pages.\r\n *\r\n * This app runs as a client-side SPA (see `src/start.ts`'s `defaultSsr:\r\n * false`), so `beforeLoad` only ever runs in the browser — reading\r\n * `isAuthenticated` straight from the Zustand store (hydrated synchronously\r\n * from the `token` cookie) is sufficient; no server round trip is needed.\r\n */\r\nexport const Route = createFileRoute('/(public-pages)/(auth)')({\r\n beforeLoad: () => {\r\n if (useAuthStore.getState().isAuthenticated) {\r\n throw redirect({ to: '/dashboard' });\r\n }\r\n },\r\n\r\n component: PublicLayout,\r\n});\r\n\r\nfunction PublicLayout() {\r\n return (\r\n <AuthLayout>\r\n <Outlet />\r\n </AuthLayout>\r\n );\r\n}\r\n",
1297
+ "src/routes/(public-pages)/(auth)/login.tsx": "import { createFileRoute } from '@tanstack/react-router';\r\nimport { LoginScreen } from '@/components/screens/login';\r\nimport { m } from '#/paraglide/messages';\r\n\r\nexport const Route = createFileRoute('/(public-pages)/(auth)/login')({\r\n head: () => ({ meta: [{ title: m.login_page() }] }),\r\n component: LoginScreen,\r\n});\r\n",
1298
+ "src/components/screens/login/index.ts": "export { LoginScreen } from './login-screen';\r\n",
1299
+ "src/components/screens/login/login-screen.tsx": "import { m } from '#/paraglide/messages';\r\n\r\nimport { LoginForm } from './login-form';\r\n\r\nexport function LoginScreen() {\r\n return (\r\n <main className='w-full max-w-md'>\r\n <div className='rounded-xl border border-border bg-card p-8 shadow-sm'>\r\n <div className='mb-8 text-center'>\r\n <h1 className='text-2xl font-bold tracking-tight text-foreground'>\r\n {m.login_title()}\r\n </h1>\r\n <p className='text-muted-foreground mt-2 text-sm'>\r\n {m.login_intro()}\r\n </p>\r\n </div>\r\n\r\n <LoginForm />\r\n </div>\r\n </main>\r\n );\r\n}\r\n",
1300
+ "src/components/screens/login/login-form.tsx": "import { zodResolver } from '@hookform/resolvers/zod';\r\nimport { useForm } from 'react-hook-form';\r\n\r\nimport { useLoginMutation } from '@/apis/auth/hooks';\r\nimport { InputFieldController } from '@/components/common/controllers';\r\nimport { LoadingButton } from '@/components/ui/composed/button/loading-button';\r\nimport { setResponseFormErrors } from '@/hooks/use-response-error';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\nimport { loginSchema } from './login-schema';\r\n\r\nimport type { TLoginFormValues } from './login-schema';\r\n\r\nexport function LoginForm() {\r\n const { mutate, isPending, isSuccess } = useLoginMutation();\r\n\r\n const form = useForm<TLoginFormValues>({\r\n resolver: zodResolver(loginSchema()),\r\n defaultValues: { email: '', password: '' },\r\n });\r\n\r\n // Keep the button busy through the post-success redirect.\r\n const isLoading = isPending || isSuccess;\r\n\r\n const onSubmit = (values: TLoginFormValues) =>\r\n mutate(values, {\r\n onError: error => setResponseFormErrors(form.setError, error),\r\n });\r\n\r\n return (\r\n <form\r\n noValidate\r\n autoComplete='off'\r\n onSubmit={form.handleSubmit(onSubmit)}\r\n className='flex flex-col gap-6'\r\n >\r\n <InputFieldController\r\n control={form.control}\r\n name='email'\r\n type='email'\r\n inputMode='email'\r\n autoComplete='email'\r\n label={m.login_email_label()}\r\n placeholder={m.login_email_placeholder()}\r\n />\r\n\r\n <InputFieldController\r\n control={form.control}\r\n name='password'\r\n type='password'\r\n autoComplete='current-password'\r\n label={m.login_password_label()}\r\n placeholder={m.login_password_placeholder()}\r\n />\r\n\r\n <LoadingButton\r\n type='submit'\r\n className='w-full'\r\n loading={isLoading}\r\n loadingText={m.login_submitting()}\r\n >\r\n {m.login_submit()}\r\n </LoadingButton>\r\n </form>\r\n );\r\n}\r\n",
1301
+ "src/components/screens/login/login-schema.ts": "import { z } from 'zod';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\n/**\r\n * Built lazily as a function so the Paraglide message getters resolve against\r\n * the active locale at validation time (ported from flowi-admin-frontend).\r\n */\r\nexport const loginSchema = () =>\r\n z.object({\r\n email: z\r\n .string()\r\n .min(1, { message: m.validation_email_required() })\r\n .email({ message: m.validation_email_invalid() }),\r\n password: z.string().min(1, { message: m.validation_password_required() }),\r\n });\r\n\r\nexport type TLoginFormValues = z.infer<ReturnType<typeof loginSchema>>;\r\n",
1302
+ "src/components/layouts/index.ts": "export { AuthLayout } from './auth-layout';\r\nexport { DashboardLayout } from './dashboard-layout';\r\n",
1303
+ "src/components/layouts/auth-layout.tsx": "import type { ReactNode } from 'react';\r\n\r\nimport { ThemeToggle } from '@/components/ui/composed/button/theme-toggle';\r\nimport { LocaleSwitcher } from '@/components/ui/composed/dropdown-menu/locale-switcher';\r\n\r\ntype AuthLayoutProps = {\r\n children: ReactNode;\r\n};\r\n\r\nexport function AuthLayout({ children }: AuthLayoutProps) {\r\n return (\r\n <div className='bg-background relative flex min-h-svh flex-col items-center justify-center px-6 py-12'>\r\n <div className='absolute top-4 inset-e-4 flex items-center gap-2'>\r\n <LocaleSwitcher />\r\n <ThemeToggle />\r\n </div>\r\n {children}\r\n </div>\r\n );\r\n}\r\n",
1304
+ "src/apis/auth/client/login.ts": "import { clientFetch } from '@/lib/fetch/client';\r\n\r\nexport type TLoginPayload = {\r\n email: string;\r\n password: string;\r\n};\r\n\r\nexport type TLoginResponse = {\r\n data: {\r\n token: string;\r\n token_type?: string;\r\n expires_in?: number;\r\n };\r\n};\r\n\r\n/**\r\n * Authenticate with email/password. On success the backend returns a bearer\r\n * token. The token is wired into the session by the `useLoginMutation` hook\r\n * (cookie + Zustand store), not here.\r\n *\r\n * NOTE: `auth/login` is the assumed endpoint for this boilerplate — adjust to\r\n * the real login route if it differs.\r\n */\r\nexport const login = async (payload: TLoginPayload) => {\r\n const response = await clientFetch.post('auth/login', { json: payload });\r\n return response.json<TLoginResponse>();\r\n};\r\n",
1305
+ "src/apis/auth/client/register.ts": "import { clientFetch } from '@/lib/fetch/client';\r\n\r\nexport type TRegisterPayload = {\r\n name: string;\r\n email: string;\r\n password: string;\r\n};\r\n\r\nexport type TRegisterResponse = {\r\n data: {\r\n token: string;\r\n token_type?: string;\r\n expires_in?: number;\r\n };\r\n};\r\n\r\n/**\r\n * Register a new account with name/email/password. On success the backend\r\n * returns a bearer token, same shape as login. The token is wired into the\r\n * session by the `useRegisterMutation` hook (cookie + Zustand store), not\r\n * here.\r\n *\r\n * NOTE: `auth/register` is the assumed endpoint for this boilerplate — adjust\r\n * to the real registration route if it differs.\r\n */\r\nexport const register = async (payload: TRegisterPayload) => {\r\n const response = await clientFetch.post('auth/register', { json: payload });\r\n return response.json<TRegisterResponse>();\r\n};\r\n",
1306
+ "src/apis/auth/client/index.ts": "export { getProfile } from './get-profile';\r\nexport { login } from './login';\r\nexport type { TLoginPayload, TLoginResponse } from './login';\r\nexport { register } from './register';\r\nexport type { TRegisterPayload, TRegisterResponse } from './register';\r\n",
1307
+ "src/apis/auth/client/get-profile.ts": "import { clientFetch } from '@/lib/fetch/client';\r\n\r\nimport type { TUserProfileResponse } from '@/@types/user';\r\n\r\n/**\r\n * Fetch the current user's profile from the browser. A 401 here means the\r\n * token is invalid/expired; `clientFetch` handles the redirect to `/login`.\r\n *\r\n * The current-user route on the flowi backend is `auth` (GET).\r\n */\r\nexport const getProfile = async () => {\r\n const response = await clientFetch.get('auth');\r\n return response.json<TUserProfileResponse>();\r\n};\r\n",
1308
+ "src/apis/auth/hooks/index.ts": "export * from './mutations';\r\n",
1309
+ "src/apis/auth/hooks/mutations/index.ts": "export { useLoginMutation } from './use-login.mutation';\r\nexport { useRegisterMutation } from './use-register.mutation';\r\n",
1310
+ "src/apis/auth/hooks/mutations/use-login.mutation.ts": "import { useMutation } from '@tanstack/react-query';\r\nimport { useNavigate } from '@tanstack/react-router';\r\nimport { toast } from 'sonner';\r\n\r\nimport { getProfile, login } from '@/apis/auth/client';\r\nimport { useAuthStore } from '@/store/auth-state';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\nimport type { TLoginPayload, TLoginResponse } from '@/apis/auth/client';\r\n\r\n/**\r\n * Drives the full login flow (ported from flowi-admin-frontend):\r\n * 1. POST credentials, receive a bearer token.\r\n * 2. Fetch the current user's profile.\r\n * 3. Hydrate the Zustand auth store, which persists the token to the\r\n * shared auth cookie so `clientFetch` attaches the `Authorization`\r\n * header on subsequent requests.\r\n * 4. Redirect into the protected area.\r\n *\r\n * Errors are surfaced as toasts by the `clientFetch` response interceptor, so\r\n * no `onError` handler is needed here.\r\n */\r\nexport const useLoginMutation = () => {\r\n const navigate = useNavigate();\r\n\r\n return useMutation<TLoginResponse, Error, TLoginPayload>({\r\n mutationKey: ['LOGIN'],\r\n mutationFn: payload => login(payload),\r\n onSuccess: async ({ data }) => {\r\n useAuthStore.getState().setToken(data.token);\r\n\r\n const profile = await getProfile();\r\n useAuthStore.getState().storeLogin({ user: profile, token: data.token });\r\n\r\n toast.success(m.login_success());\r\n navigate({ to: '/dashboard' });\r\n },\r\n });\r\n};\r\n",
1311
+ "src/apis/auth/hooks/mutations/use-register.mutation.ts": "import { useMutation } from '@tanstack/react-query';\r\nimport { useNavigate } from '@tanstack/react-router';\r\nimport { toast } from 'sonner';\r\n\r\nimport { getProfile, register } from '@/apis/auth/client';\r\nimport { useAuthStore } from '@/store/auth-state';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\nimport type { TRegisterPayload, TRegisterResponse } from '@/apis/auth/client';\r\n\r\n/**\r\n * Drives the full signup flow, mirroring `useLoginMutation`:\r\n * 1. POST name/email/password, receive a bearer token.\r\n * 2. Fetch the current user's profile.\r\n * 3. Hydrate the Zustand auth store, which persists the token to the\r\n * shared auth cookie so `clientFetch` attaches the `Authorization`\r\n * header on subsequent requests.\r\n * 4. Redirect into the protected area.\r\n *\r\n * Errors are surfaced as toasts by the `clientFetch` response interceptor, so\r\n * no `onError` handler is needed here.\r\n */\r\nexport const useRegisterMutation = () => {\r\n const navigate = useNavigate();\r\n\r\n return useMutation<TRegisterResponse, Error, TRegisterPayload>({\r\n mutationKey: ['REGISTER'],\r\n mutationFn: payload => register(payload),\r\n onSuccess: async ({ data }) => {\r\n useAuthStore.getState().setToken(data.token);\r\n\r\n const profile = await getProfile();\r\n useAuthStore.getState().storeLogin({ user: profile, token: data.token });\r\n\r\n toast.success(m.signup_success());\r\n navigate({ to: '/dashboard' });\r\n },\r\n });\r\n};\r\n",
1312
+ "src/apis/query-keys/auth.ts": "export const authKeys = {\r\n all: ['auth'] as const,\r\n me: () => [...authKeys.all, 'me'] as const,\r\n} as const;\r\n\r\nexport type AuthKeys = typeof authKeys;\r\n",
1313
+ "src/store/auth-state.ts": "import jsCookie from 'js-cookie';\r\nimport { create } from 'zustand';\r\nimport { queryClient } from '@/lib/query-client';\r\nimport { AUTH_TOKEN_COOKIE } from '@/lib/auth/token-cookie';\r\nimport type { TUserProfileResponse } from '@/@types/user';\r\n\r\ntype AuthState = {\r\n isAuthenticated: boolean;\r\n user: Partial<TUserProfileResponse> | null;\r\n token: string | null;\r\n storeUser: (user: TUserProfileResponse | null) => void;\r\n logout: () => void;\r\n setToken: (token: string) => void;\r\n storeLogin: ({\r\n user,\r\n token,\r\n }: {\r\n user: TUserProfileResponse | null;\r\n token: string;\r\n }) => void;\r\n};\r\n\r\nconst initialToken = jsCookie.get(AUTH_TOKEN_COOKIE) ?? null;\r\n\r\nexport const useAuthStore = create<AuthState>()(set => ({\r\n isAuthenticated: Boolean(initialToken),\r\n user: null,\r\n token: initialToken,\r\n storeUser: user => {\r\n set({\r\n user,\r\n isAuthenticated: Boolean(user),\r\n });\r\n },\r\n logout: () => {\r\n set({\r\n isAuthenticated: false,\r\n user: null,\r\n token: null,\r\n });\r\n queryClient.clear();\r\n jsCookie.remove(AUTH_TOKEN_COOKIE, { path: '/' });\r\n },\r\n setToken: token => {\r\n jsCookie.set(AUTH_TOKEN_COOKIE, token, { expires: 3, path: '/' });\r\n set({ token });\r\n },\r\n storeLogin: ({ user, token }) => {\r\n jsCookie.set(AUTH_TOKEN_COOKIE, token, { expires: 3, path: '/' });\r\n set({ isAuthenticated: true, user, token });\r\n },\r\n}));\r\n",
1314
+ "src/lib/auth/token-cookie.ts": "export const AUTH_TOKEN_COOKIE = 'token';\r\n",
1315
+ "src/@types/user.ts": "// =============================================================RESPONSES=============================================================\r\n\r\nimport type { TMeta } from './common';\r\n\r\nexport type TUsersListResponse = {\r\n data: TUsersListData;\r\n meta: TMeta;\r\n};\r\n\r\nexport type TUserDetailsResponse = {\r\n data: TUserDetailsData;\r\n};\r\n\r\nexport type TUserProfileResponse = {\r\n data: TUserProfileData;\r\n};\r\n\r\n// =============================================================Data=============================================================\r\n\r\nexport type TUsersListData = TUsersListItem[];\r\n\r\nexport type TUserDetailsData = TUserDetailsItem;\r\n\r\nexport type TUserProfileData = TUserProfileItem;\r\n\r\n// =============================================================Item=============================================================\r\n\r\nexport type TUsersListItem = {\r\n id: number;\r\n name: string;\r\n email: string;\r\n avatar?: string;\r\n is_active: boolean;\r\n};\r\n\r\nexport type TUserDetailsItem = {\r\n id: number;\r\n name: string;\r\n email: string;\r\n phone?: string;\r\n avatar?: string;\r\n is_active: boolean;\r\n role: TUserRole;\r\n permissions: string[];\r\n created_at: string;\r\n updated_at: string;\r\n};\r\n\r\nexport type TUserProfileItem = {\r\n id: number;\r\n name: string;\r\n email: string;\r\n phone?: string;\r\n avatar?: string;\r\n is_active: boolean;\r\n role: TUserRole;\r\n permissions: string[];\r\n};\r\n\r\n// =============================================================Others=============================================================\r\n\r\nexport type TUserRole = {\r\n id: number;\r\n name: string;\r\n slug: string;\r\n};\r\n\r\n// Alias for backwards compatibility with auth-state\r\nexport type TUser = TUserProfileItem;\r\nexport type User = TUserProfileItem;\r\n",
1316
+ "src/components/common/controllers/input-field-controller.tsx": "import type { ComponentProps } from 'react';\r\nimport { useController } from 'react-hook-form';\r\nimport type {\r\n Control,\r\n FieldValues,\r\n Path,\r\n RegisterOptions,\r\n} from 'react-hook-form';\r\n\r\nimport { LabeledInput } from '@/components/ui/composed/input/labeled-input';\r\n\r\ntype InputFieldControllerProps<T extends FieldValues> = Omit<\r\n ComponentProps<typeof LabeledInput>,\r\n 'value' | 'onChange' | 'error'\r\n> & {\r\n name: Path<T>;\r\n control?: Control<T>;\r\n rules?: RegisterOptions<T>;\r\n formatter?: (value: string) => string;\r\n parser?: (value: string) => string;\r\n};\r\n\r\nexport function InputFieldController<T extends FieldValues>({\r\n name,\r\n control,\r\n rules,\r\n formatter,\r\n parser,\r\n ...rest\r\n}: InputFieldControllerProps<T>) {\r\n const { field, fieldState } = useController({ name, control, rules });\r\n const displayValue =\r\n typeof field.value === 'string'\r\n ? formatter\r\n ? formatter(field.value)\r\n : field.value\r\n : (field.value ?? '');\r\n\r\n return (\r\n <LabeledInput\r\n {...rest}\r\n value={displayValue}\r\n onChange={e => {\r\n const raw = e.target.value;\r\n field.onChange(parser ? parser(raw) : raw);\r\n }}\r\n onBlur={field.onBlur}\r\n error={fieldState.error?.message}\r\n />\r\n );\r\n}\r\n",
1317
+ "src/hooks/use-response-error.ts": "import type { FieldValues, UseFormSetError } from 'react-hook-form';\r\nimport type { ApiErrorResponse } from '@/lib/fetch/client';\r\n\r\nexport type ResponseError = ApiErrorResponse | { body?: ApiErrorResponse };\r\n\r\nconst isApiErrorResponse = (value: unknown): value is ApiErrorResponse => {\r\n if (!value || typeof value !== 'object') return false;\r\n\r\n return 'errors' in value || 'message' in value;\r\n};\r\n\r\nconst getResponseErrorBody = (error: unknown): ApiErrorResponse | null => {\r\n if (!error || typeof error !== 'object') return null;\r\n\r\n if ('body' in error && isApiErrorResponse(error.body)) {\r\n return error.body;\r\n }\r\n\r\n if (isApiErrorResponse(error)) {\r\n return error;\r\n }\r\n\r\n return null;\r\n};\r\n\r\n/**\r\n * Map a structured API error response into react-hook-form field errors.\r\n *\r\n * Call this from a mutation's `onError` (or any event handler) — there is no\r\n * reactive hook here. The previous `useEffect`-driven version is replaced by\r\n * direct invocation at the moment the error arrives, which is the pattern\r\n * CLAUDE.md prescribes for responding to user-triggered side effects.\r\n *\r\n * Example:\r\n * const { mutate } = useMutation({\r\n * mutationFn,\r\n * onError: (err: ResponseError) => setResponseFormErrors(setError, err),\r\n * })\r\n */\r\nexport const setResponseFormErrors = <TFieldValues extends FieldValues>(\r\n setError: UseFormSetError<TFieldValues>,\r\n error: unknown\r\n): void => {\r\n const body = getResponseErrorBody(error);\r\n if (!body?.errors) return;\r\n\r\n const { errors } = body;\r\n Object.keys(errors).forEach(fieldName => {\r\n const errorMessage = errors[fieldName][0];\r\n setError(fieldName as never, {\r\n type: 'manual',\r\n message: errorMessage,\r\n });\r\n });\r\n};\r\n",
1318
+ "cli/templates/nextjs/src/app/(public)/(auth)/layout.tsx": "'use client';\r\n\r\nimport { useEffect, type ReactNode } from 'react';\r\nimport { useRouter } from 'next/navigation';\r\n\r\nimport { AuthLayout } from '@/components/layouts';\r\nimport { useAuthStore } from '@/store/auth-state';\r\n\r\nexport default function AuthGroupLayout({ children }: { children: ReactNode }) {\r\n const router = useRouter();\r\n const isAuthenticated = useAuthStore(state => state.isAuthenticated);\r\n\r\n useEffect(() => {\r\n if (isAuthenticated) router.replace('/dashboard');\r\n }, [isAuthenticated, router]);\r\n\r\n if (isAuthenticated) return null;\r\n\r\n return <AuthLayout>{children}</AuthLayout>;\r\n}\r\n",
1319
+ "cli/templates/nextjs/src/app/(public)/(auth)/login/page.tsx": "'use client';\r\n\r\nimport { LoginScreen } from '@/components/screens/login';\r\n\r\nexport default function LoginPage() {\r\n return <LoginScreen />;\r\n}\r\n",
1320
+ "cli/templates/nextjs/src/apis/auth/hooks/mutations/use-login.mutation.ts": "'use client';\r\n\r\nimport { useMutation } from '@tanstack/react-query';\r\nimport { useRouter } from 'next/navigation';\r\nimport { toast } from 'sonner';\r\n\r\nimport { getProfile, login } from '@/apis/auth/client';\r\nimport { t } from '@/lib/composed-strings';\r\nimport { useAuthStore } from '@/store/auth-state';\r\n\r\nimport type { TLoginPayload, TLoginResponse } from '@/apis/auth/client';\r\n\r\nexport const useLoginMutation = () => {\r\n const router = useRouter();\r\n\r\n return useMutation<TLoginResponse, Error, TLoginPayload>({\r\n mutationKey: ['LOGIN'],\r\n mutationFn: payload => login(payload),\r\n onSuccess: async ({ data }) => {\r\n useAuthStore.getState().setToken(data.token);\r\n\r\n const profile = await getProfile();\r\n useAuthStore.getState().storeLogin({ user: profile, token: data.token });\r\n\r\n toast.success(t('login_success'));\r\n router.push('/dashboard');\r\n },\r\n });\r\n};\r\n",
1321
+ "cli/templates/nextjs/src/apis/auth/hooks/mutations/use-register.mutation.ts": "'use client';\r\n\r\nimport { useMutation } from '@tanstack/react-query';\r\nimport { useRouter } from 'next/navigation';\r\nimport { toast } from 'sonner';\r\n\r\nimport { getProfile, register } from '@/apis/auth/client';\r\nimport { t } from '@/lib/composed-strings';\r\nimport { useAuthStore } from '@/store/auth-state';\r\n\r\nimport type { TRegisterPayload, TRegisterResponse } from '@/apis/auth/client';\r\n\r\nexport const useRegisterMutation = () => {\r\n const router = useRouter();\r\n\r\n return useMutation<TRegisterResponse, Error, TRegisterPayload>({\r\n mutationKey: ['REGISTER'],\r\n mutationFn: payload => register(payload),\r\n onSuccess: async ({ data }) => {\r\n useAuthStore.getState().setToken(data.token);\r\n\r\n const profile = await getProfile();\r\n useAuthStore.getState().storeLogin({ user: profile, token: data.token });\r\n\r\n toast.success(t('signup_success'));\r\n router.push('/dashboard');\r\n },\r\n });\r\n};\r\n",
1322
+ "src/lib/query-client.ts": "import { QueryClient } from '@tanstack/react-query';\r\n\r\nexport const queryClient = new QueryClient({\r\n defaultOptions: {\r\n queries: {\r\n staleTime: 60 * 1000, // 1 minute\r\n gcTime: 10 * 60 * 1000, // 10 minutes\r\n retry: 0,\r\n refetchOnWindowFocus: false,\r\n },\r\n mutations: {\r\n retry: 0,\r\n },\r\n },\r\n});\r\n",
1323
+ "src/routes/(public-pages)/(auth)/signup.tsx": "import { createFileRoute } from '@tanstack/react-router';\r\nimport { SignupScreen } from '@/components/screens/signup';\r\nimport { m } from '#/paraglide/messages';\r\n\r\nexport const Route = createFileRoute('/(public-pages)/(auth)/signup')({\r\n head: () => ({ meta: [{ title: m.signup_page() }] }),\r\n component: SignupScreen,\r\n});\r\n",
1324
+ "src/components/screens/signup/index.ts": "export { SignupScreen } from './signup-screen';\r\n",
1325
+ "src/components/screens/signup/signup-screen.tsx": "import { m } from '#/paraglide/messages';\r\n\r\nimport { SignupForm } from './signup-form';\r\n\r\nexport function SignupScreen() {\r\n return (\r\n <main className='w-full max-w-md'>\r\n <div className='rounded-xl border border-border bg-card p-8 shadow-sm'>\r\n <div className='mb-8 text-center'>\r\n <h1 className='text-2xl font-bold tracking-tight text-foreground'>\r\n {m.signup_title()}\r\n </h1>\r\n <p className='text-muted-foreground mt-2 text-sm'>\r\n {m.signup_intro()}\r\n </p>\r\n </div>\r\n\r\n <SignupForm />\r\n </div>\r\n </main>\r\n );\r\n}\r\n",
1326
+ "src/components/screens/signup/signup-form.tsx": "import { zodResolver } from '@hookform/resolvers/zod';\r\nimport { useForm } from 'react-hook-form';\r\n\r\nimport { useRegisterMutation } from '@/apis/auth/hooks';\r\nimport { InputFieldController } from '@/components/common/controllers';\r\nimport { LoadingButton } from '@/components/ui/composed/button/loading-button';\r\nimport { setResponseFormErrors } from '@/hooks/use-response-error';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\nimport { signupSchema } from './signup-schema';\r\n\r\nimport type { TSignupFormValues } from './signup-schema';\r\n\r\nexport function SignupForm() {\r\n const { mutate, isPending, isSuccess } = useRegisterMutation();\r\n\r\n const form = useForm<TSignupFormValues>({\r\n resolver: zodResolver(signupSchema()),\r\n defaultValues: { name: '', email: '', password: '', confirmPassword: '' },\r\n });\r\n\r\n // Keep the button busy through the post-success redirect.\r\n const isLoading = isPending || isSuccess;\r\n\r\n const onSubmit = (values: TSignupFormValues) =>\r\n mutate(\r\n { name: values.name, email: values.email, password: values.password },\r\n { onError: error => setResponseFormErrors(form.setError, error) }\r\n );\r\n\r\n return (\r\n <form\r\n noValidate\r\n autoComplete='off'\r\n onSubmit={form.handleSubmit(onSubmit)}\r\n className='flex flex-col gap-6'\r\n >\r\n <InputFieldController\r\n control={form.control}\r\n name='name'\r\n type='text'\r\n autoComplete='name'\r\n label={m.signup_name_label()}\r\n placeholder={m.signup_name_placeholder()}\r\n />\r\n\r\n <InputFieldController\r\n control={form.control}\r\n name='email'\r\n type='email'\r\n inputMode='email'\r\n autoComplete='email'\r\n label={m.signup_email_label()}\r\n placeholder={m.signup_email_placeholder()}\r\n />\r\n\r\n <InputFieldController\r\n control={form.control}\r\n name='password'\r\n type='password'\r\n autoComplete='new-password'\r\n label={m.signup_password_label()}\r\n placeholder={m.signup_password_placeholder()}\r\n />\r\n\r\n <InputFieldController\r\n control={form.control}\r\n name='confirmPassword'\r\n type='password'\r\n autoComplete='new-password'\r\n label={m.signup_confirm_password_label()}\r\n placeholder={m.signup_confirm_password_placeholder()}\r\n />\r\n\r\n <LoadingButton\r\n type='submit'\r\n className='w-full'\r\n loading={isLoading}\r\n loadingText={m.signup_submitting()}\r\n >\r\n {m.signup_submit()}\r\n </LoadingButton>\r\n </form>\r\n );\r\n}\r\n",
1327
+ "src/components/screens/signup/signup-schema.ts": "import { z } from 'zod';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\n/**\r\n * Built lazily as a function so the Paraglide message getters resolve against\r\n * the active locale at validation time (same pattern as loginSchema).\r\n */\r\nexport const signupSchema = () =>\r\n z\r\n .object({\r\n name: z.string().min(1, { message: m.validation_name_required() }),\r\n email: z\r\n .string()\r\n .min(1, { message: m.validation_email_required() })\r\n .email({ message: m.validation_email_invalid() }),\r\n password: z\r\n .string()\r\n .min(1, { message: m.validation_password_required() }),\r\n confirmPassword: z\r\n .string()\r\n .min(1, { message: m.validation_confirm_password_required() }),\r\n })\r\n .refine(values => values.password === values.confirmPassword, {\r\n message: m.validation_password_mismatch(),\r\n path: ['confirmPassword'],\r\n });\r\n\r\nexport type TSignupFormValues = z.infer<ReturnType<typeof signupSchema>>;\r\n",
1328
+ "cli/templates/nextjs/src/app/(public)/(auth)/signup/page.tsx": "'use client';\r\n\r\nimport { SignupScreen } from '@/components/screens/signup';\r\n\r\nexport default function SignupPage() {\r\n return <SignupScreen />;\r\n}\r\n",
1329
+ "src/routes/(protected-pages)/route.tsx": "import { Outlet, createFileRoute, redirect } from '@tanstack/react-router';\r\nimport { useQuery } from '@tanstack/react-query';\r\n\r\nimport { getProfile } from '@/apis/auth/client';\r\nimport { authKeys } from '@/apis/query-keys';\r\nimport { DashboardLayout } from '@/components/layouts';\r\nimport { useAuthStore } from '@/store/auth-state';\r\n\r\n/**\r\n * Client-side auth guard for every page under `(protected-pages)`.\r\n *\r\n * This app runs as a client-side SPA (see `src/start.ts`'s `defaultSsr:\r\n * false`), so `beforeLoad` only ever runs in the browser — reading\r\n * `isAuthenticated` straight from the Zustand store (hydrated synchronously\r\n * from the `token` cookie) is sufficient; no server round trip is needed.\r\n *\r\n * The layout component separately fetches the current user via React Query\r\n * and stores it in Zustand — that's reactive profile state (instant display\r\n * elsewhere in the UI, kept fresh via React Query), not the auth guard\r\n * itself.\r\n */\r\nexport const Route = createFileRoute('/(protected-pages)')({\r\n beforeLoad: () => {\r\n if (!useAuthStore.getState().isAuthenticated) {\r\n throw redirect({ to: '/login' });\r\n }\r\n },\r\n\r\n component: ProtectedLayout,\r\n});\r\n\r\nfunction ProtectedLayout() {\r\n useQuery({\r\n queryKey: authKeys.me(),\r\n queryFn: async () => {\r\n const profile = await getProfile();\r\n useAuthStore.getState().storeUser(profile);\r\n return profile;\r\n },\r\n });\r\n\r\n return (\r\n <DashboardLayout>\r\n <Outlet />\r\n </DashboardLayout>\r\n );\r\n}\r\n",
1330
+ "src/routes/(protected-pages)/dashboard.tsx": "import { createFileRoute } from '@tanstack/react-router';\r\n\r\nimport { DashboardScreen } from '@/components/screens/dashboard';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\nexport const Route = createFileRoute('/(protected-pages)/dashboard')({\r\n head: () => ({ meta: [{ title: m.dashboard_page() }] }),\r\n component: DashboardScreen,\r\n});\r\n",
1331
+ "src/components/screens/dashboard/index.ts": "export { DashboardScreen } from './dashboard-screen';\r\n",
1332
+ "src/components/screens/dashboard/dashboard-screen.tsx": "import { m } from '#/paraglide/messages';\r\n\r\nexport function DashboardScreen() {\r\n return (\r\n <div className='mx-auto max-w-3xl px-6 py-12'>\r\n <h1 className='text-4xl font-bold tracking-tight'>\r\n {m.dashboard_title()}\r\n </h1>\r\n <p className='text-muted-foreground mt-4 text-lg'>\r\n {m.dashboard_intro()}\r\n </p>\r\n </div>\r\n );\r\n}\r\n",
1333
+ "src/components/layouts/dashboard-layout.tsx": "import type { ReactNode } from 'react';\r\n\r\nimport {\r\n SidebarInset,\r\n SidebarProvider,\r\n SidebarTrigger,\r\n} from '@/components/ui/sidebar';\r\nimport { ThemeToggle } from '@/components/ui/composed/button/theme-toggle';\r\nimport { LocaleSwitcher } from '@/components/ui/composed/dropdown-menu/locale-switcher';\r\nimport { TooltipProvider } from '@/components/ui/tooltip';\r\nimport { useDir } from '@/hooks/use-dir';\r\n\r\nimport { DashboardSidebar } from './dashboard-sidebar';\r\n\r\ntype DashboardLayoutProps = {\r\n children: ReactNode;\r\n};\r\n\r\nexport function DashboardLayout({ children }: DashboardLayoutProps) {\r\n const direction = useDir();\r\n const side = direction === 'rtl' ? 'right' : 'left';\r\n\r\n return (\r\n <TooltipProvider>\r\n <SidebarProvider>\r\n <DashboardSidebar side={side} dir={direction} />\r\n <SidebarInset>\r\n <header className='bg-background sticky top-0 z-10 flex h-14 items-center gap-2 border-b px-4'>\r\n <SidebarTrigger />\r\n <div className='ms-auto flex items-center gap-2'>\r\n <LocaleSwitcher />\r\n <ThemeToggle />\r\n </div>\r\n </header>\r\n <div className='flex-1'>{children}</div>\r\n </SidebarInset>\r\n </SidebarProvider>\r\n </TooltipProvider>\r\n );\r\n}\r\n",
1334
+ "src/components/layouts/dashboard-sidebar.tsx": "import { Link, useMatchRoute, useNavigate } from '@tanstack/react-router';\r\nimport { LayoutDashboardIcon, LogOutIcon } from 'lucide-react';\r\n\r\nimport { UserAvatar } from '@/components/ui/composed/avatar/user-avatar';\r\nimport {\r\n Sidebar,\r\n SidebarContent,\r\n SidebarFooter,\r\n SidebarGroup,\r\n SidebarGroupContent,\r\n SidebarHeader,\r\n SidebarMenu,\r\n SidebarMenuButton,\r\n SidebarMenuItem,\r\n SidebarRail,\r\n} from '@/components/ui/sidebar';\r\nimport { useAuthStore } from '@/store/auth-state';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\ntype DashboardSidebarProps = {\r\n side?: 'left' | 'right';\r\n dir?: 'ltr' | 'rtl';\r\n};\r\n\r\nexport function DashboardSidebar({\r\n side = 'left',\r\n dir,\r\n}: DashboardSidebarProps) {\r\n const matchRoute = useMatchRoute();\r\n const navigate = useNavigate();\r\n const user = useAuthStore(state => state.user);\r\n const logout = useAuthStore(state => state.logout);\r\n\r\n const profile = user?.data;\r\n const displayName = profile?.name ?? profile?.email ?? '';\r\n const email = profile?.email ?? '';\r\n const isDashboard = Boolean(matchRoute({ to: '/dashboard' }));\r\n\r\n const onLogout = () => {\r\n logout();\r\n navigate({ to: '/login' });\r\n };\r\n\r\n return (\r\n <Sidebar side={side} dir={dir} collapsible='icon'>\r\n <SidebarHeader>\r\n <SidebarMenu>\r\n <SidebarMenuItem>\r\n <SidebarMenuButton size='lg' render={<Link to='/dashboard' />}>\r\n <span className='text-sm font-semibold'>\r\n {m.dashboard_page()}\r\n </span>\r\n </SidebarMenuButton>\r\n </SidebarMenuItem>\r\n </SidebarMenu>\r\n </SidebarHeader>\r\n <SidebarContent>\r\n <SidebarGroup>\r\n <SidebarGroupContent>\r\n <SidebarMenu>\r\n <SidebarMenuItem>\r\n <SidebarMenuButton\r\n isActive={isDashboard}\r\n tooltip={m.dashboard_page()}\r\n render={<Link to='/dashboard' />}\r\n >\r\n <LayoutDashboardIcon />\r\n <span>{m.dashboard_page()}</span>\r\n </SidebarMenuButton>\r\n </SidebarMenuItem>\r\n </SidebarMenu>\r\n </SidebarGroupContent>\r\n </SidebarGroup>\r\n </SidebarContent>\r\n <SidebarFooter>\r\n <SidebarMenu>\r\n {displayName ? (\r\n <SidebarMenuItem>\r\n <SidebarMenuButton size='lg' className='pointer-events-none'>\r\n <UserAvatar name={displayName} src={profile?.avatar} />\r\n <span className='grid flex-1 text-start text-sm leading-tight'>\r\n <span className='truncate font-medium'>{displayName}</span>\r\n {email && displayName !== email ? (\r\n <span className='text-muted-foreground truncate text-xs'>\r\n {email}\r\n </span>\r\n ) : null}\r\n </span>\r\n </SidebarMenuButton>\r\n </SidebarMenuItem>\r\n ) : null}\r\n <SidebarMenuItem>\r\n <SidebarMenuButton\r\n tooltip={m.dashboard_logout()}\r\n onClick={onLogout}\r\n >\r\n <LogOutIcon />\r\n <span>{m.dashboard_logout()}</span>\r\n </SidebarMenuButton>\r\n </SidebarMenuItem>\r\n </SidebarMenu>\r\n </SidebarFooter>\r\n <SidebarRail />\r\n </Sidebar>\r\n );\r\n}\r\n",
1335
+ "cli/templates/nextjs/src/app/(protected)/layout.tsx": "'use client';\r\n\r\nimport { useEffect, type ReactNode } from 'react';\r\nimport { useRouter } from 'next/navigation';\r\n\r\nimport { getProfile } from '@/apis/auth/client';\r\nimport { DashboardLayout } from '@/components/layouts';\r\nimport { useAuthStore } from '@/store/auth-state';\r\n\r\nexport default function ProtectedLayout({ children }: { children: ReactNode }) {\r\n const router = useRouter();\r\n const isAuthenticated = useAuthStore(state => state.isAuthenticated);\r\n\r\n useEffect(() => {\r\n if (!isAuthenticated) {\r\n router.replace('/login');\r\n return;\r\n }\r\n void getProfile().then(profile => useAuthStore.getState().storeUser(profile));\r\n }, [isAuthenticated, router]);\r\n\r\n if (!isAuthenticated) return null;\r\n\r\n return <DashboardLayout>{children}</DashboardLayout>;\r\n}\r\n",
1336
+ "cli/templates/nextjs/src/app/(protected)/dashboard/page.tsx": "'use client';\r\n\r\nimport { DashboardScreen } from '@/components/screens/dashboard';\r\n\r\nexport default function DashboardPage() {\r\n return <DashboardScreen />;\r\n}\r\n",
1337
+ "cli/templates/nextjs/src/components/layouts/dashboard-sidebar.tsx": "'use client';\r\n\r\nimport Link from 'next/link';\r\nimport { usePathname, useRouter } from 'next/navigation';\r\nimport { LayoutDashboardIcon, LogOutIcon } from 'lucide-react';\r\n\r\nimport { UserAvatar } from '@/components/ui/composed/avatar/user-avatar';\r\nimport {\r\n Sidebar,\r\n SidebarContent,\r\n SidebarFooter,\r\n SidebarGroup,\r\n SidebarGroupContent,\r\n SidebarHeader,\r\n SidebarMenu,\r\n SidebarMenuButton,\r\n SidebarMenuItem,\r\n SidebarRail,\r\n} from '@/components/ui/sidebar';\r\nimport { t } from '@/lib/composed-strings';\r\nimport { useAuthStore } from '@/store/auth-state';\r\n\r\ntype DashboardSidebarProps = {\r\n side?: 'left' | 'right';\r\n dir?: 'ltr' | 'rtl';\r\n};\r\n\r\nexport function DashboardSidebar({\r\n side = 'left',\r\n dir,\r\n}: DashboardSidebarProps) {\r\n const pathname = usePathname();\r\n const router = useRouter();\r\n const user = useAuthStore(state => state.user);\r\n const logout = useAuthStore(state => state.logout);\r\n\r\n const profile = user?.data;\r\n const displayName = profile?.name ?? profile?.email ?? '';\r\n const email = profile?.email ?? '';\r\n const isDashboard = pathname === '/dashboard' || pathname.startsWith('/dashboard/');\r\n\r\n const onLogout = () => {\r\n logout();\r\n router.push('/login');\r\n };\r\n\r\n return (\r\n <Sidebar side={side} dir={dir} collapsible='icon'>\r\n <SidebarHeader>\r\n <SidebarMenu>\r\n <SidebarMenuItem>\r\n <SidebarMenuButton size='lg' render={<Link href='/dashboard' />}>\r\n <span className='text-sm font-semibold'>{t('dashboard_page')}</span>\r\n </SidebarMenuButton>\r\n </SidebarMenuItem>\r\n </SidebarMenu>\r\n </SidebarHeader>\r\n <SidebarContent>\r\n <SidebarGroup>\r\n <SidebarGroupContent>\r\n <SidebarMenu>\r\n <SidebarMenuItem>\r\n <SidebarMenuButton\r\n isActive={isDashboard}\r\n tooltip={t('dashboard_page')}\r\n render={<Link href='/dashboard' />}\r\n >\r\n <LayoutDashboardIcon />\r\n <span>{t('dashboard_page')}</span>\r\n </SidebarMenuButton>\r\n </SidebarMenuItem>\r\n </SidebarMenu>\r\n </SidebarGroupContent>\r\n </SidebarGroup>\r\n </SidebarContent>\r\n <SidebarFooter>\r\n <SidebarMenu>\r\n {displayName ? (\r\n <SidebarMenuItem>\r\n <SidebarMenuButton size='lg' className='pointer-events-none'>\r\n <UserAvatar name={displayName} src={profile?.avatar} />\r\n <span className='grid flex-1 text-start text-sm leading-tight'>\r\n <span className='truncate font-medium'>{displayName}</span>\r\n {email && displayName !== email ? (\r\n <span className='text-muted-foreground truncate text-xs'>\r\n {email}\r\n </span>\r\n ) : null}\r\n </span>\r\n </SidebarMenuButton>\r\n </SidebarMenuItem>\r\n ) : null}\r\n <SidebarMenuItem>\r\n <SidebarMenuButton tooltip={t('dashboard_logout')} onClick={onLogout}>\r\n <LogOutIcon />\r\n <span>{t('dashboard_logout')}</span>\r\n </SidebarMenuButton>\r\n </SidebarMenuItem>\r\n </SidebarMenu>\r\n </SidebarFooter>\r\n <SidebarRail />\r\n </Sidebar>\r\n );\r\n}\r\n",
1338
+ "src/routes/(public-pages)/about.tsx": "import { createFileRoute } from '@tanstack/react-router';\r\n\r\nimport { AboutScreen } from '@/components/screens/about';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\nexport const Route = createFileRoute('/(public-pages)/about')({\r\n head: () => ({ meta: [{ title: m.about_page() }] }),\r\n component: AboutScreen,\r\n});\r\n",
1339
+ "src/components/screens/about/index.ts": "export { AboutScreen } from './about-screen';\r\n",
1340
+ "src/components/screens/about/about-screen.tsx": "import { m } from '#/paraglide/messages';\r\n\r\nexport function AboutScreen() {\r\n return (\r\n <main className='mx-auto max-w-3xl px-6 py-12'>\r\n <h1 className='text-4xl font-bold tracking-tight'>{m.about_title()}</h1>\r\n <p className='text-muted-foreground mt-4 text-lg'>{m.about_intro()}</p>\r\n </main>\r\n );\r\n}\r\n",
1341
+ "cli/templates/nextjs/src/app/(public)/about/page.tsx": "import { AboutScreen } from '@/components/screens/about';\r\n\r\nexport default function AboutPage() {\r\n return <AboutScreen />;\r\n}\r\n",
1342
+ "src/routes/(public-pages)/contact.tsx": "import { createFileRoute } from '@tanstack/react-router';\r\n\r\nimport { ContactScreen } from '@/components/screens/contact';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\nexport const Route = createFileRoute('/(public-pages)/contact')({\r\n head: () => ({ meta: [{ title: m.contact_page() }] }),\r\n component: ContactScreen,\r\n});\r\n",
1343
+ "src/components/screens/contact/index.ts": "export { ContactScreen } from './contact-screen';\r\n",
1344
+ "src/components/screens/contact/contact-screen.tsx": "import { m } from '#/paraglide/messages';\r\n\r\nexport function ContactScreen() {\r\n return (\r\n <main className='mx-auto max-w-3xl px-6 py-12'>\r\n <h1 className='text-4xl font-bold tracking-tight'>{m.contact_title()}</h1>\r\n <p className='text-muted-foreground mt-4 text-lg'>{m.contact_intro()}</p>\r\n </main>\r\n );\r\n}\r\n",
1345
+ "cli/templates/nextjs/src/app/(public)/contact/page.tsx": "import { ContactScreen } from '@/components/screens/contact';\r\n\r\nexport default function ContactPage() {\r\n return <ContactScreen />;\r\n}\r\n",
1346
+ "src/components/common/controllers/checkbox-controller.tsx": "import type { ComponentProps } from 'react';\r\nimport { useController } from 'react-hook-form';\r\nimport type {\r\n Control,\r\n FieldValues,\r\n Path,\r\n RegisterOptions,\r\n} from 'react-hook-form';\r\n\r\nimport { CheckboxField } from '@/components/ui/composed/checkbox/checkbox-field';\r\n\r\ntype CheckboxControllerProps<T extends FieldValues> = Omit<\r\n ComponentProps<typeof CheckboxField>,\r\n 'checked' | 'onCheckedChange' | 'error'\r\n> & {\r\n name: Path<T>;\r\n control?: Control<T>;\r\n rules?: RegisterOptions<T>;\r\n};\r\n\r\nexport function CheckboxController<T extends FieldValues>({\r\n name,\r\n control,\r\n rules,\r\n ...rest\r\n}: CheckboxControllerProps<T>) {\r\n const { field, fieldState } = useController({ name, control, rules });\r\n\r\n return (\r\n <CheckboxField\r\n {...rest}\r\n checked={field.value ?? false}\r\n onCheckedChange={field.onChange}\r\n error={fieldState.error?.message}\r\n />\r\n );\r\n}\r\n",
1347
+ "src/components/common/controllers/checkbox-group-controller.tsx": "import type { ComponentProps } from 'react';\r\nimport { useController } from 'react-hook-form';\r\nimport type {\r\n Control,\r\n FieldValues,\r\n Path,\r\n RegisterOptions,\r\n} from 'react-hook-form';\r\n\r\nimport { CheckboxGroup } from '@/components/ui/composed/checkbox/checkbox-group';\r\n\r\ntype CheckboxGroupControllerProps<T extends FieldValues> = Omit<\r\n ComponentProps<typeof CheckboxGroup>,\r\n 'value' | 'defaultValue' | 'onValueChange' | 'error'\r\n> & {\r\n name: Path<T>;\r\n control?: Control<T>;\r\n rules?: RegisterOptions<T>;\r\n};\r\n\r\nexport function CheckboxGroupController<T extends FieldValues>({\r\n name,\r\n control,\r\n rules,\r\n ...rest\r\n}: CheckboxGroupControllerProps<T>) {\r\n const { field, fieldState } = useController({ name, control, rules });\r\n\r\n return (\r\n <CheckboxGroup\r\n {...rest}\r\n value={field.value ?? []}\r\n onValueChange={field.onChange}\r\n error={fieldState.error?.message}\r\n />\r\n );\r\n}\r\n",
1348
+ "src/components/common/controllers/combobox-controller.tsx": "import type { ComponentProps } from 'react';\r\nimport { useController } from 'react-hook-form';\r\nimport type {\r\n Control,\r\n FieldValues,\r\n Path,\r\n RegisterOptions,\r\n} from 'react-hook-form';\r\n\r\nimport { SearchableSelect } from '@/components/ui/composed/combobox/searchable-select';\r\n\r\ntype ComboboxControllerProps<T extends FieldValues> = Omit<\r\n ComponentProps<typeof SearchableSelect>,\r\n 'value' | 'onValueChange' | 'error'\r\n> & {\r\n name: Path<T>;\r\n control?: Control<T>;\r\n rules?: RegisterOptions<T>;\r\n};\r\n\r\nexport function ComboboxController<T extends FieldValues>({\r\n name,\r\n control,\r\n rules,\r\n ...rest\r\n}: ComboboxControllerProps<T>) {\r\n const { field, fieldState } = useController({ name, control, rules });\r\n\r\n return (\r\n <SearchableSelect\r\n {...rest}\r\n value={field.value ?? ''}\r\n onValueChange={field.onChange}\r\n error={fieldState.error?.message}\r\n />\r\n );\r\n}\r\n",
1349
+ "src/components/common/controllers/date-picker-controller.tsx": "import type { ComponentProps } from 'react';\r\nimport { useController } from 'react-hook-form';\r\nimport type {\r\n Control,\r\n FieldValues,\r\n Path,\r\n RegisterOptions,\r\n} from 'react-hook-form';\r\n\r\nimport { DatePickerField } from '@/components/ui/composed/date-picker/date-picker-field';\r\n\r\ntype DatePickerControllerProps<T extends FieldValues> = Omit<\r\n ComponentProps<typeof DatePickerField>,\r\n 'value' | 'onValueChange' | 'error'\r\n> & {\r\n name: Path<T>;\r\n control?: Control<T>;\r\n rules?: RegisterOptions<T>;\r\n};\r\n\r\nexport function DatePickerController<T extends FieldValues>({\r\n name,\r\n control,\r\n rules,\r\n ...rest\r\n}: DatePickerControllerProps<T>) {\r\n const { field, fieldState } = useController({ name, control, rules });\r\n\r\n return (\r\n <DatePickerField\r\n {...rest}\r\n value={field.value}\r\n onValueChange={field.onChange}\r\n error={fieldState.error?.message}\r\n />\r\n );\r\n}\r\n",
1350
+ "src/components/common/controllers/file-dropzone-controller.tsx": "import type { ComponentProps } from 'react';\r\nimport { useController } from 'react-hook-form';\r\nimport type {\r\n Control,\r\n FieldValues,\r\n Path,\r\n RegisterOptions,\r\n} from 'react-hook-form';\r\n\r\nimport { FileDropzone } from '@/components/ui/composed/upload/file-dropzone';\r\n\r\ntype FileDropzoneControllerProps<T extends FieldValues> = Omit<\r\n ComponentProps<typeof FileDropzone>,\r\n 'value' | 'onValueChange' | 'error'\r\n> & {\r\n name: Path<T>;\r\n control?: Control<T>;\r\n rules?: RegisterOptions<T>;\r\n};\r\n\r\nexport function FileDropzoneController<T extends FieldValues>({\r\n name,\r\n control,\r\n rules,\r\n ...rest\r\n}: FileDropzoneControllerProps<T>) {\r\n const { field, fieldState } = useController({ name, control, rules });\r\n\r\n return (\r\n <FileDropzone\r\n {...rest}\r\n value={field.value ?? []}\r\n onValueChange={field.onChange}\r\n error={fieldState.error?.message}\r\n />\r\n );\r\n}\r\n",
1351
+ "src/components/common/controllers/index.ts": "export { CheckboxController } from './checkbox-controller';\r\nexport { CheckboxGroupController } from './checkbox-group-controller';\r\nexport { InputFieldController } from './input-field-controller';\r\nexport { OtpFieldController } from './input-otp-controller';\r\nexport { RadioGroupController } from './radio-group-controller';\r\nexport { ComboboxController } from './combobox-controller';\r\nexport { DatePickerController } from './date-picker-controller';\r\nexport { FileDropzoneController } from './file-dropzone-controller';\r\nexport { SelectController } from './select-controller';\r\nexport { SelectGroupsController } from './select-groups-controller';\r\nexport { TextareaController } from './textarea-controller';\r\n",
1352
+ "src/components/common/controllers/input-otp-controller.tsx": "import type { ComponentProps } from 'react';\r\nimport { useController } from 'react-hook-form';\r\nimport type {\r\n Control,\r\n FieldValues,\r\n Path,\r\n RegisterOptions,\r\n} from 'react-hook-form';\r\n\r\nimport { OtpField } from '@/components/ui/composed/input-otp/otp-field';\r\n\r\ntype OtpFieldControllerProps<T extends FieldValues> = Omit<\r\n ComponentProps<typeof OtpField>,\r\n 'value' | 'onChange' | 'error'\r\n> & {\r\n name: Path<T>;\r\n control?: Control<T>;\r\n rules?: RegisterOptions<T>;\r\n};\r\n\r\nexport function OtpFieldController<T extends FieldValues>({\r\n name,\r\n control,\r\n rules,\r\n ...rest\r\n}: OtpFieldControllerProps<T>) {\r\n const { field, fieldState } = useController({ name, control, rules });\r\n\r\n return (\r\n <OtpField\r\n {...rest}\r\n value={field.value ?? ''}\r\n onChange={field.onChange}\r\n error={fieldState.error?.message}\r\n />\r\n );\r\n}\r\n",
1353
+ "src/components/common/controllers/radio-group-controller.tsx": "import type { ComponentProps } from 'react';\r\nimport { useController } from 'react-hook-form';\r\nimport type {\r\n Control,\r\n FieldValues,\r\n Path,\r\n RegisterOptions,\r\n} from 'react-hook-form';\r\n\r\nimport { OptionsRadioGroup } from '@/components/ui/composed/radio-group/options-radio-group';\r\n\r\ntype RadioGroupControllerProps<T extends FieldValues> = Omit<\r\n ComponentProps<typeof OptionsRadioGroup>,\r\n 'value' | 'onValueChange' | 'error'\r\n> & {\r\n name: Path<T>;\r\n control?: Control<T>;\r\n rules?: RegisterOptions<T>;\r\n};\r\n\r\nexport function RadioGroupController<T extends FieldValues>({\r\n name,\r\n control,\r\n rules,\r\n ...rest\r\n}: RadioGroupControllerProps<T>) {\r\n const { field, fieldState } = useController({ name, control, rules });\r\n\r\n return (\r\n <OptionsRadioGroup\r\n {...rest}\r\n value={field.value}\r\n onValueChange={field.onChange}\r\n error={fieldState.error?.message}\r\n />\r\n );\r\n}\r\n",
1354
+ "src/components/common/controllers/select-controller.tsx": "import type { ComponentProps } from 'react';\r\nimport { useController } from 'react-hook-form';\r\nimport type {\r\n Control,\r\n FieldValues,\r\n Path,\r\n RegisterOptions,\r\n} from 'react-hook-form';\r\n\r\nimport { LabeledSelect } from '@/components/ui/composed/select/labeled-select';\r\n\r\ntype SelectControllerProps<T extends FieldValues> = Omit<\r\n ComponentProps<typeof LabeledSelect>,\r\n 'value' | 'onValueChange' | 'error'\r\n> & {\r\n name: Path<T>;\r\n control?: Control<T>;\r\n rules?: RegisterOptions<T>;\r\n};\r\n\r\nexport function SelectController<T extends FieldValues>({\r\n name,\r\n control,\r\n rules,\r\n ...rest\r\n}: SelectControllerProps<T>) {\r\n const { field, fieldState } = useController({ name, control, rules });\r\n\r\n return (\r\n <LabeledSelect\r\n {...rest}\r\n value={field.value ?? ''}\r\n onValueChange={field.onChange}\r\n error={fieldState.error?.message}\r\n />\r\n );\r\n}\r\n",
1355
+ "src/components/common/controllers/select-groups-controller.tsx": "import type { ComponentProps } from 'react';\r\nimport { useController } from 'react-hook-form';\r\nimport type {\r\n Control,\r\n FieldValues,\r\n Path,\r\n RegisterOptions,\r\n} from 'react-hook-form';\r\n\r\nimport { SelectGroups } from '@/components/ui/composed/select/select-groups';\r\n\r\ntype SelectGroupsControllerProps<T extends FieldValues> = Omit<\r\n ComponentProps<typeof SelectGroups>,\r\n 'value' | 'onValueChange' | 'error'\r\n> & {\r\n name: Path<T>;\r\n control?: Control<T>;\r\n rules?: RegisterOptions<T>;\r\n};\r\n\r\nexport function SelectGroupsController<T extends FieldValues>({\r\n name,\r\n control,\r\n rules,\r\n ...rest\r\n}: SelectGroupsControllerProps<T>) {\r\n const { field, fieldState } = useController({ name, control, rules });\r\n\r\n return (\r\n <SelectGroups\r\n {...rest}\r\n value={field.value ?? ''}\r\n onValueChange={field.onChange}\r\n error={fieldState.error?.message}\r\n />\r\n );\r\n}\r\n",
1356
+ "src/components/common/controllers/textarea-controller.tsx": "import type { ComponentProps } from 'react';\r\nimport { useController } from 'react-hook-form';\r\nimport type {\r\n Control,\r\n FieldValues,\r\n Path,\r\n RegisterOptions,\r\n} from 'react-hook-form';\r\n\r\nimport { LabeledTextarea } from '@/components/ui/composed/textarea/labeled-textarea';\r\n\r\ntype TextareaControllerProps<T extends FieldValues> = Omit<\r\n ComponentProps<typeof LabeledTextarea>,\r\n 'value' | 'onChange' | 'error'\r\n> & {\r\n name: Path<T>;\r\n control?: Control<T>;\r\n rules?: RegisterOptions<T>;\r\n};\r\n\r\nexport function TextareaController<T extends FieldValues>({\r\n name,\r\n control,\r\n rules,\r\n ...rest\r\n}: TextareaControllerProps<T>) {\r\n const { field, fieldState } = useController({ name, control, rules });\r\n\r\n return (\r\n <LabeledTextarea\r\n {...rest}\r\n value={field.value ?? ''}\r\n onChange={field.onChange}\r\n onBlur={field.onBlur}\r\n error={fieldState.error?.message}\r\n />\r\n );\r\n}\r\n",
964
1357
  "src/lib/utils.ts": "import { clsx } from 'clsx';\r\nimport type { ClassValue } from 'clsx';\r\nimport { twMerge } from 'tailwind-merge';\r\n\r\nexport function cn(...inputs: ClassValue[]) {\r\n return twMerge(clsx(inputs));\r\n}\r\n"
965
1358
  },
966
1359
  "messages": {
@@ -976,6 +1369,9 @@
976
1369
  "dashboard_intro": "Welcome back. Here's an overview of your account.",
977
1370
  "dashboard_signed_in_as": "Signed in as {email}",
978
1371
  "dashboard_logout": "Log out",
1372
+ "home_page": "Home",
1373
+ "home_title": "Welcome to TanStack Start",
1374
+ "home_intro": "Edit src/components/screens/home/home-screen.tsx to get started.",
979
1375
  "login_page": "Log in",
980
1376
  "login_title": "Log in",
981
1377
  "login_intro": "Sign in to continue to your account.",
@@ -986,8 +1382,27 @@
986
1382
  "login_submit": "Log in",
987
1383
  "login_submitting": "Logging in...",
988
1384
  "login_success": "Welcome! You have successfully logged in.",
1385
+ "signup_page": "Sign up",
1386
+ "signup_title": "Create an account",
1387
+ "signup_intro": "Sign up to get started.",
1388
+ "signup_name_label": "Full name",
1389
+ "signup_name_placeholder": "Enter your full name",
1390
+ "signup_email_label": "Email",
1391
+ "signup_email_placeholder": "Enter your email",
1392
+ "signup_password_label": "Password",
1393
+ "signup_password_placeholder": "Create a password",
1394
+ "signup_confirm_password_label": "Confirm password",
1395
+ "signup_confirm_password_placeholder": "Re-enter your password",
1396
+ "signup_submit": "Create account",
1397
+ "signup_submitting": "Creating account...",
1398
+ "signup_success": "Welcome! Your account has been created.",
1399
+ "status_title_404": "Page not found",
989
1400
  "status_error_404": "The requested resource was not found.",
1401
+ "status_title_500": "Something went wrong",
990
1402
  "status_error_500": "Something went wrong. Please try again.",
1403
+ "status_go_home": "Go to homepage",
1404
+ "status_go_back": "Go back",
1405
+ "status_retry": "Try again",
991
1406
  "ui_select_placeholder": "Select an option",
992
1407
  "ui_search_placeholder": "Search...",
993
1408
  "ui_no_results": "No results found.",
@@ -1002,13 +1417,19 @@
1002
1417
  "ui_previous_page": "Go to previous page",
1003
1418
  "ui_next_page": "Go to next page",
1004
1419
  "ui_toggle_theme": "Toggle theme",
1420
+ "ui_switch_language": "Switch language",
1421
+ "ui_language_en": "English",
1422
+ "ui_language_ar": "Arabic",
1005
1423
  "ui_dropzone_prompt": "Drag and drop a file here, or browse",
1006
1424
  "ui_browse_files": "Browse files",
1007
1425
  "ui_remove_file": "Remove file",
1008
1426
  "ui_max_size": "Maximum size {size}",
1009
1427
  "validation_email_required": "Email field is required",
1010
1428
  "validation_email_invalid": "Email is invalid",
1011
- "validation_password_required": "Password field is required"
1429
+ "validation_password_required": "Password field is required",
1430
+ "validation_name_required": "Name field is required",
1431
+ "validation_confirm_password_required": "Please confirm your password",
1432
+ "validation_password_mismatch": "Passwords do not match"
1012
1433
  },
1013
1434
  "ar": {
1014
1435
  "about_page": "صفحة من نحن",
@@ -1022,6 +1443,9 @@
1022
1443
  "dashboard_intro": "مرحبًا بعودتك. إليك نظرة عامة على حسابك.",
1023
1444
  "dashboard_signed_in_as": "تم تسجيل الدخول باسم {email}",
1024
1445
  "dashboard_logout": "تسجيل الخروج",
1446
+ "home_page": "الرئيسية",
1447
+ "home_title": "مرحبًا بك في TanStack Start",
1448
+ "home_intro": "عدّل src/components/screens/home/home-screen.tsx للبدء.",
1025
1449
  "login_page": "تسجيل الدخول",
1026
1450
  "login_title": "تسجيل الدخول",
1027
1451
  "login_intro": "سجّل الدخول للمتابعة إلى حسابك.",
@@ -1032,8 +1456,27 @@
1032
1456
  "login_submit": "تسجيل الدخول",
1033
1457
  "login_submitting": "جارٍ تسجيل الدخول...",
1034
1458
  "login_success": "مرحبًا! لقد سجّلت الدخول بنجاح.",
1459
+ "signup_page": "إنشاء حساب",
1460
+ "signup_title": "إنشاء حساب",
1461
+ "signup_intro": "أنشئ حسابًا للبدء.",
1462
+ "signup_name_label": "الاسم الكامل",
1463
+ "signup_name_placeholder": "أدخل اسمك الكامل",
1464
+ "signup_email_label": "البريد الإلكتروني",
1465
+ "signup_email_placeholder": "أدخل بريدك الإلكتروني",
1466
+ "signup_password_label": "كلمة المرور",
1467
+ "signup_password_placeholder": "أنشئ كلمة مرور",
1468
+ "signup_confirm_password_label": "تأكيد كلمة المرور",
1469
+ "signup_confirm_password_placeholder": "أعد إدخال كلمة المرور",
1470
+ "signup_submit": "إنشاء حساب",
1471
+ "signup_submitting": "جارٍ إنشاء الحساب...",
1472
+ "signup_success": "مرحبًا! تم إنشاء حسابك بنجاح.",
1473
+ "status_title_404": "الصفحة غير موجودة",
1035
1474
  "status_error_404": "تعذّر العثور على المورد المطلوب.",
1475
+ "status_title_500": "حدث خطأ ما",
1036
1476
  "status_error_500": "حدث خطأ ما. يُرجى المحاولة مرة أخرى.",
1477
+ "status_go_home": "الذهاب إلى الصفحة الرئيسية",
1478
+ "status_go_back": "رجوع",
1479
+ "status_retry": "حاول مرة أخرى",
1037
1480
  "ui_select_placeholder": "اختر خيارًا",
1038
1481
  "ui_search_placeholder": "بحث...",
1039
1482
  "ui_no_results": "لا توجد نتائج.",
@@ -1048,13 +1491,19 @@
1048
1491
  "ui_previous_page": "الانتقال إلى الصفحة السابقة",
1049
1492
  "ui_next_page": "الانتقال إلى الصفحة التالية",
1050
1493
  "ui_toggle_theme": "تبديل المظهر",
1494
+ "ui_switch_language": "تغيير اللغة",
1495
+ "ui_language_en": "الإنجليزية",
1496
+ "ui_language_ar": "العربية",
1051
1497
  "ui_dropzone_prompt": "اسحب الملف وأفلته هنا، أو تصفح",
1052
1498
  "ui_browse_files": "تصفح الملفات",
1053
1499
  "ui_remove_file": "إزالة الملف",
1054
1500
  "ui_max_size": "الحد الأقصى للحجم {size}",
1055
1501
  "validation_email_required": "حقل البريد الإلكتروني مطلوب",
1056
1502
  "validation_email_invalid": "البريد الإلكتروني غير صالح",
1057
- "validation_password_required": "حقل كلمة المرور مطلوب"
1503
+ "validation_password_required": "حقل كلمة المرور مطلوب",
1504
+ "validation_name_required": "حقل الاسم مطلوب",
1505
+ "validation_confirm_password_required": "يرجى تأكيد كلمة المرور",
1506
+ "validation_password_mismatch": "كلمتا المرور غير متطابقتين"
1058
1507
  }
1059
1508
  }
1060
1509
  }