@stackcraftmx/calcita 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +7 -0
  2. package/dist/DataTable/AuditTrailDialog.js +4 -1
  3. package/dist/DataTable/AuditTrailDialog.js.map +1 -1
  4. package/dist/DataTable/AuditTrailTable.js +4 -1
  5. package/dist/DataTable/AuditTrailTable.js.map +1 -1
  6. package/dist/DataTable/ColumnFilters/HeaderFilterPopover.js +3 -1
  7. package/dist/DataTable/ColumnFilters/HeaderFilterPopover.js.map +1 -1
  8. package/dist/DataTable/ColumnFilters/MultiSelectColumnFilter.js +3 -1
  9. package/dist/DataTable/ColumnFilters/MultiSelectColumnFilter.js.map +1 -1
  10. package/dist/DataTable/ColumnFilters/SortableFilterHeader.js +4 -1
  11. package/dist/DataTable/ColumnFilters/SortableFilterHeader.js.map +1 -1
  12. package/dist/DataTable/ColumnFilters/TextColumnFilter.js +3 -1
  13. package/dist/DataTable/ColumnFilters/TextColumnFilter.js.map +1 -1
  14. package/dist/DataTable/EmptyState.js +3 -1
  15. package/dist/DataTable/EmptyState.js.map +1 -1
  16. package/dist/DataTable/FilterChips.js +3 -1
  17. package/dist/DataTable/FilterChips.js.map +1 -1
  18. package/dist/DataTable/GoToPage.js +3 -1
  19. package/dist/DataTable/GoToPage.js.map +1 -1
  20. package/dist/DataTable/ServerTable.js +4 -1
  21. package/dist/DataTable/ServerTable.js.map +1 -1
  22. package/dist/DataTable/columns.js +3 -1
  23. package/dist/DataTable/columns.js.map +1 -1
  24. package/dist/access/Can.js +3 -1
  25. package/dist/access/Can.js.map +1 -1
  26. package/dist/access/PermissionMatrix.js +3 -1
  27. package/dist/access/PermissionMatrix.js.map +1 -1
  28. package/dist/access/RequireAccess.js +3 -1
  29. package/dist/access/RequireAccess.js.map +1 -1
  30. package/dist/account/AccountProvider.js +3 -1
  31. package/dist/account/AccountProvider.js.map +1 -1
  32. package/dist/account/AccountSecuritySection.js +3 -1
  33. package/dist/account/AccountSecuritySection.js.map +1 -1
  34. package/dist/account/DemoTotpCodeButton.js +3 -1
  35. package/dist/account/DemoTotpCodeButton.js.map +1 -1
  36. package/dist/account/ForgotPasswordScreen.js +3 -1
  37. package/dist/account/ForgotPasswordScreen.js.map +1 -1
  38. package/dist/account/ProfileScreen.js +4 -1
  39. package/dist/account/ProfileScreen.js.map +1 -1
  40. package/dist/account/ResetPasswordScreen.js +4 -1
  41. package/dist/account/ResetPasswordScreen.js.map +1 -1
  42. package/dist/account/SignInScreen.js +3 -1
  43. package/dist/account/SignInScreen.js.map +1 -1
  44. package/dist/appShell/AccountMenu.js +3 -1
  45. package/dist/appShell/AccountMenu.js.map +1 -1
  46. package/dist/appShell/AppLayout.js +4 -1
  47. package/dist/appShell/AppLayout.js.map +1 -1
  48. package/dist/appShell/AppearanceSettings.js +4 -1
  49. package/dist/appShell/AppearanceSettings.js.map +1 -1
  50. package/dist/appShell/AuthLayout.js +3 -1
  51. package/dist/appShell/AuthLayout.js.map +1 -1
  52. package/dist/appShell/BarActions.js +3 -1
  53. package/dist/appShell/BarActions.js.map +1 -1
  54. package/dist/appShell/BarSearch.js +3 -1
  55. package/dist/appShell/BarSearch.js.map +1 -1
  56. package/dist/appShell/HeaderTools.js +4 -1
  57. package/dist/appShell/HeaderTools.js.map +1 -1
  58. package/dist/appShell/MenuLinks.js +3 -1
  59. package/dist/appShell/MenuLinks.js.map +1 -1
  60. package/dist/appShell/MobileTabBar.js +3 -1
  61. package/dist/appShell/MobileTabBar.js.map +1 -1
  62. package/dist/appShell/SettingsMenu.js +3 -1
  63. package/dist/appShell/SettingsMenu.js.map +1 -1
  64. package/dist/appShell/SideMenu.js +4 -1
  65. package/dist/appShell/SideMenu.js.map +1 -1
  66. package/dist/appShell/SideMenuEntry.js +3 -1
  67. package/dist/appShell/SideMenuEntry.js.map +1 -1
  68. package/dist/appShell/SubmenuActionsMenu.js +3 -1
  69. package/dist/appShell/SubmenuActionsMenu.js.map +1 -1
  70. package/dist/appShell/TabBarEditor.js +3 -1
  71. package/dist/appShell/TabBarEditor.js.map +1 -1
  72. package/dist/appShell/TopMenu.js +3 -1
  73. package/dist/appShell/TopMenu.js.map +1 -1
  74. package/dist/appShell/Wordmark.js +3 -1
  75. package/dist/appShell/Wordmark.js.map +1 -1
  76. package/dist/appShell/pageTitle.js +3 -1
  77. package/dist/appShell/pageTitle.js.map +1 -1
  78. package/dist/auth/PasswordChecklist.js +3 -1
  79. package/dist/auth/PasswordChecklist.js.map +1 -1
  80. package/dist/calendar/EventCalendar.js +4 -1
  81. package/dist/calendar/EventCalendar.js.map +1 -1
  82. package/dist/calendar/EventDetails.js +3 -1
  83. package/dist/calendar/EventDetails.js.map +1 -1
  84. package/dist/datePickers/DateInput.js +4 -1
  85. package/dist/datePickers/DateInput.js.map +1 -1
  86. package/dist/downloads/DownloadManager.js +3 -1
  87. package/dist/downloads/DownloadManager.js.map +1 -1
  88. package/dist/feedback/ConfirmDialog.js +4 -1
  89. package/dist/feedback/ConfirmDialog.js.map +1 -1
  90. package/dist/feedback/ModalWaiting.js +3 -1
  91. package/dist/feedback/ModalWaiting.js.map +1 -1
  92. package/dist/feedback/NewBuildNotice.js +3 -1
  93. package/dist/feedback/NewBuildNotice.js.map +1 -1
  94. package/dist/filepond/FileDropzone.js +4 -1
  95. package/dist/filepond/FileDropzone.js.map +1 -1
  96. package/dist/formatting.js +8 -5
  97. package/dist/formatting.js.map +1 -1
  98. package/dist/forms/FormProblemAlert.js +3 -1
  99. package/dist/forms/FormProblemAlert.js.map +1 -1
  100. package/dist/forms/FormServerProblem.js +3 -1
  101. package/dist/forms/FormServerProblem.js.map +1 -1
  102. package/dist/forms/FormWizard.js +3 -1
  103. package/dist/forms/FormWizard.js.map +1 -1
  104. package/dist/forms/auth/ChangePasswordForm.js +3 -1
  105. package/dist/forms/auth/ChangePasswordForm.js.map +1 -1
  106. package/dist/forms/auth/ForgotPasswordForm.js +3 -1
  107. package/dist/forms/auth/ForgotPasswordForm.js.map +1 -1
  108. package/dist/forms/auth/SignInForm.js +3 -1
  109. package/dist/forms/auth/SignInForm.js.map +1 -1
  110. package/dist/forms/security/SecondStepForm.js +3 -1
  111. package/dist/forms/security/SecondStepForm.js.map +1 -1
  112. package/dist/forms/security/TwoStepVerificationCard.js +3 -1
  113. package/dist/forms/security/TwoStepVerificationCard.js.map +1 -1
  114. package/dist/hookForm/FormProblemAlert.js +3 -1
  115. package/dist/hookForm/FormProblemAlert.js.map +1 -1
  116. package/dist/hookForm/FormWizard.js +3 -1
  117. package/dist/hookForm/FormWizard.js.map +1 -1
  118. package/dist/inbox/InboxLayout.js +4 -1
  119. package/dist/inbox/InboxLayout.js.map +1 -1
  120. package/dist/inbox/MessageList.js +4 -1
  121. package/dist/inbox/MessageList.js.map +1 -1
  122. package/dist/inbox/MessageView.js +3 -1
  123. package/dist/inbox/MessageView.js.map +1 -1
  124. package/dist/index.js +3 -1
  125. package/dist/inputs/CatalogInput.js +4 -1
  126. package/dist/inputs/CatalogInput.js.map +1 -1
  127. package/dist/inputs/ColorInput.js +4 -1
  128. package/dist/inputs/ColorInput.js.map +1 -1
  129. package/dist/inputs/FieldFrame.js +3 -1
  130. package/dist/inputs/FieldFrame.js.map +1 -1
  131. package/dist/inputs/FormSection.js +5 -2
  132. package/dist/inputs/FormSection.js.map +1 -1
  133. package/dist/inputs/NumberInput.js +4 -1
  134. package/dist/inputs/NumberInput.js.map +1 -1
  135. package/dist/inputs/SelectInput.js +4 -1
  136. package/dist/inputs/SelectInput.js.map +1 -1
  137. package/dist/inputs/WizardLayout.js +4 -1
  138. package/dist/inputs/WizardLayout.js.map +1 -1
  139. package/dist/kanban/KanbanBoard.js +4 -1
  140. package/dist/kanban/KanbanBoard.js.map +1 -1
  141. package/dist/maps/MapView.js +4 -1
  142. package/dist/maps/MapView.js.map +1 -1
  143. package/dist/pwa/InstallAppSetting.js +3 -1
  144. package/dist/pwa/InstallAppSetting.js.map +1 -1
  145. package/dist/pwa/InstallInstructionsDialog.js +3 -1
  146. package/dist/pwa/InstallInstructionsDialog.js.map +1 -1
  147. package/dist/pwa/InstallPromptBanner.js +3 -1
  148. package/dist/pwa/InstallPromptBanner.js.map +1 -1
  149. package/dist/security/ActivityLog.js +3 -1
  150. package/dist/security/ActivityLog.js.map +1 -1
  151. package/dist/security/RecoveryCodesDialog.js +3 -1
  152. package/dist/security/RecoveryCodesDialog.js.map +1 -1
  153. package/dist/security/SessionsCard.js +3 -1
  154. package/dist/security/SessionsCard.js.map +1 -1
  155. package/dist/theme/createToolkitTheme.js +4 -1
  156. package/dist/theme/createToolkitTheme.js.map +1 -1
  157. package/dist/toolkit/ComponentTexts.js +3 -1
  158. package/dist/toolkit/ComponentTexts.js.map +1 -1
  159. package/dist/toolkit/ToolkitProvider.js +24 -3
  160. package/dist/toolkit/ToolkitProvider.js.map +1 -1
  161. package/dist/toolkit/toolkitContext.js.map +1 -1
  162. package/dist/toolkit/toolkitSettings.js +9 -0
  163. package/dist/toolkit/toolkitSettings.js.map +1 -1
  164. package/dist/toolkit/unknownProps.js +33 -0
  165. package/dist/toolkit/unknownProps.js.map +1 -0
  166. package/dist/types/DataTable/AuditTrailTable.d.ts +1 -1
  167. package/dist/types/DataTable/ColumnFilters/TextColumnFilter.d.ts +1 -1
  168. package/dist/types/DataTable/columns.d.ts +3 -1
  169. package/dist/types/access/Can.d.ts +1 -1
  170. package/dist/types/access/RequireAccess.d.ts +1 -1
  171. package/dist/types/account/AccountProvider.d.ts +1 -1
  172. package/dist/types/account/ForgotPasswordScreen.d.ts +1 -1
  173. package/dist/types/account/ResetPasswordScreen.d.ts +1 -1
  174. package/dist/types/appShell/MenuLinks.d.ts +1 -1
  175. package/dist/types/appShell/pageTitle.d.ts +1 -1
  176. package/dist/types/datePickers/DateInput.d.ts +1 -1
  177. package/dist/types/downloads/DownloadManager.d.ts +1 -1
  178. package/dist/types/formatting.d.ts +2 -1
  179. package/dist/types/forms/FormProblemAlert.d.ts +1 -1
  180. package/dist/types/forms/FormServerProblem.d.ts +1 -1
  181. package/dist/types/forms/FormWizard.d.ts +1 -1
  182. package/dist/types/forms/auth/ChangePasswordForm.d.ts +1 -1
  183. package/dist/types/forms/auth/ForgotPasswordForm.d.ts +1 -1
  184. package/dist/types/hookForm/FormProblemAlert.d.ts +1 -1
  185. package/dist/types/hookForm/FormWizard.d.ts +1 -1
  186. package/dist/types/index.d.ts +1 -1
  187. package/dist/types/inputs/CatalogInput.d.ts +1 -1
  188. package/dist/types/inputs/FieldFrame.d.ts +1 -1
  189. package/dist/types/inputs/NumberInput.d.ts +1 -1
  190. package/dist/types/pwa/InstallAppSetting.d.ts +1 -1
  191. package/dist/types/theme/createToolkitTheme.d.ts +1 -1
  192. package/dist/types/toolkit/ComponentTexts.d.ts +1 -1
  193. package/dist/types/toolkit/ToolkitProvider.d.ts +4 -1
  194. package/dist/types/toolkit/toolkitContext.d.ts +2 -1
  195. package/dist/types/toolkit/toolkitSettings.d.ts +13 -2
  196. package/dist/types/toolkit/unknownProps.d.ts +23 -0
  197. package/dist/types/ui/actions/CopyButton.d.ts +1 -1
  198. package/dist/types/ui/actions/SecretField.d.ts +1 -1
  199. package/dist/types/ui/labels/DictionaryText.d.ts +1 -1
  200. package/dist/types/ui/layout/FullPageLoader.d.ts +1 -1
  201. package/dist/types/ui/metrics/CalendarHeatmap.d.ts +9 -8
  202. package/dist/types/ui/metrics/chartParts.d.ts +3 -3
  203. package/dist/types/ui/metrics/heatmapScale.d.ts +23 -0
  204. package/dist/types/ui/navigation/DocumentTitle.d.ts +1 -1
  205. package/dist/ui/actions/ActionsMenu.js +3 -1
  206. package/dist/ui/actions/ActionsMenu.js.map +1 -1
  207. package/dist/ui/actions/CopyButton.js +3 -1
  208. package/dist/ui/actions/CopyButton.js.map +1 -1
  209. package/dist/ui/actions/DownloadMenu.js +4 -1
  210. package/dist/ui/actions/DownloadMenu.js.map +1 -1
  211. package/dist/ui/actions/SecretField.js +3 -1
  212. package/dist/ui/actions/SecretField.js.map +1 -1
  213. package/dist/ui/actions/SelectionActions.js +3 -1
  214. package/dist/ui/actions/SelectionActions.js.map +1 -1
  215. package/dist/ui/filters/DateRangeFilter.js +3 -1
  216. package/dist/ui/filters/DateRangeFilter.js.map +1 -1
  217. package/dist/ui/filters/PeriodRangeSelector.js +3 -1
  218. package/dist/ui/filters/PeriodRangeSelector.js.map +1 -1
  219. package/dist/ui/filters/QuickFilters.js +3 -1
  220. package/dist/ui/filters/QuickFilters.js.map +1 -1
  221. package/dist/ui/filters/SegmentedControl.js +3 -1
  222. package/dist/ui/filters/SegmentedControl.js.map +1 -1
  223. package/dist/ui/labels/DictionaryText.js +3 -1
  224. package/dist/ui/labels/DictionaryText.js.map +1 -1
  225. package/dist/ui/labels/StatusChip.js +3 -1
  226. package/dist/ui/labels/StatusChip.js.map +1 -1
  227. package/dist/ui/layout/CollapsibleSection.js +3 -1
  228. package/dist/ui/layout/CollapsibleSection.js.map +1 -1
  229. package/dist/ui/layout/ContentDialog.js +3 -1
  230. package/dist/ui/layout/ContentDialog.js.map +1 -1
  231. package/dist/ui/layout/DefinitionList.js +3 -1
  232. package/dist/ui/layout/DefinitionList.js.map +1 -1
  233. package/dist/ui/layout/DetailHeader.js +3 -1
  234. package/dist/ui/layout/DetailHeader.js.map +1 -1
  235. package/dist/ui/layout/ErrorPage.js +4 -1
  236. package/dist/ui/layout/ErrorPage.js.map +1 -1
  237. package/dist/ui/layout/FullPageLoader.js +3 -1
  238. package/dist/ui/layout/FullPageLoader.js.map +1 -1
  239. package/dist/ui/layout/PageHeader.js +3 -1
  240. package/dist/ui/layout/PageHeader.js.map +1 -1
  241. package/dist/ui/layout/PageHeading.js +3 -1
  242. package/dist/ui/layout/PageHeading.js.map +1 -1
  243. package/dist/ui/layout/ProfileHeader.js +3 -1
  244. package/dist/ui/layout/ProfileHeader.js.map +1 -1
  245. package/dist/ui/layout/SectionCard.js +3 -1
  246. package/dist/ui/layout/SectionCard.js.map +1 -1
  247. package/dist/ui/metrics/BarList.js +3 -1
  248. package/dist/ui/metrics/BarList.js.map +1 -1
  249. package/dist/ui/metrics/CalendarHeatmap.js +13 -4
  250. package/dist/ui/metrics/CalendarHeatmap.js.map +1 -1
  251. package/dist/ui/metrics/ChartTooltip.js.map +1 -1
  252. package/dist/ui/metrics/ColumnChart.js +4 -1
  253. package/dist/ui/metrics/ColumnChart.js.map +1 -1
  254. package/dist/ui/metrics/LineChart.js +4 -1
  255. package/dist/ui/metrics/LineChart.js.map +1 -1
  256. package/dist/ui/metrics/MetricCard.js +4 -1
  257. package/dist/ui/metrics/MetricCard.js.map +1 -1
  258. package/dist/ui/metrics/TableToggle.js +3 -1
  259. package/dist/ui/metrics/TableToggle.js.map +1 -1
  260. package/dist/ui/metrics/chartParts.js +7 -3
  261. package/dist/ui/metrics/chartParts.js.map +1 -1
  262. package/dist/ui/metrics/heatmapScale.js +15 -1
  263. package/dist/ui/metrics/heatmapScale.js.map +1 -1
  264. package/dist/ui/navigation/ColorSchemeToggle.js +3 -1
  265. package/dist/ui/navigation/ColorSchemeToggle.js.map +1 -1
  266. package/dist/ui/navigation/DocumentTitle.js +3 -1
  267. package/dist/ui/navigation/DocumentTitle.js.map +1 -1
  268. package/dist/ui/navigation/FontSizeToggle.js +3 -1
  269. package/dist/ui/navigation/FontSizeToggle.js.map +1 -1
  270. package/dist/ui/navigation/LanguageToggle.js +3 -1
  271. package/dist/ui/navigation/LanguageToggle.js.map +1 -1
  272. package/dist/ui/navigation/PageBreadcrumbs.js +3 -1
  273. package/dist/ui/navigation/PageBreadcrumbs.js.map +1 -1
  274. package/dist/ui/timeline/GroupedTimeline.js +3 -1
  275. package/dist/ui/timeline/GroupedTimeline.js.map +1 -1
  276. package/dist/upload/FileDropzone.js +4 -1
  277. package/dist/upload/FileDropzone.js.map +1 -1
  278. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"FormWizard.js","sources":["../../src/hookForm/FormWizard.jsx"],"sourcesContent":["// Formulario por pasos (wizard) con react-hook-form: el mismo del de formik (@stackcraftmx/calcita/forms), con las mismas\n// props y el mismo comportamiento:\n// - «Siguiente» revisa SOLO los campos del paso (trigger con sus nombres) y no avanza si alguno tiene error;\n// - «Omitir este paso» (solo en los opcionales) regresa sus campos a su valor de inicio (resetField): omitir es no dar ese\n// dato, no dejar lo que se escribió a medias;\n// - el último paso envía TODO el formulario (el esquema de yup completo vale para todos los pasos);\n// - si el envío falla por un campo de un paso anterior (yup o el 422 del servidor con applyServerProblem), regresa solo a\n// ese paso: un error que no se ve no se puede corregir.\n//\n// Va DENTRO de <FormProvider> y del <form> con su handleSubmit (usa useFormContext):\n// const form = useForm({resolver: yupResolver(schema), defaultValues});\n// <FormProvider {...form}>\n// <form noValidate onSubmit={form.handleSubmit(send)}>\n// <FormWizard submitLabel=\"Enviar solicitud\" steps={[\n// {key: 'datos', label: 'Sus datos', fields: ['name', 'email'], content: <>…campos…</>},\n// {key: 'aval', label: 'Aval', optional: true, fields: ['guarantor'], content: <>…</>},\n// ]} />\n// </form>\n// </FormProvider>\n//\n// Lo que se ve vive en WizardLayout (inputs/). Los textos se cambian con `texts`: {wizard: {next: 'Continuar'}}.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useRef, useState} from 'react';\nimport {useFormContext} from 'react-hook-form';\n\nimport FormProblemAlert from '@/hookForm/FormProblemAlert.jsx';\nimport SubmitButton from '@/hookForm/SubmitButton.jsx';\nimport WizardLayout from '@/inputs/WizardLayout.jsx';\n\n/**\n * @param {object} props\n * @param {Object[]} props.steps en orden\n * @param {string} props.steps[].key\n * @param {string} props.steps[].label ya traducido\n * @param {string[]} [props.steps[].fields] los campos del paso (los que revisa «Siguiente»)\n * @param {boolean} [props.steps[].optional] se puede omitir\n * @param {import('react').ReactNode} props.steps[].content los campos del paso\n * @param {string} props.submitLabel ya traducido: el botón del último paso\n * @param {object} [props.texts] los textos que cambian, con la forma de `common`: {wizard: {next, back, skip, optional, skipped, stepOf}}\n */\nexport default function FormWizard({steps, submitLabel, texts}) {\n const {trigger, resetField, getFieldState, formState} = useFormContext();\n const {isSubmitting, submitCount} = formState;\n const [activeIndex, setActiveIndex] = useState(0);\n const [skippedKeys, setSkippedKeys] = useState(() => new Set());\n const wasSubmitting = useRef(false);\n const activeStep = steps[activeIndex];\n\n // al terminar un envío fallido, regresa al paso con el primer error (solo en ese momento: escribir en otro paso no\n // debe jalar a la persona de regreso). OJO: getFieldState necesita formState para leer los errores de ahora\n useEffect(() => {\n if (wasSubmitting.current && !isSubmitting) {\n const errorStepIndex = steps.findIndex((step) => (step.fields ?? []).some((field) => getFieldState(field, formState).invalid));\n if (errorStepIndex >= 0) {\n setActiveIndex(errorStepIndex);\n }\n }\n wasSubmitting.current = isSubmitting;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isSubmitting, submitCount]);\n\n const goNext = async () => {\n const stepFields = activeStep.fields ?? [];\n // guard clause: el paso tiene errores; se queda para que se corrijan (trigger los marca y los pinta)\n const isStepValid = stepFields.length === 0 || await trigger(stepFields, {shouldFocus: true});\n if (!isStepValid) {\n return;\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys].filter((key) => key !== activeStep.key)));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n const skipStep = () => {\n for (const field of activeStep.fields ?? []) {\n resetField(field);\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys, activeStep.key]));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n return (\n <WizardLayout steps={steps} activeIndex={activeIndex} skippedKeys={skippedKeys} isSubmitting={isSubmitting}\n onBack={() => setActiveIndex((currentIndex) => currentIndex - 1)} onNext={goNext} onSkip={skipStep}\n submitButton={<SubmitButton>{submitLabel}</SubmitButton>} problemAlert={<FormProblemAlert />} texts={texts} />\n );\n}\n"],"names":[],"mappings":";;;;;;;AAyCA,SAAwB,WAAW,EAAC,OAAO,aAAa,SAAQ;AAC5D,QAAM,EAAC,SAAS,YAAY,eAAe,UAAA,IAAa,eAAA;AACxD,QAAM,EAAC,cAAc,YAAA,IAAe;AACpC,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,CAAC;AAChD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,MAAM,oBAAI,KAAK;AAC9D,QAAM,gBAAgB,OAAO,KAAK;AAClC,QAAM,aAAa,MAAM,WAAW;AAIpC,YAAU,MAAM;AACZ,QAAI,cAAc,WAAW,CAAC,cAAc;AACxC,YAAM,iBAAiB,MAAM,UAAU,CAAC,UAAU,KAAK,UAAU,CAAA,GAAI,KAAK,CAAC,UAAU,cAAc,OAAO,SAAS,EAAE,OAAO,CAAC;AAC7H,UAAI,kBAAkB,GAAG;AACrB,uBAAe,cAAc;AAAA,MACjC;AAAA,IACJ;AACA,kBAAc,UAAU;AAAA,EAE5B,GAAG,CAAC,cAAc,WAAW,CAAC;AAE9B,QAAM,SAAS,YAAY;AACvB,UAAM,aAAa,WAAW,UAAU,CAAA;AAExC,UAAM,cAAc,WAAW,WAAW,KAAK,MAAM,QAAQ,YAAY,EAAC,aAAa,MAAK;AAC5F,QAAI,CAAC,aAAa;AACd;AAAA,IACJ;AACA,mBAAe,CAAC,gBAAgB,IAAI,IAAI,CAAC,GAAG,WAAW,EAAE,OAAO,CAAC,QAAQ,QAAQ,WAAW,GAAG,CAAC,CAAC;AACjG,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA,QAAM,WAAW,MAAM;AACnB,eAAW,SAAS,WAAW,UAAU,CAAA,GAAI;AACzC,iBAAW,KAAK;AAAA,IACpB;AACA,mBAAe,CAAC,gBAAgB,oBAAI,IAAI,CAAC,GAAG,aAAa,WAAW,GAAG,CAAC,CAAC;AACzE,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAa;AAAA,MAAc;AAAA,MAA0B;AAAA,MAA0B;AAAA,MAClE,QAAQ,MAAM,eAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAQ,QAAQ;AAAA,MAC1F,cAAc,oBAAC,cAAA,EAAc,UAAA,YAAA,CAAY;AAAA,MAAiB,kCAAe,kBAAA,EAAiB;AAAA,MAAI;AAAA,IAAA;AAAA,EAAA;AAEpH;"}
1
+ {"version":3,"file":"FormWizard.js","sources":["../../src/hookForm/FormWizard.jsx"],"sourcesContent":["// Formulario por pasos (wizard) con react-hook-form: el mismo del de formik (@stackcraftmx/calcita/forms), con las mismas\n// props y el mismo comportamiento:\n// - «Siguiente» revisa SOLO los campos del paso (trigger con sus nombres) y no avanza si alguno tiene error;\n// - «Omitir este paso» (solo en los opcionales) regresa sus campos a su valor de inicio (resetField): omitir es no dar ese\n// dato, no dejar lo que se escribió a medias;\n// - el último paso envía TODO el formulario (el esquema de yup completo vale para todos los pasos);\n// - si el envío falla por un campo de un paso anterior (yup o el 422 del servidor con applyServerProblem), regresa solo a\n// ese paso: un error que no se ve no se puede corregir.\n//\n// Va DENTRO de <FormProvider> y del <form> con su handleSubmit (usa useFormContext):\n// const form = useForm({resolver: yupResolver(schema), defaultValues});\n// <FormProvider {...form}>\n// <form noValidate onSubmit={form.handleSubmit(send)}>\n// <FormWizard submitLabel=\"Enviar solicitud\" steps={[\n// {key: 'datos', label: 'Sus datos', fields: ['name', 'email'], content: <>…campos…</>},\n// {key: 'aval', label: 'Aval', optional: true, fields: ['guarantor'], content: <>…</>},\n// ]} />\n// </form>\n// </FormProvider>\n//\n// Lo que se ve vive en WizardLayout (inputs/). Los textos se cambian con `texts`: {wizard: {next: 'Continuar'}}.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useRef, useState} from 'react';\nimport {useFormContext} from 'react-hook-form';\n\nimport FormProblemAlert from '@/hookForm/FormProblemAlert.jsx';\nimport SubmitButton from '@/hookForm/SubmitButton.jsx';\nimport WizardLayout from '@/inputs/WizardLayout.jsx';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n/**\n * @param {object} props\n * @param {Object[]} props.steps en orden\n * @param {string} props.steps[].key\n * @param {string} props.steps[].label ya traducido\n * @param {string[]} [props.steps[].fields] los campos del paso (los que revisa «Siguiente»)\n * @param {boolean} [props.steps[].optional] se puede omitir\n * @param {import('react').ReactNode} props.steps[].content los campos del paso\n * @param {string} props.submitLabel ya traducido: el botón del último paso\n * @param {object} [props.texts] los textos que cambian, con la forma de `common`: {wizard: {next, back, skip, optional, skipped, stepOf}}\n */\nexport default function FormWizard({steps, submitLabel, texts, ...unknownProps}) {\n warnUnknownProps('FormWizard', unknownProps);\n const {trigger, resetField, getFieldState, formState} = useFormContext();\n const {isSubmitting, submitCount} = formState;\n const [activeIndex, setActiveIndex] = useState(0);\n const [skippedKeys, setSkippedKeys] = useState(() => new Set());\n const wasSubmitting = useRef(false);\n const activeStep = steps[activeIndex];\n\n // al terminar un envío fallido, regresa al paso con el primer error (solo en ese momento: escribir en otro paso no\n // debe jalar a la persona de regreso). OJO: getFieldState necesita formState para leer los errores de ahora\n useEffect(() => {\n if (wasSubmitting.current && !isSubmitting) {\n const errorStepIndex = steps.findIndex((step) => (step.fields ?? []).some((field) => getFieldState(field, formState).invalid));\n if (errorStepIndex >= 0) {\n setActiveIndex(errorStepIndex);\n }\n }\n wasSubmitting.current = isSubmitting;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isSubmitting, submitCount]);\n\n const goNext = async () => {\n const stepFields = activeStep.fields ?? [];\n // guard clause: el paso tiene errores; se queda para que se corrijan (trigger los marca y los pinta)\n const isStepValid = stepFields.length === 0 || await trigger(stepFields, {shouldFocus: true});\n if (!isStepValid) {\n return;\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys].filter((key) => key !== activeStep.key)));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n const skipStep = () => {\n for (const field of activeStep.fields ?? []) {\n resetField(field);\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys, activeStep.key]));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n return (\n <WizardLayout steps={steps} activeIndex={activeIndex} skippedKeys={skippedKeys} isSubmitting={isSubmitting}\n onBack={() => setActiveIndex((currentIndex) => currentIndex - 1)} onNext={goNext} onSkip={skipStep}\n submitButton={<SubmitButton>{submitLabel}</SubmitButton>} problemAlert={<FormProblemAlert />} texts={texts} />\n );\n}\n"],"names":[],"mappings":";;;;;;;;AA0CA,SAAwB,WAAW,EAAC,OAAO,aAAa,OAAO,GAAG,gBAAe;AAC7E,mBAAiB,cAAc,YAAY;AAC3C,QAAM,EAAC,SAAS,YAAY,eAAe,UAAA,IAAa,eAAA;AACxD,QAAM,EAAC,cAAc,YAAA,IAAe;AACpC,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,CAAC;AAChD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,MAAM,oBAAI,KAAK;AAC9D,QAAM,gBAAgB,OAAO,KAAK;AAClC,QAAM,aAAa,MAAM,WAAW;AAIpC,YAAU,MAAM;AACZ,QAAI,cAAc,WAAW,CAAC,cAAc;AACxC,YAAM,iBAAiB,MAAM,UAAU,CAAC,UAAU,KAAK,UAAU,CAAA,GAAI,KAAK,CAAC,UAAU,cAAc,OAAO,SAAS,EAAE,OAAO,CAAC;AAC7H,UAAI,kBAAkB,GAAG;AACrB,uBAAe,cAAc;AAAA,MACjC;AAAA,IACJ;AACA,kBAAc,UAAU;AAAA,EAE5B,GAAG,CAAC,cAAc,WAAW,CAAC;AAE9B,QAAM,SAAS,YAAY;AACvB,UAAM,aAAa,WAAW,UAAU,CAAA;AAExC,UAAM,cAAc,WAAW,WAAW,KAAK,MAAM,QAAQ,YAAY,EAAC,aAAa,MAAK;AAC5F,QAAI,CAAC,aAAa;AACd;AAAA,IACJ;AACA,mBAAe,CAAC,gBAAgB,IAAI,IAAI,CAAC,GAAG,WAAW,EAAE,OAAO,CAAC,QAAQ,QAAQ,WAAW,GAAG,CAAC,CAAC;AACjG,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA,QAAM,WAAW,MAAM;AACnB,eAAW,SAAS,WAAW,UAAU,CAAA,GAAI;AACzC,iBAAW,KAAK;AAAA,IACpB;AACA,mBAAe,CAAC,gBAAgB,oBAAI,IAAI,CAAC,GAAG,aAAa,WAAW,GAAG,CAAC,CAAC;AACzE,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAa;AAAA,MAAc;AAAA,MAA0B;AAAA,MAA0B;AAAA,MAClE,QAAQ,MAAM,eAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAQ,QAAQ;AAAA,MAC1F,cAAc,oBAAC,cAAA,EAAc,UAAA,YAAA,CAAY;AAAA,MAAiB,kCAAe,kBAAA,EAAiB;AAAA,MAAI;AAAA,IAAA;AAAA,EAAA;AAEpH;"}
@@ -16,6 +16,7 @@ import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
16
16
  import { PHONE_MEDIA_QUERY, DESKTOP_MEDIA_QUERY } from "../appShell/shellSizes.js";
17
17
  import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
18
18
  import { piecePart, usePieceProps } from "../theme/themePieces.js";
19
+ import { warnUnknownProps } from "../toolkit/unknownProps.js";
19
20
  const THEME_NAME = "CalcitaInboxLayout";
20
21
  const InboxLabelDot = piecePart(THEME_NAME, "LabelDot", Box);
21
22
  const InboxFolder = piecePart(THEME_NAME, "Folder", ListItemButton);
@@ -63,8 +64,10 @@ function InboxLayout(props) {
63
64
  isDetailOpen = false,
64
65
  onCloseDetail,
65
66
  foldersHeader,
66
- texts
67
+ texts,
68
+ ...unknownProps
67
69
  } = usePieceProps(props, THEME_NAME);
70
+ warnUnknownProps(THEME_NAME, unknownProps);
68
71
  const { t } = useToolkitTranslation("inbox", texts);
69
72
  return /* @__PURE__ */ jsxs(InboxRoot, { ownSx: {
70
73
  display: "grid",
@@ -1 +1 @@
1
- {"version":3,"file":"InboxLayout.js","sources":["../../src/inbox/InboxLayout.jsx"],"sourcesContent":["// El acomodo de una bandeja tipo correo: las carpetas, la lista y la lectura. En escritorio, tres columnas; en el teléfono,\n// una a la vez: las carpetas como pestañas arriba de la lista y, al abrir un mensaje, la lectura con «Regresar».\n//\n// <InboxLayout folders={folders} selectedFolderId={folderId} onSelectFolder={setFolderId} toolbar={<Buscar y Redactar />}\n// list={<MessageList … />} detail={<MessageView … />} isDetailOpen={Boolean(selectedId)} onCloseDetail={() => setSelectedId(null)} />\n//\n// Debajo de las carpetas, las etiquetas del proyecto (labels) con su punto de color, como Gmail; se eligen igual que una\n// carpeta. foldersHeader pone algo arriba de las carpetas (un botón «Redactar») y texts cambia cualquier texto.\n//\n// Escritorio o teléfono se decide con CSS (no con useMediaQuery): así el servidor pinta lo mismo que el navegador.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaInboxLayout con defaultProps y\n// styleOverrides de sus partes: labelDot, folder, folderIcon, folderCount, root, folders, foldersHeader, labelsTitle,\n// listPane, phoneTabs, phoneTabBadge, listHeader, list, detail, backButton.\n//\n// Genérico del paquete: function (regla de la casa).\nimport Badge from '@mui/material/Badge';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport List from '@mui/material/List';\nimport ListItemButton from '@mui/material/ListItemButton';\nimport ListItemIcon from '@mui/material/ListItemIcon';\nimport ListItemText from '@mui/material/ListItemText';\nimport ListSubheader from '@mui/material/ListSubheader';\nimport Paper from '@mui/material/Paper';\nimport Tab from '@mui/material/Tab';\nimport Tabs from '@mui/material/Tabs';\nimport Typography from '@mui/material/Typography';\nimport ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';\n\nimport {DESKTOP_MEDIA_QUERY, PHONE_MEDIA_QUERY} from '@/appShell/shellSizes.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaInboxLayout)\nconst THEME_NAME = 'CalcitaInboxLayout';\nconst InboxLabelDot = piecePart(THEME_NAME, 'LabelDot', Box);\nconst InboxFolder = piecePart(THEME_NAME, 'Folder', ListItemButton);\nconst InboxFolderIcon = piecePart(THEME_NAME, 'FolderIcon', ListItemIcon);\nconst InboxFolderCount = piecePart(THEME_NAME, 'FolderCount', Typography);\nconst InboxRoot = piecePart(THEME_NAME, 'Root', Box);\nconst InboxFolders = piecePart(THEME_NAME, 'Folders', Paper);\nconst InboxFoldersHeader = piecePart(THEME_NAME, 'FoldersHeader', Box);\nconst InboxLabelsTitle = piecePart(THEME_NAME, 'LabelsTitle', ListSubheader);\nconst InboxListPane = piecePart(THEME_NAME, 'ListPane', Paper);\nconst InboxPhoneTabs = piecePart(THEME_NAME, 'PhoneTabs', Tabs);\nconst InboxPhoneTabBadge = piecePart(THEME_NAME, 'PhoneTabBadge', Badge);\nconst InboxListHeader = piecePart(THEME_NAME, 'ListHeader', Box);\nconst InboxList = piecePart(THEME_NAME, 'List', Box);\nconst InboxDetail = piecePart(THEME_NAME, 'Detail', Paper);\nconst InboxBackButton = piecePart(THEME_NAME, 'BackButton', Button);\n\nconst FOLDERS_WIDTH_PIXELS = 220;\nconst LIST_WIDTH_PIXELS = 380;\n// la bandeja ocupa el alto de la ventana (menos la barra y el encabezado de la página): cada columna desliza por dentro\nconst DESKTOP_HEIGHT = 'calc(100dvh - 220px)';\nconst DESKTOP_MIN_HEIGHT_PIXELS = 480;\nconst LABEL_DOT_PIXELS = 10;\n\n/** Un renglón de carpeta o etiqueta: su ícono (o su punto de color), su nombre y cuántos sin leer. */\nfunction FolderRow({folder, isSelected, onSelect, t}) {\n const {id, label, Icon, color, unreadCount} = folder;\n let icon = null;\n if (Icon) {\n icon = <Icon fontSize=\"small\" />;\n } else if (color) {\n icon = <InboxLabelDot aria-hidden=\"true\" ownSx={{width: LABEL_DOT_PIXELS, height: LABEL_DOT_PIXELS, borderRadius: '50%', bgcolor: `${color}.main`, ml: 0.5}} />;\n }\n return (\n <InboxFolder selected={isSelected} onClick={() => onSelect(id)} ownSx={{borderRadius: 2}}>\n {icon && <InboxFolderIcon ownSx={{minWidth: 36}}>{icon}</InboxFolderIcon>}\n <ListItemText primary={label} />\n {unreadCount > 0 && <InboxFolderCount variant=\"caption\" ownSx={{fontWeight: 700}} aria-label={t('unreadCount', {count: unreadCount})}>{unreadCount}</InboxFolderCount>}\n </InboxFolder>\n );\n}\n\n/**\n * @param {object} props\n * @param {Array<{id: string, label: string, Icon?: import('react').ElementType, unreadCount?: number}>} props.folders\n * @param {Array<{id: string, label: string, color?: string, unreadCount?: number}>} [props.labels] las etiquetas, debajo de las\n * carpetas con su punto de color (un color del tema); su id no debe repetir el de una carpeta\n * @param {string} props.selectedFolderId la carpeta o etiqueta elegida\n * @param {(folderId: string) => void} props.onSelectFolder\n * @param {import('react').ReactNode} [props.toolbar] arriba de la lista (buscar, redactar)\n * @param {import('react').ReactNode} props.list normalmente MessageList\n * @param {import('react').ReactNode} props.detail normalmente MessageView\n * @param {boolean} [props.isDetailOpen] en el teléfono, se ve la lectura en lugar de la lista\n * @param {() => void} [props.onCloseDetail] «Regresar» del teléfono\n * @param {import('react').ReactNode} [props.foldersHeader] arriba de las carpetas en escritorio (un botón «Redactar»)\n * @param {object} [props.texts] los textos que cambian, con la forma de `inbox` en los textos del paquete\n */\nexport default function InboxLayout(props) {\n // lo que el proyecto puso en su tema (components.CalcitaInboxLayout.defaultProps) y encima lo que llega aquí\n const {\n folders, labels = [], selectedFolderId, onSelectFolder, toolbar, list, detail, isDetailOpen = false, onCloseDetail, foldersHeader, texts,\n} = usePieceProps(props, THEME_NAME);\n const {t} = useToolkitTranslation('inbox', texts);\n\n return (\n <InboxRoot ownSx={{\n display: 'grid', gap: 2,\n [DESKTOP_MEDIA_QUERY]: {\n gridTemplateColumns: `${FOLDERS_WIDTH_PIXELS}px ${LIST_WIDTH_PIXELS}px minmax(0, 1fr)`, height: DESKTOP_HEIGHT, minHeight: DESKTOP_MIN_HEIGHT_PIXELS,\n },\n }}>\n {/* las carpetas: columna en escritorio */}\n <InboxFolders variant=\"outlined\" component=\"nav\" aria-label={t('folders')} ownSx={{p: 1, overflowY: 'auto', borderRadius: 3, [PHONE_MEDIA_QUERY]: {display: 'none'}}}>\n {foldersHeader && <InboxFoldersHeader ownSx={{p: 0.5, pb: 1}}>{foldersHeader}</InboxFoldersHeader>}\n <List disablePadding>\n {folders.map((folder) => (\n <FolderRow key={folder.id} folder={folder} isSelected={folder.id === selectedFolderId} onSelect={onSelectFolder} t={t} />\n ))}\n </List>\n {labels.length > 0 && (\n <List disablePadding aria-label={t('labels')}\n subheader={<InboxLabelsTitle disableSticky ownSx={{bgcolor: 'transparent', lineHeight: 2.5, mt: 1}}>{t('labels')}</InboxLabelsTitle>}>\n {labels.map((label) => (\n <FolderRow key={label.id} folder={label} isSelected={label.id === selectedFolderId} onSelect={onSelectFolder} t={t} />\n ))}\n </List>\n )}\n </InboxFolders>\n\n {/* la lista, con las carpetas como pestañas en el teléfono */}\n <InboxListPane variant=\"outlined\" ownSx={{display: 'flex', flexDirection: 'column', minHeight: 0, borderRadius: 3, overflow: 'hidden',\n [PHONE_MEDIA_QUERY]: isDetailOpen ? {display: 'none'} : {}}}>\n <InboxPhoneTabs value={selectedFolderId} onChange={(event, folderId) => onSelectFolder(folderId)} variant=\"scrollable\" aria-label={t('folders')}\n ownSx={{px: 1, [DESKTOP_MEDIA_QUERY]: {display: 'none'}}}>\n {[...folders, ...labels].map(({id, label, unreadCount}) => (\n <Tab key={id} value={id} label={unreadCount > 0 ? <InboxPhoneTabBadge color=\"primary\" badgeContent={unreadCount} ownSx={{pr: 1.5}}>{label}</InboxPhoneTabBadge> : label} />\n ))}\n </InboxPhoneTabs>\n {toolbar && <InboxListHeader ownSx={{p: 1.5, pb: 1}}>{toolbar}</InboxListHeader>}\n <InboxList ownSx={{flex: 1, minHeight: 0, overflowY: 'auto', px: 1, pb: 1}}>{list}</InboxList>\n </InboxListPane>\n\n {/* la lectura: en el teléfono solo con un mensaje abierto, con su «Regresar» */}\n <InboxDetail variant=\"outlined\" ownSx={{p: {xs: 2, md: 3}, overflowY: 'auto', borderRadius: 3, [PHONE_MEDIA_QUERY]: isDetailOpen ? {} : {display: 'none'}}}>\n {onCloseDetail && (\n <InboxBackButton startIcon={<ArrowBackRoundedIcon />} onClick={onCloseDetail} ownSx={{mb: 1, [DESKTOP_MEDIA_QUERY]: {display: 'none'}}}>{t('back')}</InboxBackButton>\n )}\n {detail}\n </InboxDetail>\n </InboxRoot>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"InboxLayout.js","sources":["../../src/inbox/InboxLayout.jsx"],"sourcesContent":["// El acomodo de una bandeja tipo correo: las carpetas, la lista y la lectura. En escritorio, tres columnas; en el teléfono,\n// una a la vez: las carpetas como pestañas arriba de la lista y, al abrir un mensaje, la lectura con «Regresar».\n//\n// <InboxLayout folders={folders} selectedFolderId={folderId} onSelectFolder={setFolderId} toolbar={<Buscar y Redactar />}\n// list={<MessageList … />} detail={<MessageView … />} isDetailOpen={Boolean(selectedId)} onCloseDetail={() => setSelectedId(null)} />\n//\n// Debajo de las carpetas, las etiquetas del proyecto (labels) con su punto de color, como Gmail; se eligen igual que una\n// carpeta. foldersHeader pone algo arriba de las carpetas (un botón «Redactar») y texts cambia cualquier texto.\n//\n// Escritorio o teléfono se decide con CSS (no con useMediaQuery): así el servidor pinta lo mismo que el navegador.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaInboxLayout con defaultProps y\n// styleOverrides de sus partes: labelDot, folder, folderIcon, folderCount, root, folders, foldersHeader, labelsTitle,\n// listPane, phoneTabs, phoneTabBadge, listHeader, list, detail, backButton.\n//\n// Genérico del paquete: function (regla de la casa).\nimport Badge from '@mui/material/Badge';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport List from '@mui/material/List';\nimport ListItemButton from '@mui/material/ListItemButton';\nimport ListItemIcon from '@mui/material/ListItemIcon';\nimport ListItemText from '@mui/material/ListItemText';\nimport ListSubheader from '@mui/material/ListSubheader';\nimport Paper from '@mui/material/Paper';\nimport Tab from '@mui/material/Tab';\nimport Tabs from '@mui/material/Tabs';\nimport Typography from '@mui/material/Typography';\nimport ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';\n\nimport {DESKTOP_MEDIA_QUERY, PHONE_MEDIA_QUERY} from '@/appShell/shellSizes.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaInboxLayout)\nconst THEME_NAME = 'CalcitaInboxLayout';\nconst InboxLabelDot = piecePart(THEME_NAME, 'LabelDot', Box);\nconst InboxFolder = piecePart(THEME_NAME, 'Folder', ListItemButton);\nconst InboxFolderIcon = piecePart(THEME_NAME, 'FolderIcon', ListItemIcon);\nconst InboxFolderCount = piecePart(THEME_NAME, 'FolderCount', Typography);\nconst InboxRoot = piecePart(THEME_NAME, 'Root', Box);\nconst InboxFolders = piecePart(THEME_NAME, 'Folders', Paper);\nconst InboxFoldersHeader = piecePart(THEME_NAME, 'FoldersHeader', Box);\nconst InboxLabelsTitle = piecePart(THEME_NAME, 'LabelsTitle', ListSubheader);\nconst InboxListPane = piecePart(THEME_NAME, 'ListPane', Paper);\nconst InboxPhoneTabs = piecePart(THEME_NAME, 'PhoneTabs', Tabs);\nconst InboxPhoneTabBadge = piecePart(THEME_NAME, 'PhoneTabBadge', Badge);\nconst InboxListHeader = piecePart(THEME_NAME, 'ListHeader', Box);\nconst InboxList = piecePart(THEME_NAME, 'List', Box);\nconst InboxDetail = piecePart(THEME_NAME, 'Detail', Paper);\nconst InboxBackButton = piecePart(THEME_NAME, 'BackButton', Button);\n\nconst FOLDERS_WIDTH_PIXELS = 220;\nconst LIST_WIDTH_PIXELS = 380;\n// la bandeja ocupa el alto de la ventana (menos la barra y el encabezado de la página): cada columna desliza por dentro\nconst DESKTOP_HEIGHT = 'calc(100dvh - 220px)';\nconst DESKTOP_MIN_HEIGHT_PIXELS = 480;\nconst LABEL_DOT_PIXELS = 10;\n\n/** Un renglón de carpeta o etiqueta: su ícono (o su punto de color), su nombre y cuántos sin leer. */\nfunction FolderRow({folder, isSelected, onSelect, t}) {\n const {id, label, Icon, color, unreadCount} = folder;\n let icon = null;\n if (Icon) {\n icon = <Icon fontSize=\"small\" />;\n } else if (color) {\n icon = <InboxLabelDot aria-hidden=\"true\" ownSx={{width: LABEL_DOT_PIXELS, height: LABEL_DOT_PIXELS, borderRadius: '50%', bgcolor: `${color}.main`, ml: 0.5}} />;\n }\n return (\n <InboxFolder selected={isSelected} onClick={() => onSelect(id)} ownSx={{borderRadius: 2}}>\n {icon && <InboxFolderIcon ownSx={{minWidth: 36}}>{icon}</InboxFolderIcon>}\n <ListItemText primary={label} />\n {unreadCount > 0 && <InboxFolderCount variant=\"caption\" ownSx={{fontWeight: 700}} aria-label={t('unreadCount', {count: unreadCount})}>{unreadCount}</InboxFolderCount>}\n </InboxFolder>\n );\n}\n\n/**\n * @param {object} props\n * @param {Array<{id: string, label: string, Icon?: import('react').ElementType, unreadCount?: number}>} props.folders\n * @param {Array<{id: string, label: string, color?: string, unreadCount?: number}>} [props.labels] las etiquetas, debajo de las\n * carpetas con su punto de color (un color del tema); su id no debe repetir el de una carpeta\n * @param {string} props.selectedFolderId la carpeta o etiqueta elegida\n * @param {(folderId: string) => void} props.onSelectFolder\n * @param {import('react').ReactNode} [props.toolbar] arriba de la lista (buscar, redactar)\n * @param {import('react').ReactNode} props.list normalmente MessageList\n * @param {import('react').ReactNode} props.detail normalmente MessageView\n * @param {boolean} [props.isDetailOpen] en el teléfono, se ve la lectura en lugar de la lista\n * @param {() => void} [props.onCloseDetail] «Regresar» del teléfono\n * @param {import('react').ReactNode} [props.foldersHeader] arriba de las carpetas en escritorio (un botón «Redactar»)\n * @param {object} [props.texts] los textos que cambian, con la forma de `inbox` en los textos del paquete\n */\nexport default function InboxLayout(props) {\n // lo que el proyecto puso en su tema (components.CalcitaInboxLayout.defaultProps) y encima lo que llega aquí\n const {\n folders, labels = [], selectedFolderId, onSelectFolder, toolbar, list, detail, isDetailOpen = false, onCloseDetail, foldersHeader, texts,\n ...unknownProps\n} = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('inbox', texts);\n\n return (\n <InboxRoot ownSx={{\n display: 'grid', gap: 2,\n [DESKTOP_MEDIA_QUERY]: {\n gridTemplateColumns: `${FOLDERS_WIDTH_PIXELS}px ${LIST_WIDTH_PIXELS}px minmax(0, 1fr)`, height: DESKTOP_HEIGHT, minHeight: DESKTOP_MIN_HEIGHT_PIXELS,\n },\n }}>\n {/* las carpetas: columna en escritorio */}\n <InboxFolders variant=\"outlined\" component=\"nav\" aria-label={t('folders')} ownSx={{p: 1, overflowY: 'auto', borderRadius: 3, [PHONE_MEDIA_QUERY]: {display: 'none'}}}>\n {foldersHeader && <InboxFoldersHeader ownSx={{p: 0.5, pb: 1}}>{foldersHeader}</InboxFoldersHeader>}\n <List disablePadding>\n {folders.map((folder) => (\n <FolderRow key={folder.id} folder={folder} isSelected={folder.id === selectedFolderId} onSelect={onSelectFolder} t={t} />\n ))}\n </List>\n {labels.length > 0 && (\n <List disablePadding aria-label={t('labels')}\n subheader={<InboxLabelsTitle disableSticky ownSx={{bgcolor: 'transparent', lineHeight: 2.5, mt: 1}}>{t('labels')}</InboxLabelsTitle>}>\n {labels.map((label) => (\n <FolderRow key={label.id} folder={label} isSelected={label.id === selectedFolderId} onSelect={onSelectFolder} t={t} />\n ))}\n </List>\n )}\n </InboxFolders>\n\n {/* la lista, con las carpetas como pestañas en el teléfono */}\n <InboxListPane variant=\"outlined\" ownSx={{display: 'flex', flexDirection: 'column', minHeight: 0, borderRadius: 3, overflow: 'hidden',\n [PHONE_MEDIA_QUERY]: isDetailOpen ? {display: 'none'} : {}}}>\n <InboxPhoneTabs value={selectedFolderId} onChange={(event, folderId) => onSelectFolder(folderId)} variant=\"scrollable\" aria-label={t('folders')}\n ownSx={{px: 1, [DESKTOP_MEDIA_QUERY]: {display: 'none'}}}>\n {[...folders, ...labels].map(({id, label, unreadCount}) => (\n <Tab key={id} value={id} label={unreadCount > 0 ? <InboxPhoneTabBadge color=\"primary\" badgeContent={unreadCount} ownSx={{pr: 1.5}}>{label}</InboxPhoneTabBadge> : label} />\n ))}\n </InboxPhoneTabs>\n {toolbar && <InboxListHeader ownSx={{p: 1.5, pb: 1}}>{toolbar}</InboxListHeader>}\n <InboxList ownSx={{flex: 1, minHeight: 0, overflowY: 'auto', px: 1, pb: 1}}>{list}</InboxList>\n </InboxListPane>\n\n {/* la lectura: en el teléfono solo con un mensaje abierto, con su «Regresar» */}\n <InboxDetail variant=\"outlined\" ownSx={{p: {xs: 2, md: 3}, overflowY: 'auto', borderRadius: 3, [PHONE_MEDIA_QUERY]: isDetailOpen ? {} : {display: 'none'}}}>\n {onCloseDetail && (\n <InboxBackButton startIcon={<ArrowBackRoundedIcon />} onClick={onCloseDetail} ownSx={{mb: 1, [DESKTOP_MEDIA_QUERY]: {display: 'none'}}}>{t('back')}</InboxBackButton>\n )}\n {detail}\n </InboxDetail>\n </InboxRoot>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -22,6 +22,7 @@ import { tagOf } from "../ui/labels/tagOf.js";
22
22
  import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
23
23
  import ActionsMenu from "../ui/actions/ActionsMenu.js";
24
24
  import { piecePart, usePieceProps } from "../theme/themePieces.js";
25
+ import { warnUnknownProps } from "../toolkit/unknownProps.js";
25
26
  const THEME_NAME = "CalcitaMessageList";
26
27
  const MessagesEmpty = piecePart(THEME_NAME, "Empty", Typography);
27
28
  const MessagesSelectedAvatar = piecePart(THEME_NAME, "SelectedAvatar", Avatar);
@@ -54,8 +55,10 @@ function MessageList(props) {
54
55
  messageActions,
55
56
  renderMessage,
56
57
  emptyText,
57
- texts
58
+ texts,
59
+ ...unknownProps
58
60
  } = usePieceProps(props, THEME_NAME);
61
+ warnUnknownProps(THEME_NAME, unknownProps);
59
62
  const { t } = useToolkitTranslation("inbox", texts);
60
63
  const formatters = useFormatters();
61
64
  if (messages.length === 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"MessageList.js","sources":["../../src/inbox/MessageList.jsx"],"sourcesContent":["// La lista de mensajes de una bandeja tipo correo: quién lo manda (y cuántos mensajes trae la conversación), el asunto con sus\n// etiquetas, el principio del texto, la fecha corta, la estrella y el clip si trae archivos. Los no leídos van en negritas con\n// su punto.\n//\n// Lo de un producto, todo hacia afuera (la lista no guarda nada ni llama a ningún servidor):\n// - elegir varios: con onSelectionChange el monograma de cada mensaje se vuelve su casilla (como Gmail en el teléfono) y la\n// pantalla pone las acciones de lo elegido (SelectionActions: archivar, borrar, marcar como leído…);\n// - «⋯» de cada mensaje con las acciones que dé la pantalla (messageActions);\n// - renderMessage para pintar lo de adentro de cada renglón a su manera, y texts para cualquier texto.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaMessageList con defaultProps y\n// styleOverrides de sus partes: empty, selectedAvatar, item, actions, starButton, itemButton, avatarSlot, avatarButton,\n// avatar, body, text, topRow, unreadDot, sender, threadCount, date, subject, labels.\n//\n// Genérico del paquete: function (regla de la casa).\nimport Avatar from '@mui/material/Avatar';\nimport Box from '@mui/material/Box';\nimport ButtonBase from '@mui/material/ButtonBase';\nimport Chip from '@mui/material/Chip';\nimport IconButton from '@mui/material/IconButton';\nimport List from '@mui/material/List';\nimport ListItem from '@mui/material/ListItem';\nimport ListItemAvatar from '@mui/material/ListItemAvatar';\nimport ListItemButton from '@mui/material/ListItemButton';\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\nimport AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';\nimport CheckRoundedIcon from '@mui/icons-material/CheckRounded';\nimport StarBorderRoundedIcon from '@mui/icons-material/StarBorderRounded';\nimport StarRoundedIcon from '@mui/icons-material/StarRounded';\n\nimport {initialsOf} from '@/appShell/menuItemText.js';\nimport {inboxDateOf} from '@/inbox/inboxDates.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {tagOf} from '@/ui/labels/tagOf.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport ActionsMenu from '@/ui/actions/ActionsMenu.jsx';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaMessageList)\nconst THEME_NAME = 'CalcitaMessageList';\nconst MessagesEmpty = piecePart(THEME_NAME, 'Empty', Typography);\nconst MessagesSelectedAvatar = piecePart(THEME_NAME, 'SelectedAvatar', Avatar);\nconst MessagesItem = piecePart(THEME_NAME, 'Item', ListItem);\nconst MessagesActions = piecePart(THEME_NAME, 'Actions', Stack);\nconst MessagesStarButton = piecePart(THEME_NAME, 'StarButton', IconButton);\nconst MessagesItemButton = piecePart(THEME_NAME, 'ItemButton', ListItemButton);\nconst MessagesAvatarSlot = piecePart(THEME_NAME, 'AvatarSlot', ListItemAvatar);\nconst MessagesAvatarButton = piecePart(THEME_NAME, 'AvatarButton', ButtonBase);\nconst MessagesAvatar = piecePart(THEME_NAME, 'Avatar', Avatar);\nconst MessagesBody = piecePart(THEME_NAME, 'Body', Box);\nconst MessagesText = piecePart(THEME_NAME, 'Text', Box);\nconst MessagesTopRow = piecePart(THEME_NAME, 'TopRow', Stack);\nconst MessagesUnreadDot = piecePart(THEME_NAME, 'UnreadDot', Box);\nconst MessagesSender = piecePart(THEME_NAME, 'Sender', Typography);\nconst MessagesThreadCount = piecePart(THEME_NAME, 'ThreadCount', Typography);\nconst MessagesDate = piecePart(THEME_NAME, 'Date', Typography);\nconst MessagesSubject = piecePart(THEME_NAME, 'Subject', Typography);\nconst MessagesLabels = piecePart(THEME_NAME, 'Labels', Stack);\n\nconst UNREAD_DOT_PIXELS = 8;\nconst AVATAR_PIXELS = 40;\n\n/**\n * @param {object} props\n * @param {Array<{id: string, from: {name: string, email: string}, subject: string, preview: string, receivedAt: string, isUnread?: boolean, isStarred?: boolean, hasAttachments?: boolean, threadCount?: number, labels?: Array<string|{label: string, color?: string}>} & Record<string, any>>} props.messages\n * cada mensaje puede traer sus campos propios (llegan a renderMessage y a messageActions tal cual). threadCount: cuántos\n * mensajes trae la conversación (con 2 o más se pinta «(3)»); labels: texto o {label, color} del tema\n * @param {string|null} [props.selectedId] el mensaje abierto\n * @param {(messageId: string) => void} props.onSelect abrir un mensaje\n * @param {(messageId: string) => void} [props.onToggleStar]\n * @param {string[]} [props.selectedIds] los elegidos para una acción en grupo\n * @param {(messageIds: string[]) => void} [props.onSelectionChange] con él, cada mensaje se puede elegir picando su monograma\n * @param {(message: any) => Array<{key: string, label: string, icon?: import('react').ReactNode, color?: string, disabled?: boolean, onClick: () => void}>} [props.messageActions]\n * las acciones del «⋯» de cada mensaje (archivar, posponer, mover…); sin ellas no hay «⋯»\n * @param {(message: any) => import('react').ReactNode} [props.renderMessage] lo de adentro de cada renglón, a su manera\n * @param {string} [props.emptyText] ya traducido; por omisión el del paquete\n * @param {object} [props.texts] los textos que cambian, con la forma de `inbox` en los textos del paquete\n */\nexport default function MessageList(props) {\n // lo que el proyecto puso en su tema (components.CalcitaMessageList.defaultProps) y encima lo que llega aquí\n const {\n messages, selectedId = null, onSelect, onToggleStar, selectedIds = [], onSelectionChange, messageActions, renderMessage, emptyText, texts,\n} = usePieceProps(props, THEME_NAME);\n const {t} = useToolkitTranslation('inbox', texts);\n const formatters = useFormatters();\n\n if (messages.length === 0) {\n return <MessagesEmpty variant=\"body2\" color=\"text.secondary\" ownSx={{p: 3, textAlign: 'center'}}>{emptyText ?? t('empty')}</MessagesEmpty>;\n }\n\n const toggleSelected = (messageId, isChecked) => {\n onSelectionChange(isChecked ? [...selectedIds, messageId] : selectedIds.filter((id) => id !== messageId));\n };\n\n return (\n <List disablePadding aria-label={t('listLabel')}>\n {messages.map((message) => {\n const weight = message.isUnread ? 700 : 400;\n const isChecked = selectedIds.includes(message.id);\n const avatar = <MessagesSelectedAvatar ownSx={{width: AVATAR_PIXELS, height: AVATAR_PIXELS}}>{initialsOf(message.from.name)}</MessagesSelectedAvatar>;\n return (\n <MessagesItem key={message.id} disablePadding secondaryAction={(onToggleStar || messageActions) && (\n <MessagesActions direction=\"row\" ownSx={{alignItems: 'center'}}>\n {onToggleStar && (\n <MessagesStarButton size=\"small\" onClick={() => onToggleStar(message.id)} aria-pressed={Boolean(message.isStarred)}\n aria-label={message.isStarred ? t('unstar') : t('star')} ownSx={{color: message.isStarred ? 'warning.main' : 'text.secondary'}}>\n {message.isStarred ? <StarRoundedIcon fontSize=\"small\" /> : <StarBorderRoundedIcon fontSize=\"small\" />}\n </MessagesStarButton>\n )}\n {messageActions && <ActionsMenu label={t('messageActions', {subject: message.subject})} actions={messageActions(message)} />}\n </MessagesActions>\n )} ownSx={{'& .MuiListItemSecondaryAction-root': {right: 4}}}>\n <MessagesItemButton selected={message.id === selectedId || isChecked} onClick={() => onSelect(message.id)} alignItems=\"flex-start\"\n ownSx={{borderRadius: 2, pr: onToggleStar && messageActions ? 10 : 6}}>\n <MessagesAvatarSlot ownSx={{mt: 0.5}}>\n {/* elegir: el monograma es la casilla (el clic no abre el mensaje). Un botón con papel de casilla y\n no el Checkbox de MUI: el tema le pinta a la casilla marcada un cuadro blanco detrás de su dibujo\n y la palomita salía como un cuadro */}\n {onSelectionChange ? (\n <MessagesAvatarButton role=\"checkbox\" aria-checked={isChecked} aria-label={t('select', {subject: message.subject})}\n ownSx={{borderRadius: '50%'}}\n onClick={(event) => {\n event.stopPropagation();\n toggleSelected(message.id, !isChecked);\n }}>\n {isChecked ? (\n <MessagesAvatar ownSx={{width: AVATAR_PIXELS, height: AVATAR_PIXELS, bgcolor: 'primary.main', color: 'primary.contrastText'}}>\n <CheckRoundedIcon />\n </MessagesAvatar>\n ) : avatar}\n </MessagesAvatarButton>\n ) : avatar}\n </MessagesAvatarSlot>\n {renderMessage ? <MessagesBody ownSx={{minWidth: 0, flex: 1}}>{renderMessage(message)}</MessagesBody> : (\n <MessagesText ownSx={{minWidth: 0, flex: 1}}>\n <MessagesTopRow direction=\"row\" spacing={1} ownSx={{alignItems: 'center'}}>\n {message.isUnread && (\n <MessagesUnreadDot component=\"span\" aria-label={t('unread')} ownSx={{width: UNREAD_DOT_PIXELS, height: UNREAD_DOT_PIXELS, borderRadius: '50%', bgcolor: 'primary.main', flexShrink: 0}} />\n )}\n <MessagesSender variant=\"body2\" noWrap ownSx={{fontWeight: weight, flex: 1}}>\n {message.from.name}\n {message.threadCount > 1 && (\n <MessagesThreadCount component=\"span\" variant=\"caption\" color=\"text.secondary\" ownSx={{ml: 0.5}} aria-label={t('threadCount', {count: message.threadCount})}>\n ({message.threadCount})\n </MessagesThreadCount>\n )}\n </MessagesSender>\n {message.hasAttachments && <AttachFileRoundedIcon fontSize=\"inherit\" color=\"action\" aria-label={t('attachments')} />}\n <MessagesDate variant=\"caption\" color=\"text.secondary\" ownSx={{flexShrink: 0, fontVariantNumeric: 'tabular-nums'}}>\n {inboxDateOf(message.receivedAt, new Date(), formatters)}\n </MessagesDate>\n </MessagesTopRow>\n <MessagesSubject variant=\"body2\" noWrap ownSx={{fontWeight: weight}}>{message.subject}</MessagesSubject>\n <Typography variant=\"body2\" color=\"text.secondary\" noWrap>{message.preview}</Typography>\n {message.labels?.length > 0 && (\n <MessagesLabels direction=\"row\" spacing={0.5} useFlexGap ownSx={{flexWrap: 'wrap', mt: 0.5}}>\n {message.labels.map((rawLabel) => {\n const label = tagOf(rawLabel);\n return <Chip key={label.label} size=\"small\" variant=\"outlined\" color={label.color} label={label.label} />;\n })}\n </MessagesLabels>\n )}\n </MessagesText>\n )}\n </MessagesItemButton>\n </MessagesItem>\n );\n })}\n </List>\n );\n}\n"],"names":["CheckRoundedIcon"],"mappings":"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"}
1
+ {"version":3,"file":"MessageList.js","sources":["../../src/inbox/MessageList.jsx"],"sourcesContent":["// La lista de mensajes de una bandeja tipo correo: quién lo manda (y cuántos mensajes trae la conversación), el asunto con sus\n// etiquetas, el principio del texto, la fecha corta, la estrella y el clip si trae archivos. Los no leídos van en negritas con\n// su punto.\n//\n// Lo de un producto, todo hacia afuera (la lista no guarda nada ni llama a ningún servidor):\n// - elegir varios: con onSelectionChange el monograma de cada mensaje se vuelve su casilla (como Gmail en el teléfono) y la\n// pantalla pone las acciones de lo elegido (SelectionActions: archivar, borrar, marcar como leído…);\n// - «⋯» de cada mensaje con las acciones que dé la pantalla (messageActions);\n// - renderMessage para pintar lo de adentro de cada renglón a su manera, y texts para cualquier texto.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaMessageList con defaultProps y\n// styleOverrides de sus partes: empty, selectedAvatar, item, actions, starButton, itemButton, avatarSlot, avatarButton,\n// avatar, body, text, topRow, unreadDot, sender, threadCount, date, subject, labels.\n//\n// Genérico del paquete: function (regla de la casa).\nimport Avatar from '@mui/material/Avatar';\nimport Box from '@mui/material/Box';\nimport ButtonBase from '@mui/material/ButtonBase';\nimport Chip from '@mui/material/Chip';\nimport IconButton from '@mui/material/IconButton';\nimport List from '@mui/material/List';\nimport ListItem from '@mui/material/ListItem';\nimport ListItemAvatar from '@mui/material/ListItemAvatar';\nimport ListItemButton from '@mui/material/ListItemButton';\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\nimport AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';\nimport CheckRoundedIcon from '@mui/icons-material/CheckRounded';\nimport StarBorderRoundedIcon from '@mui/icons-material/StarBorderRounded';\nimport StarRoundedIcon from '@mui/icons-material/StarRounded';\n\nimport {initialsOf} from '@/appShell/menuItemText.js';\nimport {inboxDateOf} from '@/inbox/inboxDates.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {tagOf} from '@/ui/labels/tagOf.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport ActionsMenu from '@/ui/actions/ActionsMenu.jsx';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaMessageList)\nconst THEME_NAME = 'CalcitaMessageList';\nconst MessagesEmpty = piecePart(THEME_NAME, 'Empty', Typography);\nconst MessagesSelectedAvatar = piecePart(THEME_NAME, 'SelectedAvatar', Avatar);\nconst MessagesItem = piecePart(THEME_NAME, 'Item', ListItem);\nconst MessagesActions = piecePart(THEME_NAME, 'Actions', Stack);\nconst MessagesStarButton = piecePart(THEME_NAME, 'StarButton', IconButton);\nconst MessagesItemButton = piecePart(THEME_NAME, 'ItemButton', ListItemButton);\nconst MessagesAvatarSlot = piecePart(THEME_NAME, 'AvatarSlot', ListItemAvatar);\nconst MessagesAvatarButton = piecePart(THEME_NAME, 'AvatarButton', ButtonBase);\nconst MessagesAvatar = piecePart(THEME_NAME, 'Avatar', Avatar);\nconst MessagesBody = piecePart(THEME_NAME, 'Body', Box);\nconst MessagesText = piecePart(THEME_NAME, 'Text', Box);\nconst MessagesTopRow = piecePart(THEME_NAME, 'TopRow', Stack);\nconst MessagesUnreadDot = piecePart(THEME_NAME, 'UnreadDot', Box);\nconst MessagesSender = piecePart(THEME_NAME, 'Sender', Typography);\nconst MessagesThreadCount = piecePart(THEME_NAME, 'ThreadCount', Typography);\nconst MessagesDate = piecePart(THEME_NAME, 'Date', Typography);\nconst MessagesSubject = piecePart(THEME_NAME, 'Subject', Typography);\nconst MessagesLabels = piecePart(THEME_NAME, 'Labels', Stack);\n\nconst UNREAD_DOT_PIXELS = 8;\nconst AVATAR_PIXELS = 40;\n\n/**\n * @param {object} props\n * @param {Array<{id: string, from: {name: string, email: string}, subject: string, preview: string, receivedAt: string, isUnread?: boolean, isStarred?: boolean, hasAttachments?: boolean, threadCount?: number, labels?: Array<string|{label: string, color?: string}>} & Record<string, any>>} props.messages\n * cada mensaje puede traer sus campos propios (llegan a renderMessage y a messageActions tal cual). threadCount: cuántos\n * mensajes trae la conversación (con 2 o más se pinta «(3)»); labels: texto o {label, color} del tema\n * @param {string|null} [props.selectedId] el mensaje abierto\n * @param {(messageId: string) => void} props.onSelect abrir un mensaje\n * @param {(messageId: string) => void} [props.onToggleStar]\n * @param {string[]} [props.selectedIds] los elegidos para una acción en grupo\n * @param {(messageIds: string[]) => void} [props.onSelectionChange] con él, cada mensaje se puede elegir picando su monograma\n * @param {(message: any) => Array<{key: string, label: string, icon?: import('react').ReactNode, color?: string, disabled?: boolean, onClick: () => void}>} [props.messageActions]\n * las acciones del «⋯» de cada mensaje (archivar, posponer, mover…); sin ellas no hay «⋯»\n * @param {(message: any) => import('react').ReactNode} [props.renderMessage] lo de adentro de cada renglón, a su manera\n * @param {string} [props.emptyText] ya traducido; por omisión el del paquete\n * @param {object} [props.texts] los textos que cambian, con la forma de `inbox` en los textos del paquete\n */\nexport default function MessageList(props) {\n // lo que el proyecto puso en su tema (components.CalcitaMessageList.defaultProps) y encima lo que llega aquí\n const {\n messages, selectedId = null, onSelect, onToggleStar, selectedIds = [], onSelectionChange, messageActions, renderMessage, emptyText, texts,\n ...unknownProps\n} = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('inbox', texts);\n const formatters = useFormatters();\n\n if (messages.length === 0) {\n return <MessagesEmpty variant=\"body2\" color=\"text.secondary\" ownSx={{p: 3, textAlign: 'center'}}>{emptyText ?? t('empty')}</MessagesEmpty>;\n }\n\n const toggleSelected = (messageId, isChecked) => {\n onSelectionChange(isChecked ? [...selectedIds, messageId] : selectedIds.filter((id) => id !== messageId));\n };\n\n return (\n <List disablePadding aria-label={t('listLabel')}>\n {messages.map((message) => {\n const weight = message.isUnread ? 700 : 400;\n const isChecked = selectedIds.includes(message.id);\n const avatar = <MessagesSelectedAvatar ownSx={{width: AVATAR_PIXELS, height: AVATAR_PIXELS}}>{initialsOf(message.from.name)}</MessagesSelectedAvatar>;\n return (\n <MessagesItem key={message.id} disablePadding secondaryAction={(onToggleStar || messageActions) && (\n <MessagesActions direction=\"row\" ownSx={{alignItems: 'center'}}>\n {onToggleStar && (\n <MessagesStarButton size=\"small\" onClick={() => onToggleStar(message.id)} aria-pressed={Boolean(message.isStarred)}\n aria-label={message.isStarred ? t('unstar') : t('star')} ownSx={{color: message.isStarred ? 'warning.main' : 'text.secondary'}}>\n {message.isStarred ? <StarRoundedIcon fontSize=\"small\" /> : <StarBorderRoundedIcon fontSize=\"small\" />}\n </MessagesStarButton>\n )}\n {messageActions && <ActionsMenu label={t('messageActions', {subject: message.subject})} actions={messageActions(message)} />}\n </MessagesActions>\n )} ownSx={{'& .MuiListItemSecondaryAction-root': {right: 4}}}>\n <MessagesItemButton selected={message.id === selectedId || isChecked} onClick={() => onSelect(message.id)} alignItems=\"flex-start\"\n ownSx={{borderRadius: 2, pr: onToggleStar && messageActions ? 10 : 6}}>\n <MessagesAvatarSlot ownSx={{mt: 0.5}}>\n {/* elegir: el monograma es la casilla (el clic no abre el mensaje). Un botón con papel de casilla y\n no el Checkbox de MUI: el tema le pinta a la casilla marcada un cuadro blanco detrás de su dibujo\n y la palomita salía como un cuadro */}\n {onSelectionChange ? (\n <MessagesAvatarButton role=\"checkbox\" aria-checked={isChecked} aria-label={t('select', {subject: message.subject})}\n ownSx={{borderRadius: '50%'}}\n onClick={(event) => {\n event.stopPropagation();\n toggleSelected(message.id, !isChecked);\n }}>\n {isChecked ? (\n <MessagesAvatar ownSx={{width: AVATAR_PIXELS, height: AVATAR_PIXELS, bgcolor: 'primary.main', color: 'primary.contrastText'}}>\n <CheckRoundedIcon />\n </MessagesAvatar>\n ) : avatar}\n </MessagesAvatarButton>\n ) : avatar}\n </MessagesAvatarSlot>\n {renderMessage ? <MessagesBody ownSx={{minWidth: 0, flex: 1}}>{renderMessage(message)}</MessagesBody> : (\n <MessagesText ownSx={{minWidth: 0, flex: 1}}>\n <MessagesTopRow direction=\"row\" spacing={1} ownSx={{alignItems: 'center'}}>\n {message.isUnread && (\n <MessagesUnreadDot component=\"span\" aria-label={t('unread')} ownSx={{width: UNREAD_DOT_PIXELS, height: UNREAD_DOT_PIXELS, borderRadius: '50%', bgcolor: 'primary.main', flexShrink: 0}} />\n )}\n <MessagesSender variant=\"body2\" noWrap ownSx={{fontWeight: weight, flex: 1}}>\n {message.from.name}\n {message.threadCount > 1 && (\n <MessagesThreadCount component=\"span\" variant=\"caption\" color=\"text.secondary\" ownSx={{ml: 0.5}} aria-label={t('threadCount', {count: message.threadCount})}>\n ({message.threadCount})\n </MessagesThreadCount>\n )}\n </MessagesSender>\n {message.hasAttachments && <AttachFileRoundedIcon fontSize=\"inherit\" color=\"action\" aria-label={t('attachments')} />}\n <MessagesDate variant=\"caption\" color=\"text.secondary\" ownSx={{flexShrink: 0, fontVariantNumeric: 'tabular-nums'}}>\n {inboxDateOf(message.receivedAt, new Date(), formatters)}\n </MessagesDate>\n </MessagesTopRow>\n <MessagesSubject variant=\"body2\" noWrap ownSx={{fontWeight: weight}}>{message.subject}</MessagesSubject>\n <Typography variant=\"body2\" color=\"text.secondary\" noWrap>{message.preview}</Typography>\n {message.labels?.length > 0 && (\n <MessagesLabels direction=\"row\" spacing={0.5} useFlexGap ownSx={{flexWrap: 'wrap', mt: 0.5}}>\n {message.labels.map((rawLabel) => {\n const label = tagOf(rawLabel);\n return <Chip key={label.label} size=\"small\" variant=\"outlined\" color={label.color} label={label.label} />;\n })}\n </MessagesLabels>\n )}\n </MessagesText>\n )}\n </MessagesItemButton>\n </MessagesItem>\n );\n })}\n </List>\n );\n}\n"],"names":["CheckRoundedIcon"],"mappings":"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"}
@@ -15,6 +15,7 @@ import { inboxDateOf } from "./inboxDates.js";
15
15
  import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
16
16
  import { tagOf } from "../ui/labels/tagOf.js";
17
17
  import { piecePart, usePieceProps } from "../theme/themePieces.js";
18
+ import { warnUnknownProps } from "../toolkit/unknownProps.js";
18
19
  const THEME_NAME = "CalcitaMessageView";
19
20
  const MessageHeaderPart = piecePart(THEME_NAME, "Header", Stack);
20
21
  const MessageHeaderText = piecePart(THEME_NAME, "HeaderText", Box);
@@ -67,7 +68,8 @@ function MessageBody({ entry, t, onAttachmentClick, renderBody }) {
67
68
  ] });
68
69
  }
69
70
  function MessageView(props) {
70
- const { message, actions, footer, onAttachmentClick, onExpand, renderBody, texts } = usePieceProps(props, THEME_NAME);
71
+ const { message, actions, footer, onAttachmentClick, onExpand, renderBody, texts, ...unknownProps } = usePieceProps(props, THEME_NAME);
72
+ warnUnknownProps(THEME_NAME, unknownProps);
71
73
  const { t } = useToolkitTranslation("inbox", texts);
72
74
  const formatters = useFormatters();
73
75
  const [expanded, setExpanded] = useState({ messageId: null, ids: [] });
@@ -1 +1 @@
1
- {"version":3,"file":"MessageView.js","sources":["../../src/inbox/MessageView.jsx"],"sourcesContent":["// La lectura de un mensaje o de una conversación completa: el asunto con sus etiquetas y, debajo, cada mensaje de la\n// conversación (los anteriores encogidos en un renglón con quién y cuándo, como Gmail; al picarlos se abren). De cada uno:\n// quién lo manda (con su correo), para quién y con copia a quién, la fecha completa, sus archivos y el texto. Arriba, las\n// acciones que quiera el proyecto (responder, archivar, posponer, borrar); abajo, lo que quiera poner (una respuesta rápida).\n//\n// El texto se pinta como TEXTO, con sus saltos de renglón: nunca como HTML (un correo con HTML adentro sería un XSS). Si un\n// proyecto necesita HTML, lo pinta con renderBody y es SU responsabilidad limpiarlo antes (DOMPurify o su backend).\n//\n// Todo hacia afuera: onAttachmentClick al picar un archivo (descargarlo, verlo), onExpand al abrir un mensaje anterior (para\n// marcarlo como leído), renderBody, footer y texts.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaMessageView con defaultProps y\n// styleOverrides de sus partes: header, headerText, recipients, body, empty, labels, attachments, divider, collapsed,\n// collapsedAvatar, collapsedText, collapsedSender, collapsedDate.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport Avatar from '@mui/material/Avatar';\nimport Box from '@mui/material/Box';\nimport ButtonBase from '@mui/material/ButtonBase';\nimport Chip from '@mui/material/Chip';\nimport Divider from '@mui/material/Divider';\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\nimport AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';\n\nimport {initialsOf} from '@/appShell/menuItemText.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {inboxDateOf} from '@/inbox/inboxDates.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {tagOf} from '@/ui/labels/tagOf.js';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaMessageView)\nconst THEME_NAME = 'CalcitaMessageView';\nconst MessageHeaderPart = piecePart(THEME_NAME, 'Header', Stack);\nconst MessageHeaderText = piecePart(THEME_NAME, 'HeaderText', Box);\nconst MessageRecipients = piecePart(THEME_NAME, 'Recipients', Stack);\nconst MessageBodyPart = piecePart(THEME_NAME, 'Body', Typography);\nconst MessageEmpty = piecePart(THEME_NAME, 'Empty', Typography);\nconst MessageLabels = piecePart(THEME_NAME, 'Labels', Stack);\nconst MessageAttachments = piecePart(THEME_NAME, 'Attachments', Stack);\nconst MessageDivider = piecePart(THEME_NAME, 'Divider', Divider);\nconst MessageCollapsed = piecePart(THEME_NAME, 'Collapsed', ButtonBase);\nconst MessageCollapsedAvatar = piecePart(THEME_NAME, 'CollapsedAvatar', Avatar);\nconst MessageCollapsedText = piecePart(THEME_NAME, 'CollapsedText', Box);\nconst MessageCollapsedSender = piecePart(THEME_NAME, 'CollapsedSender', Typography);\nconst MessageCollapsedDate = piecePart(THEME_NAME, 'CollapsedDate', Typography);\n\nconst PREVIEW_LENGTH = 90;\n\n/** Quién manda, para quién, con copia a quién y cuándo. */\nfunction MessageHeader({entry, t}) {\n const {formatDateTime} = useFormatters();\n return (\n <MessageHeaderPart direction=\"row\" spacing={1.5} ownSx={{alignItems: 'center'}}>\n <Avatar>{initialsOf(entry.from.name)}</Avatar>\n <MessageHeaderText ownSx={{minWidth: 0, flex: 1}}>\n <Typography variant=\"subtitle2\" component=\"p\" noWrap>\n {entry.from.name} <Typography component=\"span\" variant=\"body2\" color=\"text.secondary\">&lt;{entry.from.email}&gt;</Typography>\n </Typography>\n {entry.to?.length > 0 && <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\">{t('to', {to: entry.to.join(', ')})}</Typography>}\n {entry.cc?.length > 0 && <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\">{t('cc', {cc: entry.cc.join(', ')})}</Typography>}\n {/* la fecha debajo y no a la derecha: en el teléfono apretaba el «Para» a dos renglones */}\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\">{formatDateTime(entry.receivedAt)}</Typography>\n </MessageHeaderText>\n </MessageHeaderPart>\n );\n}\n\n/** Un mensaje abierto: su encabezado, sus archivos y su texto. */\nfunction MessageBody({entry, t, onAttachmentClick, renderBody}) {\n return (\n <Stack spacing={2}>\n <MessageHeader entry={entry} t={t} />\n {entry.attachments?.length > 0 && (\n <MessageRecipients direction=\"row\" spacing={1} useFlexGap ownSx={{flexWrap: 'wrap'}} aria-label={t('attachments')} role=\"group\">\n {entry.attachments.map((attachment) => (\n <Chip key={attachment.name} icon={<AttachFileRoundedIcon />} variant=\"outlined\"\n label={attachment.size ? `${attachment.name} · ${attachment.size}` : attachment.name}\n {...(onAttachmentClick && {onClick: () => onAttachmentClick(attachment, entry), 'aria-label': t('openAttachment', {name: attachment.name})})} />\n ))}\n </MessageRecipients>\n )}\n {renderBody ? renderBody(entry) : (\n <MessageBodyPart variant=\"body1\" component=\"div\" ownSx={{whiteSpace: 'pre-wrap', overflowWrap: 'anywhere'}}>{entry.body}</MessageBodyPart>\n )}\n </Stack>\n );\n}\n\n/**\n * @param {object} props\n * @param {{id?: string, subject: string, from: {name: string, email: string}, to?: string[], cc?: string[], receivedAt: string, body: string, attachments?: Array<{name: string, size?: string} & Record<string, any>>, labels?: Array<string|{label: string, color?: string}>, thread?: Array<{id: string, from: {name: string, email: string}, to?: string[], cc?: string[], receivedAt: string, body: string, attachments?: Array<{name: string, size?: string}>}>}|null} props.message\n * el mensaje más nuevo de la conversación; thread: los anteriores, del más viejo al más nuevo (encogidos). null: todavía\n * no se elige ninguno\n * @param {import('react').ReactNode} [props.actions] los botones de arriba\n * @param {import('react').ReactNode} [props.footer] lo de abajo (una respuesta rápida, firmas…)\n * @param {(attachment: any, message: any) => void} [props.onAttachmentClick] al picar un archivo; sin él los archivos solo se ven\n * @param {(threadMessageId: string) => void} [props.onExpand] al abrir un mensaje anterior de la conversación\n * @param {(message: any) => import('react').ReactNode} [props.renderBody] el texto a su manera (HTML ya limpio por el proyecto)\n * @param {object} [props.texts] los textos que cambian, con la forma de `inbox` en los textos del paquete\n */\nexport default function MessageView(props) {\n // lo que el proyecto puso en su tema (components.CalcitaMessageView.defaultProps) y encima lo que llega aquí\n const {message, actions, footer, onAttachmentClick, onExpand, renderBody, texts} = usePieceProps(props, THEME_NAME);\n const {t} = useToolkitTranslation('inbox', texts);\n const formatters = useFormatters();\n // los mensajes anteriores que la persona abrió (empiezan encogidos), de qué conversación: al cambiar de mensaje se olvidan\n const [expanded, setExpanded] = useState({messageId: null, ids: []});\n\n if (!message) {\n return <MessageEmpty variant=\"body2\" color=\"text.secondary\" ownSx={{p: 4, textAlign: 'center'}}>{t('noMessageSelected')}</MessageEmpty>;\n }\n\n const expandedIds = expanded.messageId === message.id ? expanded.ids : [];\n const expand = (threadMessageId) => {\n setExpanded({messageId: message.id, ids: [...expandedIds, threadMessageId]});\n onExpand?.(threadMessageId);\n };\n\n return (\n <Stack spacing={2} component=\"article\" aria-label={message.subject}>\n {actions && <MessageLabels direction=\"row\" spacing={0.5} useFlexGap ownSx={{flexWrap: 'wrap', alignItems: 'center'}}>{actions}</MessageLabels>}\n <Box>\n <Typography variant=\"h5\" component=\"h2\">{message.subject}</Typography>\n {message.labels?.length > 0 && (\n <MessageAttachments direction=\"row\" spacing={0.5} useFlexGap ownSx={{flexWrap: 'wrap', mt: 1}}>\n {message.labels.map((rawLabel) => {\n const label = tagOf(rawLabel);\n return <Chip key={label.label} size=\"small\" variant=\"outlined\" color={label.color} label={label.label} />;\n })}\n </MessageAttachments>\n )}\n </Box>\n {message.thread?.length > 0 && (\n <Typography variant=\"caption\" color=\"text.secondary\">{t('threadCount', {count: message.thread.length + 1})}</Typography>\n )}\n {/* los anteriores: encogidos en un renglón; abiertos, completos y con su línea abajo */}\n {message.thread?.map((entry) => (expandedIds.includes(entry.id) ? (\n <Box key={entry.id}>\n <MessageBody entry={entry} t={t} onAttachmentClick={onAttachmentClick} renderBody={renderBody} />\n <MessageDivider ownSx={{mt: 2}} />\n </Box>\n ) : (\n <MessageCollapsed key={entry.id} onClick={() => expand(entry.id)} aria-label={t('expandMessage', {name: entry.from.name, date: inboxDateOf(entry.receivedAt, new Date(), formatters)})}\n ownSx={{justifyContent: 'flex-start', textAlign: 'left', borderRadius: 2, p: 1, mx: -1, gap: 1.5, '&:hover': {bgcolor: 'action.hover'}}}>\n <MessageCollapsedAvatar ownSx={{width: 32, height: 32, fontSize: 13}}>{initialsOf(entry.from.name)}</MessageCollapsedAvatar>\n <MessageCollapsedText ownSx={{minWidth: 0, flex: 1}}>\n <MessageCollapsedSender variant=\"body2\" ownSx={{fontWeight: 600}} noWrap>{entry.from.name}</MessageCollapsedSender>\n <Typography variant=\"body2\" color=\"text.secondary\" noWrap>{entry.body.replace(/\\s+/g, ' ').slice(0, PREVIEW_LENGTH)}</Typography>\n </MessageCollapsedText>\n <MessageCollapsedDate variant=\"caption\" color=\"text.secondary\" ownSx={{flexShrink: 0}}>{inboxDateOf(entry.receivedAt, new Date(), formatters)}</MessageCollapsedDate>\n </MessageCollapsed>\n )))}\n {message.thread?.length > 0 && <Divider />}\n <MessageBody entry={message} t={t} onAttachmentClick={onAttachmentClick} renderBody={renderBody} />\n {footer}\n </Stack>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAkCA,MAAM,aAAa;AACnB,MAAM,oBAAoB,UAAU,YAAY,UAAU,KAAK;AAC/D,MAAM,oBAAoB,UAAU,YAAY,cAAc,GAAG;AACjE,MAAM,oBAAoB,UAAU,YAAY,cAAc,KAAK;AACnE,MAAM,kBAAkB,UAAU,YAAY,QAAQ,UAAU;AAChE,MAAM,eAAe,UAAU,YAAY,SAAS,UAAU;AAC9D,MAAM,gBAAgB,UAAU,YAAY,UAAU,KAAK;AAC3D,MAAM,qBAAqB,UAAU,YAAY,eAAe,KAAK;AACrE,MAAM,iBAAiB,UAAU,YAAY,WAAW,OAAO;AAC/D,MAAM,mBAAmB,UAAU,YAAY,aAAa,UAAU;AACtE,MAAM,yBAAyB,UAAU,YAAY,mBAAmB,MAAM;AAC9E,MAAM,uBAAuB,UAAU,YAAY,iBAAiB,GAAG;AACvE,MAAM,yBAAyB,UAAU,YAAY,mBAAmB,UAAU;AAClF,MAAM,uBAAuB,UAAU,YAAY,iBAAiB,UAAU;AAE9E,MAAM,iBAAiB;AAGvB,SAAS,cAAc,EAAC,OAAO,KAAI;AAC/B,QAAM,EAAC,eAAA,IAAkB,cAAA;AACzB,SACI,qBAAC,mBAAA,EAAkB,WAAU,OAAM,SAAS,KAAK,OAAO,EAAC,YAAY,SAAA,GACjE,UAAA;AAAA,IAAA,oBAAC,QAAA,EAAQ,UAAA,WAAW,MAAM,KAAK,IAAI,GAAE;AAAA,IACrC,qBAAC,qBAAkB,OAAO,EAAC,UAAU,GAAG,MAAM,KAC1C,UAAA;AAAA,MAAA,qBAAC,cAAW,SAAQ,aAAY,WAAU,KAAI,QAAM,MAC/C,UAAA;AAAA,QAAA,MAAM,KAAK;AAAA,QAAK;AAAA,6BAAE,YAAA,EAAW,WAAU,QAAO,SAAQ,SAAQ,OAAM,kBAAiB,UAAA;AAAA,UAAA;AAAA,UAAK,MAAM,KAAK;AAAA,UAAM;AAAA,QAAA,EAAA,CAAI;AAAA,MAAA,GACpH;AAAA,MACC,MAAM,IAAI,SAAS,yBAAM,YAAA,EAAW,SAAQ,WAAU,OAAM,kBAAiB,WAAU,KAAK,UAAA,EAAE,MAAM,EAAC,IAAI,MAAM,GAAG,KAAK,IAAI,EAAA,CAAE,EAAA,CAAE;AAAA,MAC/H,MAAM,IAAI,SAAS,yBAAM,YAAA,EAAW,SAAQ,WAAU,OAAM,kBAAiB,WAAU,KAAK,UAAA,EAAE,MAAM,EAAC,IAAI,MAAM,GAAG,KAAK,IAAI,EAAA,CAAE,EAAA,CAAE;AAAA,MAEhI,oBAAC,YAAA,EAAW,SAAQ,WAAU,OAAM,kBAAiB,WAAU,KAAK,UAAA,eAAe,MAAM,UAAU,EAAA,CAAE;AAAA,IAAA,EAAA,CACzG;AAAA,EAAA,GACJ;AAER;AAGA,SAAS,YAAY,EAAC,OAAO,GAAG,mBAAmB,cAAa;AAC5D,SACI,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,IAAA,oBAAC,eAAA,EAAc,OAAc,EAAA,CAAM;AAAA,IAClC,MAAM,aAAa,SAAS,KACzB,oBAAC,mBAAA,EAAkB,WAAU,OAAM,SAAS,GAAG,YAAU,MAAC,OAAO,EAAC,UAAU,OAAA,GAAS,cAAY,EAAE,aAAa,GAAG,MAAK,SACnH,UAAA,MAAM,YAAY,IAAI,CAAC,eACpB;AAAA,MAAC;AAAA,MAAA;AAAA,QAA2B,0BAAO,uBAAA,EAAsB;AAAA,QAAI,SAAQ;AAAA,QAC/D,OAAO,WAAW,OAAO,GAAG,WAAW,IAAI,MAAM,WAAW,IAAI,KAAK,WAAW;AAAA,QAC/E,GAAI,qBAAqB,EAAC,SAAS,MAAM,kBAAkB,YAAY,KAAK,GAAG,cAAc,EAAE,kBAAkB,EAAC,MAAM,WAAW,KAAA,CAAK,EAAA;AAAA,MAAC;AAAA,MAFrI,WAAW;AAAA,IAAA,CAGzB,GACL;AAAA,IAEH,aAAa,WAAW,KAAK,IAC1B,oBAAC,iBAAA,EAAgB,SAAQ,SAAQ,WAAU,OAAM,OAAO,EAAC,YAAY,YAAY,cAAc,cAAc,gBAAM,KAAA,CAAK;AAAA,EAAA,GAEhI;AAER;AAcA,SAAwB,YAAY,OAAO;AAEvC,QAAM,EAAC,SAAS,SAAS,QAAQ,mBAAmB,UAAU,YAAY,MAAA,IAAS,cAAc,OAAO,UAAU;AAClH,QAAM,EAAC,EAAA,IAAK,sBAAsB,SAAS,KAAK;AAChD,QAAM,aAAa,cAAA;AAEnB,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,EAAC,WAAW,MAAM,KAAK,CAAA,GAAG;AAEnE,MAAI,CAAC,SAAS;AACV,WAAO,oBAAC,cAAA,EAAa,SAAQ,SAAQ,OAAM,kBAAiB,OAAO,EAAC,GAAG,GAAG,WAAW,SAAA,GAAY,UAAA,EAAE,mBAAmB,GAAE;AAAA,EAC5H;AAEA,QAAM,cAAc,SAAS,cAAc,QAAQ,KAAK,SAAS,MAAM,CAAA;AACvE,QAAM,SAAS,CAAC,oBAAoB;AAChC,gBAAY,EAAC,WAAW,QAAQ,IAAI,KAAK,CAAC,GAAG,aAAa,eAAe,GAAE;AAC3E,eAAW,eAAe;AAAA,EAC9B;AAEA,SACI,qBAAC,SAAM,SAAS,GAAG,WAAU,WAAU,cAAY,QAAQ,SACtD,UAAA;AAAA,IAAA,WAAW,oBAAC,eAAA,EAAc,WAAU,OAAM,SAAS,KAAK,YAAU,MAAC,OAAO,EAAC,UAAU,QAAQ,YAAY,YAAY,UAAA,SAAQ;AAAA,yBAC7H,KAAA,EACG,UAAA;AAAA,MAAA,oBAAC,cAAW,SAAQ,MAAK,WAAU,MAAM,kBAAQ,SAAQ;AAAA,MACxD,QAAQ,QAAQ,SAAS,yBACrB,oBAAA,EAAmB,WAAU,OAAM,SAAS,KAAK,YAAU,MAAC,OAAO,EAAC,UAAU,QAAQ,IAAI,EAAA,GACtF,UAAA,QAAQ,OAAO,IAAI,CAAC,aAAa;AAC9B,cAAM,QAAQ,MAAM,QAAQ;AAC5B,eAAO,oBAAC,MAAA,EAAuB,MAAK,SAAQ,SAAQ,YAAW,OAAO,MAAM,OAAO,OAAO,MAAM,MAAA,GAA9E,MAAM,KAA+E;AAAA,MAC3G,CAAC,EAAA,CACL;AAAA,IAAA,GAER;AAAA,IACC,QAAQ,QAAQ,SAAS,KACtB,oBAAC,YAAA,EAAW,SAAQ,WAAU,OAAM,kBAAkB,UAAA,EAAE,eAAe,EAAC,OAAO,QAAQ,OAAO,SAAS,EAAA,CAAE,GAAE;AAAA,IAG9G,QAAQ,QAAQ,IAAI,CAAC,UAAW,YAAY,SAAS,MAAM,EAAE,IAC1D,qBAAC,KAAA,EACG,UAAA;AAAA,MAAA,oBAAC,aAAA,EAAY,OAAc,GAAM,mBAAsC,YAAwB;AAAA,0BAC9F,gBAAA,EAAe,OAAO,EAAC,IAAI,IAAC,CAAG;AAAA,IAAA,KAF1B,MAAM,EAGhB,IAEA;AAAA,MAAC;AAAA,MAAA;AAAA,QAAgC,SAAS,MAAM,OAAO,MAAM,EAAE;AAAA,QAAG,cAAY,EAAE,iBAAiB,EAAC,MAAM,MAAM,KAAK,MAAM,MAAM,YAAY,MAAM,YAAY,oBAAI,QAAQ,UAAU,GAAE;AAAA,QACzK,OAAO,EAAC,gBAAgB,cAAc,WAAW,QAAQ,cAAc,GAAG,GAAG,GAAG,IAAI,IAAI,KAAK,KAAK,WAAW,EAAC,SAAS,iBAAc;AAAA,QAC7I,UAAA;AAAA,UAAA,oBAAC,wBAAA,EAAuB,OAAO,EAAC,OAAO,IAAI,QAAQ,IAAI,UAAU,GAAA,GAAM,UAAA,WAAW,MAAM,KAAK,IAAI,GAAE;AAAA,UACnG,qBAAC,wBAAqB,OAAO,EAAC,UAAU,GAAG,MAAM,KAC7C,UAAA;AAAA,YAAA,oBAAC,wBAAA,EAAuB,SAAQ,SAAQ,OAAO,EAAC,YAAY,IAAA,GAAM,QAAM,MAAE,UAAA,MAAM,KAAK,MAAK;AAAA,gCACzF,YAAA,EAAW,SAAQ,SAAQ,OAAM,kBAAiB,QAAM,MAAE,UAAA,MAAM,KAAK,QAAQ,QAAQ,GAAG,EAAE,MAAM,GAAG,cAAc,EAAA,CAAE;AAAA,UAAA,GACxH;AAAA,8BACC,sBAAA,EAAqB,SAAQ,WAAU,OAAM,kBAAiB,OAAO,EAAC,YAAY,KAAK,sBAAY,MAAM,gCAAgB,KAAA,GAAQ,UAAU,EAAA,CAAE;AAAA,QAAA;AAAA,MAAA;AAAA,MAP3H,MAAM;AAAA,IAAA,CAS/B;AAAA,IACD,QAAQ,QAAQ,SAAS,yBAAM,SAAA,EAAQ;AAAA,wBACvC,aAAA,EAAY,OAAO,SAAS,GAAM,mBAAsC,YAAwB;AAAA,IAChG;AAAA,EAAA,GACL;AAER;"}
1
+ {"version":3,"file":"MessageView.js","sources":["../../src/inbox/MessageView.jsx"],"sourcesContent":["// La lectura de un mensaje o de una conversación completa: el asunto con sus etiquetas y, debajo, cada mensaje de la\n// conversación (los anteriores encogidos en un renglón con quién y cuándo, como Gmail; al picarlos se abren). De cada uno:\n// quién lo manda (con su correo), para quién y con copia a quién, la fecha completa, sus archivos y el texto. Arriba, las\n// acciones que quiera el proyecto (responder, archivar, posponer, borrar); abajo, lo que quiera poner (una respuesta rápida).\n//\n// El texto se pinta como TEXTO, con sus saltos de renglón: nunca como HTML (un correo con HTML adentro sería un XSS). Si un\n// proyecto necesita HTML, lo pinta con renderBody y es SU responsabilidad limpiarlo antes (DOMPurify o su backend).\n//\n// Todo hacia afuera: onAttachmentClick al picar un archivo (descargarlo, verlo), onExpand al abrir un mensaje anterior (para\n// marcarlo como leído), renderBody, footer y texts.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaMessageView con defaultProps y\n// styleOverrides de sus partes: header, headerText, recipients, body, empty, labels, attachments, divider, collapsed,\n// collapsedAvatar, collapsedText, collapsedSender, collapsedDate.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport Avatar from '@mui/material/Avatar';\nimport Box from '@mui/material/Box';\nimport ButtonBase from '@mui/material/ButtonBase';\nimport Chip from '@mui/material/Chip';\nimport Divider from '@mui/material/Divider';\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\nimport AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';\n\nimport {initialsOf} from '@/appShell/menuItemText.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {inboxDateOf} from '@/inbox/inboxDates.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {tagOf} from '@/ui/labels/tagOf.js';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaMessageView)\nconst THEME_NAME = 'CalcitaMessageView';\nconst MessageHeaderPart = piecePart(THEME_NAME, 'Header', Stack);\nconst MessageHeaderText = piecePart(THEME_NAME, 'HeaderText', Box);\nconst MessageRecipients = piecePart(THEME_NAME, 'Recipients', Stack);\nconst MessageBodyPart = piecePart(THEME_NAME, 'Body', Typography);\nconst MessageEmpty = piecePart(THEME_NAME, 'Empty', Typography);\nconst MessageLabels = piecePart(THEME_NAME, 'Labels', Stack);\nconst MessageAttachments = piecePart(THEME_NAME, 'Attachments', Stack);\nconst MessageDivider = piecePart(THEME_NAME, 'Divider', Divider);\nconst MessageCollapsed = piecePart(THEME_NAME, 'Collapsed', ButtonBase);\nconst MessageCollapsedAvatar = piecePart(THEME_NAME, 'CollapsedAvatar', Avatar);\nconst MessageCollapsedText = piecePart(THEME_NAME, 'CollapsedText', Box);\nconst MessageCollapsedSender = piecePart(THEME_NAME, 'CollapsedSender', Typography);\nconst MessageCollapsedDate = piecePart(THEME_NAME, 'CollapsedDate', Typography);\n\nconst PREVIEW_LENGTH = 90;\n\n/** Quién manda, para quién, con copia a quién y cuándo. */\nfunction MessageHeader({entry, t}) {\n const {formatDateTime} = useFormatters();\n return (\n <MessageHeaderPart direction=\"row\" spacing={1.5} ownSx={{alignItems: 'center'}}>\n <Avatar>{initialsOf(entry.from.name)}</Avatar>\n <MessageHeaderText ownSx={{minWidth: 0, flex: 1}}>\n <Typography variant=\"subtitle2\" component=\"p\" noWrap>\n {entry.from.name} <Typography component=\"span\" variant=\"body2\" color=\"text.secondary\">&lt;{entry.from.email}&gt;</Typography>\n </Typography>\n {entry.to?.length > 0 && <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\">{t('to', {to: entry.to.join(', ')})}</Typography>}\n {entry.cc?.length > 0 && <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\">{t('cc', {cc: entry.cc.join(', ')})}</Typography>}\n {/* la fecha debajo y no a la derecha: en el teléfono apretaba el «Para» a dos renglones */}\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\">{formatDateTime(entry.receivedAt)}</Typography>\n </MessageHeaderText>\n </MessageHeaderPart>\n );\n}\n\n/** Un mensaje abierto: su encabezado, sus archivos y su texto. */\nfunction MessageBody({entry, t, onAttachmentClick, renderBody}) {\n return (\n <Stack spacing={2}>\n <MessageHeader entry={entry} t={t} />\n {entry.attachments?.length > 0 && (\n <MessageRecipients direction=\"row\" spacing={1} useFlexGap ownSx={{flexWrap: 'wrap'}} aria-label={t('attachments')} role=\"group\">\n {entry.attachments.map((attachment) => (\n <Chip key={attachment.name} icon={<AttachFileRoundedIcon />} variant=\"outlined\"\n label={attachment.size ? `${attachment.name} · ${attachment.size}` : attachment.name}\n {...(onAttachmentClick && {onClick: () => onAttachmentClick(attachment, entry), 'aria-label': t('openAttachment', {name: attachment.name})})} />\n ))}\n </MessageRecipients>\n )}\n {renderBody ? renderBody(entry) : (\n <MessageBodyPart variant=\"body1\" component=\"div\" ownSx={{whiteSpace: 'pre-wrap', overflowWrap: 'anywhere'}}>{entry.body}</MessageBodyPart>\n )}\n </Stack>\n );\n}\n\n/**\n * @param {object} props\n * @param {{id?: string, subject: string, from: {name: string, email: string}, to?: string[], cc?: string[], receivedAt: string, body: string, attachments?: Array<{name: string, size?: string} & Record<string, any>>, labels?: Array<string|{label: string, color?: string}>, thread?: Array<{id: string, from: {name: string, email: string}, to?: string[], cc?: string[], receivedAt: string, body: string, attachments?: Array<{name: string, size?: string}>}>}|null} props.message\n * el mensaje más nuevo de la conversación; thread: los anteriores, del más viejo al más nuevo (encogidos). null: todavía\n * no se elige ninguno\n * @param {import('react').ReactNode} [props.actions] los botones de arriba\n * @param {import('react').ReactNode} [props.footer] lo de abajo (una respuesta rápida, firmas…)\n * @param {(attachment: any, message: any) => void} [props.onAttachmentClick] al picar un archivo; sin él los archivos solo se ven\n * @param {(threadMessageId: string) => void} [props.onExpand] al abrir un mensaje anterior de la conversación\n * @param {(message: any) => import('react').ReactNode} [props.renderBody] el texto a su manera (HTML ya limpio por el proyecto)\n * @param {object} [props.texts] los textos que cambian, con la forma de `inbox` en los textos del paquete\n */\nexport default function MessageView(props) {\n // lo que el proyecto puso en su tema (components.CalcitaMessageView.defaultProps) y encima lo que llega aquí\n const {message, actions, footer, onAttachmentClick, onExpand, renderBody, texts, ...unknownProps} = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('inbox', texts);\n const formatters = useFormatters();\n // los mensajes anteriores que la persona abrió (empiezan encogidos), de qué conversación: al cambiar de mensaje se olvidan\n const [expanded, setExpanded] = useState({messageId: null, ids: []});\n\n if (!message) {\n return <MessageEmpty variant=\"body2\" color=\"text.secondary\" ownSx={{p: 4, textAlign: 'center'}}>{t('noMessageSelected')}</MessageEmpty>;\n }\n\n const expandedIds = expanded.messageId === message.id ? expanded.ids : [];\n const expand = (threadMessageId) => {\n setExpanded({messageId: message.id, ids: [...expandedIds, threadMessageId]});\n onExpand?.(threadMessageId);\n };\n\n return (\n <Stack spacing={2} component=\"article\" aria-label={message.subject}>\n {actions && <MessageLabels direction=\"row\" spacing={0.5} useFlexGap ownSx={{flexWrap: 'wrap', alignItems: 'center'}}>{actions}</MessageLabels>}\n <Box>\n <Typography variant=\"h5\" component=\"h2\">{message.subject}</Typography>\n {message.labels?.length > 0 && (\n <MessageAttachments direction=\"row\" spacing={0.5} useFlexGap ownSx={{flexWrap: 'wrap', mt: 1}}>\n {message.labels.map((rawLabel) => {\n const label = tagOf(rawLabel);\n return <Chip key={label.label} size=\"small\" variant=\"outlined\" color={label.color} label={label.label} />;\n })}\n </MessageAttachments>\n )}\n </Box>\n {message.thread?.length > 0 && (\n <Typography variant=\"caption\" color=\"text.secondary\">{t('threadCount', {count: message.thread.length + 1})}</Typography>\n )}\n {/* los anteriores: encogidos en un renglón; abiertos, completos y con su línea abajo */}\n {message.thread?.map((entry) => (expandedIds.includes(entry.id) ? (\n <Box key={entry.id}>\n <MessageBody entry={entry} t={t} onAttachmentClick={onAttachmentClick} renderBody={renderBody} />\n <MessageDivider ownSx={{mt: 2}} />\n </Box>\n ) : (\n <MessageCollapsed key={entry.id} onClick={() => expand(entry.id)} aria-label={t('expandMessage', {name: entry.from.name, date: inboxDateOf(entry.receivedAt, new Date(), formatters)})}\n ownSx={{justifyContent: 'flex-start', textAlign: 'left', borderRadius: 2, p: 1, mx: -1, gap: 1.5, '&:hover': {bgcolor: 'action.hover'}}}>\n <MessageCollapsedAvatar ownSx={{width: 32, height: 32, fontSize: 13}}>{initialsOf(entry.from.name)}</MessageCollapsedAvatar>\n <MessageCollapsedText ownSx={{minWidth: 0, flex: 1}}>\n <MessageCollapsedSender variant=\"body2\" ownSx={{fontWeight: 600}} noWrap>{entry.from.name}</MessageCollapsedSender>\n <Typography variant=\"body2\" color=\"text.secondary\" noWrap>{entry.body.replace(/\\s+/g, ' ').slice(0, PREVIEW_LENGTH)}</Typography>\n </MessageCollapsedText>\n <MessageCollapsedDate variant=\"caption\" color=\"text.secondary\" ownSx={{flexShrink: 0}}>{inboxDateOf(entry.receivedAt, new Date(), formatters)}</MessageCollapsedDate>\n </MessageCollapsed>\n )))}\n {message.thread?.length > 0 && <Divider />}\n <MessageBody entry={message} t={t} onAttachmentClick={onAttachmentClick} renderBody={renderBody} />\n {footer}\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { ToolkitProvider } from "./toolkit/ToolkitProvider.js";
2
2
  import { useToolkitNavigation, useToolkitSettings } from "./toolkit/toolkitContext.js";
3
3
  import { useToolkitTranslation } from "./toolkit/useToolkitTranslation.js";
4
4
  import { default as default2 } from "./toolkit/ComponentTexts.js";
5
- import { DEFAULT_CURRENCY, DEFAULT_LANGUAGE, DEFAULT_LANGUAGES, configureToolkit, toolkitSettings, translateToolkit } from "./toolkit/toolkitSettings.js";
5
+ import { DEFAULT_CURRENCY, DEFAULT_HOUR_CYCLE, DEFAULT_LANGUAGE, DEFAULT_LANGUAGES, HOUR_CYCLE, configureToolkit, toolkitSettings, translateToolkit } from "./toolkit/toolkitSettings.js";
6
6
  import { browserNavigation, createBrowserNavigation } from "./toolkit/browserNavigation.js";
7
7
  import { createHashNavigation } from "./toolkit/hashNavigation.js";
8
8
  import { default as default3 } from "./locales/es.js";
@@ -165,6 +165,7 @@ export {
165
165
  default49 as CopyButton,
166
166
  DEFAULT_CURRENCY,
167
167
  DEFAULT_FILE_TYPES,
168
+ DEFAULT_HOUR_CYCLE,
168
169
  DEFAULT_LANGUAGE,
169
170
  DEFAULT_LANGUAGES,
170
171
  DEFAULT_PASSWORD_POLICY,
@@ -199,6 +200,7 @@ export {
199
200
  default29 as GlassLensFilter,
200
201
  default30 as GlassPanel,
201
202
  HEX_COLOR,
203
+ HOUR_CYCLE,
202
204
  INSTALL_VARIANT,
203
205
  default66 as InboxLayout,
204
206
  default71 as InstallAppSetting,
@@ -6,6 +6,7 @@ import Autocomplete from "@mui/material/Autocomplete";
6
6
  import TextField from "@mui/material/TextField";
7
7
  import { translateValidationMessage } from "../forms/validationMessages.js";
8
8
  import FieldFrame from "./FieldFrame.js";
9
+ import { warnUnknownProps } from "../toolkit/unknownProps.js";
9
10
  const MAX_VISIBLE_OPTIONS = 100;
10
11
  function plainTextOf(text) {
11
12
  return String(text ?? "").normalize("NFD").replace(new RegExp("\\p{Diacritic}", "gu"), "").toLowerCase();
@@ -25,8 +26,10 @@ function CatalogInput({
25
26
  inputRef,
26
27
  placeholder,
27
28
  multiple = false,
28
- texts
29
+ texts,
30
+ ...unknownProps
29
31
  }) {
32
+ warnUnknownProps("CatalogInput", unknownProps);
30
33
  const { t } = useToolkitTranslation("common", texts);
31
34
  const generatedId = useId();
32
35
  const inputId = `${name ?? "catalog"}-${generatedId}`;
@@ -1 +1 @@
1
- {"version":3,"file":"CatalogInput.js","sources":["../../src/inputs/CatalogInput.jsx"],"sourcesContent":["// Campo suelto que elige una clave de un catálogo ([{code, label}]): actividad económica, giro, país, banco… Se busca por la\n// clave o por el texto, sin acentos ni mayúsculas, y al formulario llega solo la clave. Con `multiple` elige varias: cada una\n// queda como chip con su ×, la lista no se cierra al elegir y al formulario llega la lista de claves en el orden elegido.\n//\n// Por qué no un <select>: con catálogos de cientos de opciones no se puede usar; y con texto libre la gente inventa claves.\n// Se enseñan a lo más MAX_VISIBLE_OPTIONS coincidencias: con miles, la lista completa congela la pantalla.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useId} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Autocomplete from '@mui/material/Autocomplete';\nimport TextField from '@mui/material/TextField';\n\nimport {translateValidationMessage} from '@/forms/validationMessages.js';\nimport FieldFrame from '@/inputs/FieldFrame.jsx';\n\nconst MAX_VISIBLE_OPTIONS = 100;\n\n/**\n * Un texto sin acentos ni mayúsculas, para que «credito» encuentre «Crédito».\n *\n * @param {unknown} text\n * @returns {string}\n */\nfunction plainTextOf(text) {\n return String(text ?? '').normalize('NFD').replace(/\\p{Diacritic}/gu, '').toLowerCase();\n}\n\n/**\n * @param {object} props\n * @param {string} [props.name]\n * @param {string} props.label ya traducido\n * @param {string|string[]|null|undefined} props.value la clave elegida ('' = ninguna); con multiple, la lista ([] = ninguna)\n * @param {(code: any) => void} props.onChange recibe la clave ('' al borrar); con multiple, la lista de claves\n * @param {() => void} [props.onBlur]\n * @param {Array<{code: string, label: string}>} props.options\n * @param {(option: {code: string, label: string}) => string} [props.formatLabel] por omisión «Nombre (clave)»\n * @param {string|{key: string, values?: object}} [props.error]\n * @param {string} [props.helperText]\n * @param {boolean} [props.loading] el catálogo todavía no llega\n * @param {string} [props.placeholder] la pista con el campo vacío, ya traducida (p. ej. «Busque un estado»): sin ella, en un\n * formulario agrupado el campo vacío solo enseña su botón y no se entiende qué es\n * @param {boolean} [props.disabled]\n * @param {import('react').Ref<HTMLInputElement>} [props.inputRef]\n * @param {boolean} [props.multiple] elegir varias claves\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nexport default function CatalogInput({\n name, label, value, onChange, onBlur, options = [], formatLabel, error, helperText = ' ', loading = false, disabled = false, inputRef, placeholder,\n multiple = false, texts,\n}) {\n const {t} = useToolkitTranslation('common', texts);\n const generatedId = useId();\n const inputId = `${name ?? 'catalog'}-${generatedId}`;\n const errorText = translateValidationMessage(t, error);\n // con varias, en el orden en que se eligieron (el de la lista de claves)\n const selectedOption = multiple\n ? (Array.isArray(value) ? value : []).map((code) => options.find((option) => option.code === code)).filter(Boolean)\n : options.find((option) => option.code === value) ?? null;\n const optionLabelOf = (option) => (formatLabel ? formatLabel(option) : `${option.label} (${option.code})`);\n\n return (\n <FieldFrame inputId={inputId} label={label}>\n <Autocomplete\n id={inputId}\n multiple={multiple}\n disableCloseOnSelect={multiple}\n fullWidth\n size=\"small\"\n options={options}\n value={selectedOption}\n loading={loading}\n disabled={disabled}\n noOptionsText={t('form.noOptions')}\n loadingText={t('form.loadingOptions')}\n getOptionLabel={optionLabelOf}\n isOptionEqualToValue={(option, currentOption) => option.code === currentOption.code}\n filterOptions={(allOptions, {inputValue}) => {\n const wantedText = plainTextOf(inputValue);\n const matchingOptions = wantedText\n ? allOptions.filter((option) => plainTextOf(optionLabelOf(option)).includes(wantedText))\n : allOptions;\n return matchingOptions.slice(0, MAX_VISIBLE_OPTIONS);\n }}\n onChange={(event, option) => onChange(multiple ? option.map((chosenOption) => chosenOption.code) : option?.code ?? '')}\n onBlur={onBlur}\n renderInput={(inputProps) => (\n <TextField {...inputProps} name={name} inputRef={inputRef} placeholder={placeholder} error={Boolean(errorText)} helperText={errorText ?? helperText} />\n )}\n />\n </FieldFrame>\n );\n}\n"],"names":[],"mappings":";;;;;;;;AAgBA,MAAM,sBAAsB;AAQ5B,SAAS,YAAY,MAAM;AACvB,SAAO,OAAO,QAAQ,EAAE,EAAE,UAAU,KAAK,EAAE,QAAQ,WAAA,kBAAA,IAAA,GAAmB,EAAE,EAAE,YAAA;AAC9E;AAqBA,SAAwB,aAAa;AAAA,EACjC;AAAA,EAAM;AAAA,EAAO;AAAA,EAAO;AAAA,EAAU;AAAA,EAAQ,UAAU,CAAA;AAAA,EAAI;AAAA,EAAa;AAAA,EAAO,aAAa;AAAA,EAAK,UAAU;AAAA,EAAO,WAAW;AAAA,EAAO;AAAA,EAAU;AAAA,EACvI,WAAW;AAAA,EAAO;AACtB,GAAG;AACC,QAAM,EAAC,EAAA,IAAK,sBAAsB,UAAU,KAAK;AACjD,QAAM,cAAc,MAAA;AACpB,QAAM,UAAU,GAAG,QAAQ,SAAS,IAAI,WAAW;AACnD,QAAM,YAAY,2BAA2B,GAAG,KAAK;AAErD,QAAM,iBAAiB,YAChB,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAA,GAAI,IAAI,CAAC,SAAS,QAAQ,KAAK,CAAC,WAAW,OAAO,SAAS,IAAI,CAAC,EAAE,OAAO,OAAO,IAChH,QAAQ,KAAK,CAAC,WAAW,OAAO,SAAS,KAAK,KAAK;AACzD,QAAM,gBAAgB,CAAC,WAAY,cAAc,YAAY,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,OAAO,IAAI;AAEtG,SACI,oBAAC,YAAA,EAAW,SAAkB,OAC1B,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACG,IAAI;AAAA,MACJ;AAAA,MACA,sBAAsB;AAAA,MACtB,WAAS;AAAA,MACT,MAAK;AAAA,MACL;AAAA,MACA,OAAO;AAAA,MACP;AAAA,MACA;AAAA,MACA,eAAe,EAAE,gBAAgB;AAAA,MACjC,aAAa,EAAE,qBAAqB;AAAA,MACpC,gBAAgB;AAAA,MAChB,sBAAsB,CAAC,QAAQ,kBAAkB,OAAO,SAAS,cAAc;AAAA,MAC/E,eAAe,CAAC,YAAY,EAAC,iBAAgB;AACzC,cAAM,aAAa,YAAY,UAAU;AACzC,cAAM,kBAAkB,aAClB,WAAW,OAAO,CAAC,WAAW,YAAY,cAAc,MAAM,CAAC,EAAE,SAAS,UAAU,CAAC,IACrF;AACN,eAAO,gBAAgB,MAAM,GAAG,mBAAmB;AAAA,MACvD;AAAA,MACA,UAAU,CAAC,OAAO,WAAW,SAAS,WAAW,OAAO,IAAI,CAAC,iBAAiB,aAAa,IAAI,IAAI,QAAQ,QAAQ,EAAE;AAAA,MACrH;AAAA,MACA,aAAa,CAAC,eACV,oBAAC,WAAA,EAAW,GAAG,YAAY,MAAY,UAAoB,aAA0B,OAAO,QAAQ,SAAS,GAAG,YAAY,aAAa,WAAA,CAAY;AAAA,IAAA;AAAA,EAAA,GAGjK;AAER;"}
1
+ {"version":3,"file":"CatalogInput.js","sources":["../../src/inputs/CatalogInput.jsx"],"sourcesContent":["// Campo suelto que elige una clave de un catálogo ([{code, label}]): actividad económica, giro, país, banco… Se busca por la\n// clave o por el texto, sin acentos ni mayúsculas, y al formulario llega solo la clave. Con `multiple` elige varias: cada una\n// queda como chip con su ×, la lista no se cierra al elegir y al formulario llega la lista de claves en el orden elegido.\n//\n// Por qué no un <select>: con catálogos de cientos de opciones no se puede usar; y con texto libre la gente inventa claves.\n// Se enseñan a lo más MAX_VISIBLE_OPTIONS coincidencias: con miles, la lista completa congela la pantalla.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useId} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Autocomplete from '@mui/material/Autocomplete';\nimport TextField from '@mui/material/TextField';\n\nimport {translateValidationMessage} from '@/forms/validationMessages.js';\nimport FieldFrame from '@/inputs/FieldFrame.jsx';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\nconst MAX_VISIBLE_OPTIONS = 100;\n\n/**\n * Un texto sin acentos ni mayúsculas, para que «credito» encuentre «Crédito».\n *\n * @param {unknown} text\n * @returns {string}\n */\nfunction plainTextOf(text) {\n return String(text ?? '').normalize('NFD').replace(/\\p{Diacritic}/gu, '').toLowerCase();\n}\n\n/**\n * @param {object} props\n * @param {string} [props.name]\n * @param {string} props.label ya traducido\n * @param {string|string[]|null|undefined} props.value la clave elegida ('' = ninguna); con multiple, la lista ([] = ninguna)\n * @param {(code: any) => void} props.onChange recibe la clave ('' al borrar); con multiple, la lista de claves\n * @param {() => void} [props.onBlur]\n * @param {Array<{code: string, label: string}>} props.options\n * @param {(option: {code: string, label: string}) => string} [props.formatLabel] por omisión «Nombre (clave)»\n * @param {string|{key: string, values?: object}} [props.error]\n * @param {string} [props.helperText]\n * @param {boolean} [props.loading] el catálogo todavía no llega\n * @param {string} [props.placeholder] la pista con el campo vacío, ya traducida (p. ej. «Busque un estado»): sin ella, en un\n * formulario agrupado el campo vacío solo enseña su botón y no se entiende qué es\n * @param {boolean} [props.disabled]\n * @param {import('react').Ref<HTMLInputElement>} [props.inputRef]\n * @param {boolean} [props.multiple] elegir varias claves\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nexport default function CatalogInput({\n name, label, value, onChange, onBlur, options = [], formatLabel, error, helperText = ' ', loading = false, disabled = false, inputRef, placeholder,\n multiple = false, texts,\n ...unknownProps\n}) {\n warnUnknownProps('CatalogInput', unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n const generatedId = useId();\n const inputId = `${name ?? 'catalog'}-${generatedId}`;\n const errorText = translateValidationMessage(t, error);\n // con varias, en el orden en que se eligieron (el de la lista de claves)\n const selectedOption = multiple\n ? (Array.isArray(value) ? value : []).map((code) => options.find((option) => option.code === code)).filter(Boolean)\n : options.find((option) => option.code === value) ?? null;\n const optionLabelOf = (option) => (formatLabel ? formatLabel(option) : `${option.label} (${option.code})`);\n\n return (\n <FieldFrame inputId={inputId} label={label}>\n <Autocomplete\n id={inputId}\n multiple={multiple}\n disableCloseOnSelect={multiple}\n fullWidth\n size=\"small\"\n options={options}\n value={selectedOption}\n loading={loading}\n disabled={disabled}\n noOptionsText={t('form.noOptions')}\n loadingText={t('form.loadingOptions')}\n getOptionLabel={optionLabelOf}\n isOptionEqualToValue={(option, currentOption) => option.code === currentOption.code}\n filterOptions={(allOptions, {inputValue}) => {\n const wantedText = plainTextOf(inputValue);\n const matchingOptions = wantedText\n ? allOptions.filter((option) => plainTextOf(optionLabelOf(option)).includes(wantedText))\n : allOptions;\n return matchingOptions.slice(0, MAX_VISIBLE_OPTIONS);\n }}\n onChange={(event, option) => onChange(multiple ? option.map((chosenOption) => chosenOption.code) : option?.code ?? '')}\n onBlur={onBlur}\n renderInput={(inputProps) => (\n <TextField {...inputProps} name={name} inputRef={inputRef} placeholder={placeholder} error={Boolean(errorText)} helperText={errorText ?? helperText} />\n )}\n />\n </FieldFrame>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;AAiBA,MAAM,sBAAsB;AAQ5B,SAAS,YAAY,MAAM;AACvB,SAAO,OAAO,QAAQ,EAAE,EAAE,UAAU,KAAK,EAAE,QAAQ,WAAA,kBAAA,IAAA,GAAmB,EAAE,EAAE,YAAA;AAC9E;AAqBA,SAAwB,aAAa;AAAA,EACjC;AAAA,EAAM;AAAA,EAAO;AAAA,EAAO;AAAA,EAAU;AAAA,EAAQ,UAAU,CAAA;AAAA,EAAI;AAAA,EAAa;AAAA,EAAO,aAAa;AAAA,EAAK,UAAU;AAAA,EAAO,WAAW;AAAA,EAAO;AAAA,EAAU;AAAA,EACvI,WAAW;AAAA,EAAO;AAAA,EAClB,GAAG;AACP,GAAG;AACC,mBAAiB,gBAAgB,YAAY;AAC7C,QAAM,EAAC,EAAA,IAAK,sBAAsB,UAAU,KAAK;AACjD,QAAM,cAAc,MAAA;AACpB,QAAM,UAAU,GAAG,QAAQ,SAAS,IAAI,WAAW;AACnD,QAAM,YAAY,2BAA2B,GAAG,KAAK;AAErD,QAAM,iBAAiB,YAChB,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAA,GAAI,IAAI,CAAC,SAAS,QAAQ,KAAK,CAAC,WAAW,OAAO,SAAS,IAAI,CAAC,EAAE,OAAO,OAAO,IAChH,QAAQ,KAAK,CAAC,WAAW,OAAO,SAAS,KAAK,KAAK;AACzD,QAAM,gBAAgB,CAAC,WAAY,cAAc,YAAY,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,OAAO,IAAI;AAEtG,SACI,oBAAC,YAAA,EAAW,SAAkB,OAC1B,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACG,IAAI;AAAA,MACJ;AAAA,MACA,sBAAsB;AAAA,MACtB,WAAS;AAAA,MACT,MAAK;AAAA,MACL;AAAA,MACA,OAAO;AAAA,MACP;AAAA,MACA;AAAA,MACA,eAAe,EAAE,gBAAgB;AAAA,MACjC,aAAa,EAAE,qBAAqB;AAAA,MACpC,gBAAgB;AAAA,MAChB,sBAAsB,CAAC,QAAQ,kBAAkB,OAAO,SAAS,cAAc;AAAA,MAC/E,eAAe,CAAC,YAAY,EAAC,iBAAgB;AACzC,cAAM,aAAa,YAAY,UAAU;AACzC,cAAM,kBAAkB,aAClB,WAAW,OAAO,CAAC,WAAW,YAAY,cAAc,MAAM,CAAC,EAAE,SAAS,UAAU,CAAC,IACrF;AACN,eAAO,gBAAgB,MAAM,GAAG,mBAAmB;AAAA,MACvD;AAAA,MACA,UAAU,CAAC,OAAO,WAAW,SAAS,WAAW,OAAO,IAAI,CAAC,iBAAiB,aAAa,IAAI,IAAI,QAAQ,QAAQ,EAAE;AAAA,MACrH;AAAA,MACA,aAAa,CAAC,eACV,oBAAC,WAAA,EAAW,GAAG,YAAY,MAAY,UAAoB,aAA0B,OAAO,QAAQ,SAAS,GAAG,YAAY,aAAa,WAAA,CAAY;AAAA,IAAA;AAAA,EAAA,GAGjK;AAER;"}
@@ -12,6 +12,7 @@ import { translateValidationMessage } from "../forms/validationMessages.js";
12
12
  import { contrastOnWhite, MIN_CONTRAST, readableVariantOf, HEX_COLOR } from "./colorContrast.js";
13
13
  import FieldFrame from "./FieldFrame.js";
14
14
  import { piecePart, usePieceProps } from "../theme/themePieces.js";
15
+ import { warnUnknownProps } from "../toolkit/unknownProps.js";
15
16
  const THEME_NAME = "CalcitaColorInput";
16
17
  const ColorSwatch = piecePart(THEME_NAME, "Swatch", Box);
17
18
  const ColorNativeInput = piecePart(THEME_NAME, "NativeInput", Box);
@@ -32,8 +33,10 @@ function ColorInput(props) {
32
33
  checkContrast = true,
33
34
  disabled = false,
34
35
  inputRef,
35
- texts
36
+ texts,
37
+ ...unknownProps
36
38
  } = usePieceProps(props, THEME_NAME);
39
+ warnUnknownProps(THEME_NAME, unknownProps);
37
40
  const { t } = useToolkitTranslation("common", texts);
38
41
  const generatedId = useId();
39
42
  const inputId = `${name ?? "color"}-${generatedId}`;
@@ -1 +1 @@
1
- {"version":3,"file":"ColorInput.js","sources":["../../src/inputs/ColorInput.jsx"],"sourcesContent":["// Campo de COLOR suelto (viene de «Identidad de documentos» de PLD): la muestra redonda (el «color well» de Apple, que abre\n// el selector del sistema) dentro del campo del texto #RRGGBB, el contraste contra el blanco mientras se elige y, si el color casi no se leería, el mismo tono más oscuro con su botón\n// para usarlo.\n//\n// Para lo que se imprime o se lee sobre blanco (el color de una marca en un comprobante). El color llega al formulario en\n// mayúsculas (#2B3A67); '' = sin color.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaColorInput con defaultProps y\n// styleOverrides de sus partes: swatch, nativeInput.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useId} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Alert from '@mui/material/Alert';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport InputAdornment from '@mui/material/InputAdornment';\nimport Stack from '@mui/material/Stack';\nimport TextField from '@mui/material/TextField';\n\nimport {translateValidationMessage} from '@/forms/validationMessages.js';\nimport {HEX_COLOR, MIN_CONTRAST, contrastOnWhite, readableVariantOf} from '@/inputs/colorContrast.js';\nimport FieldFrame from '@/inputs/FieldFrame.jsx';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaColorInput)\nconst THEME_NAME = 'CalcitaColorInput';\nconst ColorSwatch = piecePart(THEME_NAME, 'Swatch', Box);\nconst ColorNativeInput = piecePart(THEME_NAME, 'NativeInput', Box);\n\n// #RRGGBB\nconst COLOR_LENGTH = 7;\n// el color que enseña el selector del navegador mientras el texto no es un color\nconst NEUTRAL_COLOR = '#000000';\nconst RATIO_DECIMALS = 1;\n// la muestra redonda (px) y su orilla fina en claro y en oscuro\nconst SWATCH_PIXELS = 24;\nconst SWATCH_EDGE = Object.freeze({light: 'rgba(15, 23, 42, 0.18)', dark: 'rgba(255, 255, 255, 0.24)'});\n\n/**\n * @param {object} props\n * @param {string} [props.name]\n * @param {string} props.label ya traducido\n * @param {string|null|undefined} props.value '#RRGGBB' o ''\n * @param {(color: string) => void} props.onChange recibe el color en mayúsculas\n * @param {() => void} [props.onBlur]\n * @param {string|{key: string, values?: object}} [props.error]\n * @param {string} [props.helperText] cuando no hay color\n * @param {boolean} [props.checkContrast] avisa si casi no se lee sobre blanco (por omisión sí)\n * @param {boolean} [props.disabled]\n * @param {import('react').Ref<HTMLInputElement>} [props.inputRef]\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nexport default function ColorInput(props) {\n // lo que el proyecto puso en su tema (components.CalcitaColorInput.defaultProps) y encima lo que llega aquí\n const {\n name, label, value, onChange, onBlur, error, helperText = ' ', checkContrast = true, disabled = false, inputRef, texts,\n} = usePieceProps(props, THEME_NAME);\n const {t} = useToolkitTranslation('common', texts);\n const generatedId = useId();\n const inputId = `${name ?? 'color'}-${generatedId}`;\n const color = value ?? '';\n const isValidColor = HEX_COLOR.test(color);\n const contrastRatio = isValidColor ? contrastOnWhite(color) : null;\n const suggestion = checkContrast && isValidColor && contrastRatio < MIN_CONTRAST ? readableVariantOf(color) : null;\n const errorText = translateValidationMessage(t, error);\n const contrastText = contrastRatio && checkContrast ? t('form.colorContrast', {ratio: contrastRatio.toFixed(RATIO_DECIMALS)}) : helperText;\n const choose = (nextColor) => onChange(nextColor.toUpperCase());\n\n return (\n <FieldFrame inputId={inputId} label={label}>\n <Stack spacing={1}>\n {/* un solo campo: la muestra redonda al inicio (el «color well» de Apple) y el código al lado. Picar la muestra\n abre el selector de colores del sistema (un input type=color invisible encima de ella). Antes eran dos cajas\n sueltas (el selector del navegador en la suya y el código en otra) y se veían como piezas sueltas */}\n <TextField\n name={name}\n id={inputId}\n inputRef={inputRef}\n size=\"small\"\n fullWidth\n value={color}\n disabled={disabled}\n placeholder=\"#RRGGBB\"\n onChange={(event) => choose(event.target.value)}\n onBlur={onBlur}\n error={Boolean(errorText)}\n helperText={errorText ?? contrastText}\n slotProps={{\n input: {\n startAdornment: (\n <InputAdornment position=\"start\">\n <ColorSwatch component=\"label\"\n ownSx={(theme) => ({\n position: 'relative', display: 'block', flexShrink: 0, width: SWATCH_PIXELS, height: SWATCH_PIXELS,\n borderRadius: '50%', cursor: disabled ? 'default' : 'pointer',\n backgroundColor: isValidColor ? color : 'transparent',\n // la orilla de la muestra: que se vea aunque el color sea el del fondo (blanco en claro, negro en oscuro)\n boxShadow: `inset 0 0 0 1px ${SWATCH_EDGE.light}`,\n ...theme.applyStyles('dark', {boxShadow: `inset 0 0 0 1px ${SWATCH_EDGE.dark}`}),\n })}>\n <ColorNativeInput component=\"input\" type=\"color\" value={isValidColor ? color : NEUTRAL_COLOR} disabled={disabled}\n aria-label={t('form.pickColor')} onChange={(event) => choose(event.target.value)}\n ownSx={{position: 'absolute', inset: 0, width: '100%', height: '100%', opacity: 0, border: 0, p: 0, cursor: 'inherit'}} />\n </ColorSwatch>\n </InputAdornment>\n ),\n },\n htmlInput: {maxLength: COLOR_LENGTH, spellCheck: false, style: {fontVariantNumeric: 'tabular-nums'}},\n }}\n />\n {suggestion && (\n <Alert severity=\"warning\"\n action={<Button color=\"inherit\" size=\"small\" onClick={() => choose(suggestion)}>{t('form.colorUseSuggestion', {color: suggestion})}</Button>}>\n {t('form.colorLowContrast', {suggestion})}\n </Alert>\n )}\n </Stack>\n </FieldFrame>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ColorInput.js","sources":["../../src/inputs/ColorInput.jsx"],"sourcesContent":["// Campo de COLOR suelto (viene de «Identidad de documentos» de PLD): la muestra redonda (el «color well» de Apple, que abre\n// el selector del sistema) dentro del campo del texto #RRGGBB, el contraste contra el blanco mientras se elige y, si el color casi no se leería, el mismo tono más oscuro con su botón\n// para usarlo.\n//\n// Para lo que se imprime o se lee sobre blanco (el color de una marca en un comprobante). El color llega al formulario en\n// mayúsculas (#2B3A67); '' = sin color.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaColorInput con defaultProps y\n// styleOverrides de sus partes: swatch, nativeInput.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useId} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Alert from '@mui/material/Alert';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport InputAdornment from '@mui/material/InputAdornment';\nimport Stack from '@mui/material/Stack';\nimport TextField from '@mui/material/TextField';\n\nimport {translateValidationMessage} from '@/forms/validationMessages.js';\nimport {HEX_COLOR, MIN_CONTRAST, contrastOnWhite, readableVariantOf} from '@/inputs/colorContrast.js';\nimport FieldFrame from '@/inputs/FieldFrame.jsx';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaColorInput)\nconst THEME_NAME = 'CalcitaColorInput';\nconst ColorSwatch = piecePart(THEME_NAME, 'Swatch', Box);\nconst ColorNativeInput = piecePart(THEME_NAME, 'NativeInput', Box);\n\n// #RRGGBB\nconst COLOR_LENGTH = 7;\n// el color que enseña el selector del navegador mientras el texto no es un color\nconst NEUTRAL_COLOR = '#000000';\nconst RATIO_DECIMALS = 1;\n// la muestra redonda (px) y su orilla fina en claro y en oscuro\nconst SWATCH_PIXELS = 24;\nconst SWATCH_EDGE = Object.freeze({light: 'rgba(15, 23, 42, 0.18)', dark: 'rgba(255, 255, 255, 0.24)'});\n\n/**\n * @param {object} props\n * @param {string} [props.name]\n * @param {string} props.label ya traducido\n * @param {string|null|undefined} props.value '#RRGGBB' o ''\n * @param {(color: string) => void} props.onChange recibe el color en mayúsculas\n * @param {() => void} [props.onBlur]\n * @param {string|{key: string, values?: object}} [props.error]\n * @param {string} [props.helperText] cuando no hay color\n * @param {boolean} [props.checkContrast] avisa si casi no se lee sobre blanco (por omisión sí)\n * @param {boolean} [props.disabled]\n * @param {import('react').Ref<HTMLInputElement>} [props.inputRef]\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nexport default function ColorInput(props) {\n // lo que el proyecto puso en su tema (components.CalcitaColorInput.defaultProps) y encima lo que llega aquí\n const {\n name, label, value, onChange, onBlur, error, helperText = ' ', checkContrast = true, disabled = false, inputRef, texts,\n ...unknownProps\n} = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n const generatedId = useId();\n const inputId = `${name ?? 'color'}-${generatedId}`;\n const color = value ?? '';\n const isValidColor = HEX_COLOR.test(color);\n const contrastRatio = isValidColor ? contrastOnWhite(color) : null;\n const suggestion = checkContrast && isValidColor && contrastRatio < MIN_CONTRAST ? readableVariantOf(color) : null;\n const errorText = translateValidationMessage(t, error);\n const contrastText = contrastRatio && checkContrast ? t('form.colorContrast', {ratio: contrastRatio.toFixed(RATIO_DECIMALS)}) : helperText;\n const choose = (nextColor) => onChange(nextColor.toUpperCase());\n\n return (\n <FieldFrame inputId={inputId} label={label}>\n <Stack spacing={1}>\n {/* un solo campo: la muestra redonda al inicio (el «color well» de Apple) y el código al lado. Picar la muestra\n abre el selector de colores del sistema (un input type=color invisible encima de ella). Antes eran dos cajas\n sueltas (el selector del navegador en la suya y el código en otra) y se veían como piezas sueltas */}\n <TextField\n name={name}\n id={inputId}\n inputRef={inputRef}\n size=\"small\"\n fullWidth\n value={color}\n disabled={disabled}\n placeholder=\"#RRGGBB\"\n onChange={(event) => choose(event.target.value)}\n onBlur={onBlur}\n error={Boolean(errorText)}\n helperText={errorText ?? contrastText}\n slotProps={{\n input: {\n startAdornment: (\n <InputAdornment position=\"start\">\n <ColorSwatch component=\"label\"\n ownSx={(theme) => ({\n position: 'relative', display: 'block', flexShrink: 0, width: SWATCH_PIXELS, height: SWATCH_PIXELS,\n borderRadius: '50%', cursor: disabled ? 'default' : 'pointer',\n backgroundColor: isValidColor ? color : 'transparent',\n // la orilla de la muestra: que se vea aunque el color sea el del fondo (blanco en claro, negro en oscuro)\n boxShadow: `inset 0 0 0 1px ${SWATCH_EDGE.light}`,\n ...theme.applyStyles('dark', {boxShadow: `inset 0 0 0 1px ${SWATCH_EDGE.dark}`}),\n })}>\n <ColorNativeInput component=\"input\" type=\"color\" value={isValidColor ? color : NEUTRAL_COLOR} disabled={disabled}\n aria-label={t('form.pickColor')} onChange={(event) => choose(event.target.value)}\n ownSx={{position: 'absolute', inset: 0, width: '100%', height: '100%', opacity: 0, border: 0, p: 0, cursor: 'inherit'}} />\n </ColorSwatch>\n </InputAdornment>\n ),\n },\n htmlInput: {maxLength: COLOR_LENGTH, spellCheck: false, style: {fontVariantNumeric: 'tabular-nums'}},\n }}\n />\n {suggestion && (\n <Alert severity=\"warning\"\n action={<Button color=\"inherit\" size=\"small\" onClick={() => choose(suggestion)}>{t('form.colorUseSuggestion', {color: suggestion})}</Button>}>\n {t('form.colorLowContrast', {suggestion})}\n </Alert>\n )}\n </Stack>\n </FieldFrame>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -4,7 +4,9 @@ import Stack from "@mui/material/Stack";
4
4
  import Typography from "@mui/material/Typography";
5
5
  import { FormRow } from "./FormSection.js";
6
6
  import { useIsInsideFormSection } from "./formLayoutContext.js";
7
- function FieldFrame({ inputId, labelId, label, children }) {
7
+ import { warnUnknownProps } from "../toolkit/unknownProps.js";
8
+ function FieldFrame({ inputId, labelId, label, children, ...unknownProps }) {
9
+ warnUnknownProps("FieldFrame", unknownProps);
8
10
  const isRow = useIsInsideFormSection();
9
11
  if (isRow) {
10
12
  return /* @__PURE__ */ jsx(FormRow, { label, htmlFor: inputId, labelId, children });
@@ -1 +1 @@
1
- {"version":3,"file":"FieldFrame.js","sources":["../../src/inputs/FieldFrame.jsx"],"sourcesContent":["// El marco de todos los campos del paquete. Fuera de una FormSection, la etiqueta va ARRIBA del campo, ligada a él por su\n// id; dentro de una FormSection (el formulario agrupado de Apple), el campo se vuelve un renglón: la etiqueta a la\n// izquierda y el valor a la derecha (FormRow).\n//\n// La etiqueta va afuera y no flotando adentro del campo: se lee igual con el campo vacío o lleno, y no brinca al escribir.\n//\n// Genérico del paquete: function (regla de la casa).\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\n\nimport {FormRow} from '@/inputs/FormSection.jsx';\nimport {useIsInsideFormSection} from '@/inputs/formLayoutContext.js';\n\n/**\n * @param {object} props\n * @param {string} props.inputId el id del campo (la etiqueta lo nombra para el lector de pantalla)\n * @param {string} [props.labelId] el id de la etiqueta, para un campo que no es un <input> (la lista desplegable de MUI es un\n * div: `for` no la une con su etiqueta y el campo la nombra con aria-labelledby)\n * @param {import('react').ReactNode} props.label ya traducida\n * @param {import('react').ReactNode} props.children el campo\n */\nexport default function FieldFrame({inputId, labelId, label, children}) {\n const isRow = useIsInsideFormSection();\n\n if (isRow) {\n return <FormRow label={label} htmlFor={inputId} labelId={labelId}>{children}</FormRow>;\n }\n\n return (\n <Stack spacing={0.75}>\n <Typography component=\"label\" id={labelId} htmlFor={inputId} variant=\"overline\" color=\"text.secondary\">{label}</Typography>\n {children}\n </Stack>\n );\n}\n"],"names":[],"mappings":";;;;;;AAqBA,SAAwB,WAAW,EAAC,SAAS,SAAS,OAAO,YAAW;AACpE,QAAM,QAAQ,uBAAA;AAEd,MAAI,OAAO;AACP,+BAAQ,SAAA,EAAQ,OAAc,SAAS,SAAS,SAAmB,UAAS;AAAA,EAChF;AAEA,SACI,qBAAC,OAAA,EAAM,SAAS,MACZ,UAAA;AAAA,IAAA,oBAAC,YAAA,EAAW,WAAU,SAAQ,IAAI,SAAS,SAAS,SAAS,SAAQ,YAAW,OAAM,kBAAkB,UAAA,OAAM;AAAA,IAC7G;AAAA,EAAA,GACL;AAER;"}
1
+ {"version":3,"file":"FieldFrame.js","sources":["../../src/inputs/FieldFrame.jsx"],"sourcesContent":["// El marco de todos los campos del paquete. Fuera de una FormSection, la etiqueta va ARRIBA del campo, ligada a él por su\n// id; dentro de una FormSection (el formulario agrupado de Apple), el campo se vuelve un renglón: la etiqueta a la\n// izquierda y el valor a la derecha (FormRow).\n//\n// La etiqueta va afuera y no flotando adentro del campo: se lee igual con el campo vacío o lleno, y no brinca al escribir.\n//\n// Genérico del paquete: function (regla de la casa).\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\n\nimport {FormRow} from '@/inputs/FormSection.jsx';\nimport {useIsInsideFormSection} from '@/inputs/formLayoutContext.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n/**\n * @param {object} props\n * @param {string} props.inputId el id del campo (la etiqueta lo nombra para el lector de pantalla)\n * @param {string} [props.labelId] el id de la etiqueta, para un campo que no es un <input> (la lista desplegable de MUI es un\n * div: `for` no la une con su etiqueta y el campo la nombra con aria-labelledby)\n * @param {import('react').ReactNode} props.label ya traducida\n * @param {import('react').ReactNode} props.children el campo\n */\nexport default function FieldFrame({inputId, labelId, label, children, ...unknownProps}) {\n warnUnknownProps('FieldFrame', unknownProps);\n const isRow = useIsInsideFormSection();\n\n if (isRow) {\n return <FormRow label={label} htmlFor={inputId} labelId={labelId}>{children}</FormRow>;\n }\n\n return (\n <Stack spacing={0.75}>\n <Typography component=\"label\" id={labelId} htmlFor={inputId} variant=\"overline\" color=\"text.secondary\">{label}</Typography>\n {children}\n </Stack>\n );\n}\n"],"names":[],"mappings":";;;;;;;AAsBA,SAAwB,WAAW,EAAC,SAAS,SAAS,OAAO,UAAU,GAAG,gBAAe;AACrF,mBAAiB,cAAc,YAAY;AAC3C,QAAM,QAAQ,uBAAA;AAEd,MAAI,OAAO;AACP,+BAAQ,SAAA,EAAQ,OAAc,SAAS,SAAS,SAAmB,UAAS;AAAA,EAChF;AAEA,SACI,qBAAC,OAAA,EAAM,SAAS,MACZ,UAAA;AAAA,IAAA,oBAAC,YAAA,EAAW,WAAU,SAAQ,IAAI,SAAS,SAAS,SAAS,SAAQ,YAAW,OAAM,kBAAkB,UAAA,OAAM;AAAA,IAC7G;AAAA,EAAA,GACL;AAER;"}
@@ -7,6 +7,7 @@ import Typography from "@mui/material/Typography";
7
7
  import { styled, useThemeProps } from "@mui/material/styles";
8
8
  import { FormSectionContext } from "./formLayoutContext.js";
9
9
  import { fieldVariablesOf, accentTextStylesOf, PANEL_RADIUS, fieldWellFromVariablesOf } from "../theme/glassSurface.js";
10
+ import { warnUnknownProps } from "../toolkit/unknownProps.js";
10
11
  const FORM_ROW_CLASS = "calcita-form-row";
11
12
  const ROW_MIN_HEIGHT_PIXELS = 52;
12
13
  const ROW_PADDING_BLOCK_PIXELS = 8;
@@ -85,14 +86,16 @@ const FormSectionCard = styled(Paper, { name: SECTION_THEME_NAME, slot: "Card" }
85
86
  }));
86
87
  const FormSectionFooter = styled(Typography, { name: SECTION_THEME_NAME, slot: "Footer" })(({ theme }) => theme.unstable_sx({ px: 2, mt: 0.75 }));
87
88
  function FormRow(props) {
88
- const { label, htmlFor, labelId, children } = useThemeProps({ props, name: ROW_THEME_NAME });
89
+ const { label, htmlFor, labelId, children, ...unknownProps } = useThemeProps({ props, name: ROW_THEME_NAME });
90
+ warnUnknownProps(ROW_THEME_NAME, unknownProps);
89
91
  return /* @__PURE__ */ jsxs(FormRowRoot, { className: FORM_ROW_CLASS, children: [
90
92
  /* @__PURE__ */ jsx(FormRowLabel, { component: "label", id: labelId, htmlFor, variant: "body1", children: label }),
91
93
  /* @__PURE__ */ jsx(FormRowControl, { children })
92
94
  ] });
93
95
  }
94
96
  function FormSection(props) {
95
- const { title, footer, children } = useThemeProps({ props, name: SECTION_THEME_NAME });
97
+ const { title, footer, children, ...unknownProps } = useThemeProps({ props, name: SECTION_THEME_NAME });
98
+ warnUnknownProps(SECTION_THEME_NAME, unknownProps);
96
99
  const titleId = useId();
97
100
  return /* @__PURE__ */ jsx(FormSectionContext, { value: true, children: /* @__PURE__ */ jsxs(FormSectionRoot, { component: "section", "aria-labelledby": title ? titleId : void 0, children: [
98
101
  title && /* @__PURE__ */ jsx(FormSectionTitle, { id: titleId, variant: "subtitle2", component: "h2", color: "text.secondary", children: title }),