@stackcraftmx/calcita 0.54.0 → 0.56.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.
- package/README.md +54 -4
- package/dist/api/createApiClient.js +11 -3
- package/dist/api/createApiClient.js.map +1 -1
- package/dist/appShell/AppLayout.js +5 -1
- package/dist/appShell/AppLayout.js.map +1 -1
- package/dist/appShell/MobileTabBar.js +12 -1
- package/dist/appShell/MobileTabBar.js.map +1 -1
- package/dist/appShell/SideMenu.js +3 -1
- package/dist/appShell/SideMenu.js.map +1 -1
- package/dist/appShell/SideMenuEntry.js +27 -2
- package/dist/appShell/SideMenuEntry.js.map +1 -1
- package/dist/downloads/downloadQueue.js +3 -1
- package/dist/downloads/downloadQueue.js.map +1 -1
- package/dist/downloads/downloadsMachine.js +8 -0
- package/dist/downloads/downloadsMachine.js.map +1 -1
- package/dist/forms/FormWizard.js +22 -17
- package/dist/forms/FormWizard.js.map +1 -1
- package/dist/hookForm/FormWizard.js +22 -17
- package/dist/hookForm/FormWizard.js.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/inputs/FormSection.js +40 -13
- package/dist/inputs/FormSection.js.map +1 -1
- package/dist/inputs/WizardLayout.js +25 -4
- package/dist/inputs/WizardLayout.js.map +1 -1
- package/dist/inputs/formLayoutContext.js +1 -1
- package/dist/inputs/formLayoutContext.js.map +1 -1
- package/dist/inputs/useWizardSteps.js +3 -0
- package/dist/inputs/useWizardSteps.js.map +1 -1
- package/dist/types/api/createApiClient.d.ts +10 -0
- package/dist/types/appShell/AppLayout.d.ts +1 -0
- package/dist/types/appShell/MobileTabBar.d.ts +1 -0
- package/dist/types/appShell/SideMenu.d.ts +1 -0
- package/dist/types/appShell/SideMenuEntry.d.ts +6 -0
- package/dist/types/downloads/downloadQueue.d.ts +5 -1
- package/dist/types/forms/FormWizard.d.ts +4 -0
- package/dist/types/hookForm/FormWizard.d.ts +4 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/inputs/FormSection.d.ts +14 -0
- package/dist/types/inputs/WizardLayout.d.ts +1 -0
- package/dist/types/inputs/formLayoutContext.d.ts +8 -7
- package/dist/types/inputs/useWizardSteps.d.ts +2 -1
- package/dist/upload/FileUploadInput.js +1 -1
- package/dist/upload/FileUploadInput.js.map +1 -1
- package/package.json +1 -1
package/dist/forms/FormWizard.js
CHANGED
|
@@ -57,23 +57,28 @@ function FormWizard({
|
|
|
57
57
|
setSkippedKeys((currentKeys) => /* @__PURE__ */ new Set([...currentKeys, activeStep.key]));
|
|
58
58
|
setActiveIndex((currentIndex) => currentIndex + 1);
|
|
59
59
|
};
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
60
|
+
return (
|
|
61
|
+
// un paso con su motivo para no avanzar (steps[].nextDisabledReason): «Siguiente» apagado y, en el último, también el de
|
|
62
|
+
// enviar; este sí con disabled de verdad, así un Enter en un campo tampoco envía
|
|
63
|
+
/* @__PURE__ */ jsx(
|
|
64
|
+
WizardLayout,
|
|
65
|
+
{
|
|
66
|
+
steps,
|
|
67
|
+
activeIndex,
|
|
68
|
+
skippedKeys,
|
|
69
|
+
isSubmitting,
|
|
70
|
+
orientation,
|
|
71
|
+
hideBackOnFirstStep,
|
|
72
|
+
nextDisabledReason: activeStep.nextDisabledReason,
|
|
73
|
+
onBack: () => setActiveIndex((currentIndex) => currentIndex - 1),
|
|
74
|
+
onNext: goNext,
|
|
75
|
+
onSkip: skipStep,
|
|
76
|
+
onStepSelect: selectStep,
|
|
77
|
+
submitButton: /* @__PURE__ */ jsx(SubmitButton, { disabled: Boolean(activeStep.nextDisabledReason), children: submitLabel }),
|
|
78
|
+
problemAlert: /* @__PURE__ */ jsx(FormProblemAlert, {}),
|
|
79
|
+
texts
|
|
80
|
+
}
|
|
81
|
+
)
|
|
77
82
|
);
|
|
78
83
|
}
|
|
79
84
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormWizard.js","sources":["../../src/forms/FormWizard.jsx"],"sourcesContent":["// Formulario por pasos (wizard) con formik: un solo formulario partido en pasos, con pasos opcionales que se pueden omitir.\n//\n// Cómo se porta, para que la pantalla solo declare sus pasos:\n// - «Siguiente» revisa SOLO los campos del paso (fields) y no avanza si alguno tiene error; los marca como tocados para\n// que se vea por qué;\n// - «Omitir este paso» (solo en los opcionales) regresa sus campos a como empezaron: omitir es no dar ese dato, no dejar\n// 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), el wizard\n// regresa solo a ese paso: un error que no se ve no se puede corregir;\n// - para editar un registro (se entra a corregir un dato suelto): initialStepKey abre en el paso que diga el servidor,\n// orientation=\"vertical\" pone los campos dentro de su paso, y stepSelection deja pulsar el título de otro paso para\n// saltar directo (free) o solo si el paso abierto no tiene errores (checked). onStepChange avisa cada cambio de paso.\n//\n// Va DENTRO de <Formik> y <Form>:\n// <Formik initialValues={…} validationSchema={…} onSubmit={…}>\n// <Form noValidate>\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// </Formik>\n//\n// Lo que se ve vive en WizardLayout (inputs/), el mismo del FormWizard de react-hook-form. Los textos («Siguiente»,\n// «Anterior»…) 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 {getIn, useFormikContext} from 'formik';\n\nimport FormProblemAlert from '@/forms/FormProblemAlert.jsx';\nimport SubmitButton from '@/forms/SubmitButton.jsx';\nimport {useWizardSteps} from '@/inputs/useWizardSteps.js';\nimport WizardLayout from '@/inputs/WizardLayout.jsx';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n/**\n * El primer paso que tiene un campo con error; -1 si ninguno.\n *\n * @param {Array<{fields?: string[]}>} steps\n * @param {object} errors los de formik\n * @returns {number}\n */\nfunction firstStepWithErrorOf(steps, errors) {\n return steps.findIndex((step) => (step.fields ?? []).some((field) => getIn(errors, field)));\n}\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 {boolean} [props.steps[].completed] lleva la palomita (cuando los títulos se pulsan, solo si la pantalla lo dice)\n * @param {string} props.submitLabel ya traducido: el botón del último paso\n * @param {'horizontal'|'vertical'} [props.orientation] uno de WIZARD_ORIENTATION; por omisión horizontal\n * @param {boolean} [props.hideBackOnFirstStep] en el primer paso no se pinta «Anterior»; por omisión sale apagado, como antes\n * @param {string} [props.initialStepKey] la llave del paso en el que se abre (la que diga el servidor); por omisión el primero\n * @param {'none'|'free'|'checked'} [props.stepSelection] uno de WIZARD_STEP_SELECTION: si el título de otro paso se pulsa y qué\n * revisa antes; por omisión none (se avanza con «Siguiente», como siempre)\n * @param {(stepKey: string, index: number) => void} [props.onStepChange] cada vez que cambia el paso abierto\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({\n steps, submitLabel, orientation, hideBackOnFirstStep, initialStepKey, stepSelection, onStepChange, texts, ...unknownProps\n}) {\n warnUnknownProps('FormWizard', unknownProps);\n const {validateForm, setFieldTouched, setFieldValue, initialValues, errors, isSubmitting} = useFormikContext();\n const [skippedKeys, setSkippedKeys] = useState(() => new Set());\n const wasSubmitting = useRef(false);\n\n // revisa SOLO los campos de un paso y los marca como tocados para que se vea por qué no avanza\n const isStepValid = async (index) => {\n const stepFields = steps[index].fields ?? [];\n const formErrors = await validateForm();\n stepFields.forEach((field) => setFieldTouched(field, true, false));\n return !stepFields.some((field) => getIn(formErrors, field));\n };\n const {activeIndex, setActiveIndex, selectStep} = useWizardSteps({steps, initialStepKey, stepSelection, onStepChange, isStepValid});\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)\n useEffect(() => {\n if (wasSubmitting.current && !isSubmitting) {\n const errorStepIndex = firstStepWithErrorOf(steps, errors);\n if (errorStepIndex >= 0) {\n setActiveIndex(errorStepIndex);\n }\n }\n wasSubmitting.current = isSubmitting;\n }, [isSubmitting, errors, steps]);\n\n const goNext = async () => {\n // el paso tiene errores: se queda ahí para que se corrijan\n if (!(await isStepValid(activeIndex))) {\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 setFieldValue(field, getIn(initialValues, field), false);\n setFieldTouched(field, false, false);\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} orientation={orientation} hideBackOnFirstStep={hideBackOnFirstStep}\n onBack={() => setActiveIndex((currentIndex) => currentIndex - 1)}
|
|
1
|
+
{"version":3,"file":"FormWizard.js","sources":["../../src/forms/FormWizard.jsx"],"sourcesContent":["// Formulario por pasos (wizard) con formik: un solo formulario partido en pasos, con pasos opcionales que se pueden omitir.\n//\n// Cómo se porta, para que la pantalla solo declare sus pasos:\n// - «Siguiente» revisa SOLO los campos del paso (fields) y no avanza si alguno tiene error; los marca como tocados para\n// que se vea por qué;\n// - «Omitir este paso» (solo en los opcionales) regresa sus campos a como empezaron: omitir es no dar ese dato, no dejar\n// 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), el wizard\n// regresa solo a ese paso: un error que no se ve no se puede corregir;\n// - para editar un registro (se entra a corregir un dato suelto): initialStepKey abre en el paso que diga el servidor,\n// orientation=\"vertical\" pone los campos dentro de su paso, y stepSelection deja pulsar el título de otro paso para\n// saltar directo (free) o solo si el paso abierto no tiene errores (checked). onStepChange avisa cada cambio de paso.\n//\n// Va DENTRO de <Formik> y <Form>:\n// <Formik initialValues={…} validationSchema={…} onSubmit={…}>\n// <Form noValidate>\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// </Formik>\n//\n// Lo que se ve vive en WizardLayout (inputs/), el mismo del FormWizard de react-hook-form. Los textos («Siguiente»,\n// «Anterior»…) 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 {getIn, useFormikContext} from 'formik';\n\nimport FormProblemAlert from '@/forms/FormProblemAlert.jsx';\nimport SubmitButton from '@/forms/SubmitButton.jsx';\nimport {useWizardSteps} from '@/inputs/useWizardSteps.js';\nimport WizardLayout from '@/inputs/WizardLayout.jsx';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n/**\n * El primer paso que tiene un campo con error; -1 si ninguno.\n *\n * @param {Array<{fields?: string[]}>} steps\n * @param {object} errors los de formik\n * @returns {number}\n */\nfunction firstStepWithErrorOf(steps, errors) {\n return steps.findIndex((step) => (step.fields ?? []).some((field) => getIn(errors, field)));\n}\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[].nextDisabledReason] ya traducido: el paso todavía no se puede dejar («Faltan 2\n * documentos»). «Siguiente» se ve apagado con el motivo en un aviso (escrito, en el teléfono); en el último paso se apaga\n * el de enviar. Con stepSelection checked tampoco se salta a un paso de adelante. Sin él, como siempre\n * @param {import('react').ReactNode} props.steps[].content los campos del paso\n * @param {boolean} [props.steps[].completed] lleva la palomita (cuando los títulos se pulsan, solo si la pantalla lo dice)\n * @param {string} props.submitLabel ya traducido: el botón del último paso\n * @param {'horizontal'|'vertical'} [props.orientation] uno de WIZARD_ORIENTATION; por omisión horizontal\n * @param {boolean} [props.hideBackOnFirstStep] en el primer paso no se pinta «Anterior»; por omisión sale apagado, como antes\n * @param {string} [props.initialStepKey] la llave del paso en el que se abre (la que diga el servidor); por omisión el primero\n * @param {'none'|'free'|'checked'} [props.stepSelection] uno de WIZARD_STEP_SELECTION: si el título de otro paso se pulsa y qué\n * revisa antes; por omisión none (se avanza con «Siguiente», como siempre)\n * @param {(stepKey: string, index: number) => void} [props.onStepChange] cada vez que cambia el paso abierto\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({\n steps, submitLabel, orientation, hideBackOnFirstStep, initialStepKey, stepSelection, onStepChange, texts, ...unknownProps\n}) {\n warnUnknownProps('FormWizard', unknownProps);\n const {validateForm, setFieldTouched, setFieldValue, initialValues, errors, isSubmitting} = useFormikContext();\n const [skippedKeys, setSkippedKeys] = useState(() => new Set());\n const wasSubmitting = useRef(false);\n\n // revisa SOLO los campos de un paso y los marca como tocados para que se vea por qué no avanza\n const isStepValid = async (index) => {\n const stepFields = steps[index].fields ?? [];\n const formErrors = await validateForm();\n stepFields.forEach((field) => setFieldTouched(field, true, false));\n return !stepFields.some((field) => getIn(formErrors, field));\n };\n const {activeIndex, setActiveIndex, selectStep} = useWizardSteps({steps, initialStepKey, stepSelection, onStepChange, isStepValid});\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)\n useEffect(() => {\n if (wasSubmitting.current && !isSubmitting) {\n const errorStepIndex = firstStepWithErrorOf(steps, errors);\n if (errorStepIndex >= 0) {\n setActiveIndex(errorStepIndex);\n }\n }\n wasSubmitting.current = isSubmitting;\n }, [isSubmitting, errors, steps]);\n\n const goNext = async () => {\n // el paso tiene errores: se queda ahí para que se corrijan\n if (!(await isStepValid(activeIndex))) {\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 setFieldValue(field, getIn(initialValues, field), false);\n setFieldTouched(field, false, false);\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys, activeStep.key]));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n return (\n // un paso con su motivo para no avanzar (steps[].nextDisabledReason): «Siguiente» apagado y, en el último, también el de\n // enviar; este sí con disabled de verdad, así un Enter en un campo tampoco envía\n <WizardLayout steps={steps} activeIndex={activeIndex} skippedKeys={skippedKeys} isSubmitting={isSubmitting} orientation={orientation} hideBackOnFirstStep={hideBackOnFirstStep}\n nextDisabledReason={activeStep.nextDisabledReason} onBack={() => setActiveIndex((currentIndex) => currentIndex - 1)}\n onNext={goNext} onSkip={skipStep} onStepSelect={selectStep}\n submitButton={<SubmitButton disabled={Boolean(activeStep.nextDisabledReason)}>{submitLabel}</SubmitButton>}\n problemAlert={<FormProblemAlert />} texts={texts} />\n );\n}\n"],"names":[],"mappings":";;;;;;;;;AA4CA,SAAS,qBAAqB,OAAO,QAAQ;AACzC,SAAO,MAAM,UAAU,CAAC,UAAU,KAAK,UAAU,CAAA,GAAI,KAAK,CAAC,UAAU,MAAM,QAAQ,KAAK,CAAC,CAAC;AAC9F;AAuBA,SAAwB,WAAW;AAAA,EAC/B;AAAA,EAAO;AAAA,EAAa;AAAA,EAAa;AAAA,EAAqB;AAAA,EAAgB;AAAA,EAAe;AAAA,EAAc;AAAA,EAAO,GAAG;AACjH,GAAG;AACC,mBAAiB,cAAc,YAAY;AAC3C,QAAM,EAAC,cAAc,iBAAiB,eAAe,eAAe,QAAQ,aAAA,IAAgB,iBAAA;AAC5F,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,MAAM,oBAAI,KAAK;AAC9D,QAAM,gBAAgB,OAAO,KAAK;AAGlC,QAAM,cAAc,OAAO,UAAU;AACjC,UAAM,aAAa,MAAM,KAAK,EAAE,UAAU,CAAA;AAC1C,UAAM,aAAa,MAAM,aAAA;AACzB,eAAW,QAAQ,CAAC,UAAU,gBAAgB,OAAO,MAAM,KAAK,CAAC;AACjE,WAAO,CAAC,WAAW,KAAK,CAAC,UAAU,MAAM,YAAY,KAAK,CAAC;AAAA,EAC/D;AACA,QAAM,EAAC,aAAa,gBAAgB,WAAA,IAAc,eAAe,EAAC,OAAO,gBAAgB,eAAe,cAAc,YAAA,CAAY;AAClI,QAAM,aAAa,MAAM,WAAW;AAIpC,YAAU,MAAM;AACZ,QAAI,cAAc,WAAW,CAAC,cAAc;AACxC,YAAM,iBAAiB,qBAAqB,OAAO,MAAM;AACzD,UAAI,kBAAkB,GAAG;AACrB,uBAAe,cAAc;AAAA,MACjC;AAAA,IACJ;AACA,kBAAc,UAAU;AAAA,EAC5B,GAAG,CAAC,cAAc,QAAQ,KAAK,CAAC;AAEhC,QAAM,SAAS,YAAY;AAEvB,QAAI,CAAE,MAAM,YAAY,WAAW,GAAI;AACnC;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,oBAAc,OAAO,MAAM,eAAe,KAAK,GAAG,KAAK;AACvD,sBAAgB,OAAO,OAAO,KAAK;AAAA,IACvC;AACA,mBAAe,CAAC,gBAAgB,oBAAI,IAAI,CAAC,GAAG,aAAa,WAAW,GAAG,CAAC,CAAC;AACzE,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA;AAAA;AAAA;AAAA,IAGI;AAAA,MAAC;AAAA,MAAA;AAAA,QAAa;AAAA,QAAc;AAAA,QAA0B;AAAA,QAA0B;AAAA,QAA4B;AAAA,QAA0B;AAAA,QACxH,oBAAoB,WAAW;AAAA,QAAoB,QAAQ,MAAM,eAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,QAClH,QAAQ;AAAA,QAAQ,QAAQ;AAAA,QAAU,cAAc;AAAA,QAChD,kCAAe,cAAA,EAAa,UAAU,QAAQ,WAAW,kBAAkB,GAAI,UAAA,aAAY;AAAA,QAC3F,kCAAe,kBAAA,EAAiB;AAAA,QAAI;AAAA,MAAA;AAAA,IAAA;AAAA;AAE1D;"}
|
|
@@ -52,23 +52,28 @@ function FormWizard({
|
|
|
52
52
|
setSkippedKeys((currentKeys) => /* @__PURE__ */ new Set([...currentKeys, activeStep.key]));
|
|
53
53
|
setActiveIndex((currentIndex) => currentIndex + 1);
|
|
54
54
|
};
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
55
|
+
return (
|
|
56
|
+
// un paso con su motivo para no avanzar (steps[].nextDisabledReason): «Siguiente» apagado y, en el último, también el de
|
|
57
|
+
// enviar; este sí con disabled de verdad, así un Enter en un campo tampoco envía
|
|
58
|
+
/* @__PURE__ */ jsx(
|
|
59
|
+
WizardLayout,
|
|
60
|
+
{
|
|
61
|
+
steps,
|
|
62
|
+
activeIndex,
|
|
63
|
+
skippedKeys,
|
|
64
|
+
isSubmitting,
|
|
65
|
+
orientation,
|
|
66
|
+
hideBackOnFirstStep,
|
|
67
|
+
nextDisabledReason: activeStep.nextDisabledReason,
|
|
68
|
+
onBack: () => setActiveIndex((currentIndex) => currentIndex - 1),
|
|
69
|
+
onNext: goNext,
|
|
70
|
+
onSkip: skipStep,
|
|
71
|
+
onStepSelect: selectStep,
|
|
72
|
+
submitButton: /* @__PURE__ */ jsx(SubmitButton, { disabled: Boolean(activeStep.nextDisabledReason), children: submitLabel }),
|
|
73
|
+
problemAlert: /* @__PURE__ */ jsx(FormProblemAlert, {}),
|
|
74
|
+
texts
|
|
75
|
+
}
|
|
76
|
+
)
|
|
72
77
|
);
|
|
73
78
|
}
|
|
74
79
|
export {
|
|
@@ -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// - para editar un registro (se entra a corregir un dato suelto): initialStepKey abre en el paso que diga el servidor,\n// orientation=\"vertical\" pone los campos dentro de su paso, y stepSelection deja pulsar el título de otro paso para\n// saltar directo (free) o solo si el paso abierto no tiene errores (checked). onStepChange avisa cada cambio de paso.\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 {useWizardSteps} from '@/inputs/useWizardSteps.js';\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 {boolean} [props.steps[].completed] lleva la palomita (cuando los títulos se pulsan, solo si la pantalla lo dice)\n * @param {string} props.submitLabel ya traducido: el botón del último paso\n * @param {'horizontal'|'vertical'} [props.orientation] uno de WIZARD_ORIENTATION; por omisión horizontal\n * @param {boolean} [props.hideBackOnFirstStep] en el primer paso no se pinta «Anterior»; por omisión sale apagado, como antes\n * @param {string} [props.initialStepKey] la llave del paso en el que se abre (la que diga el servidor); por omisión el primero\n * @param {'none'|'free'|'checked'} [props.stepSelection] uno de WIZARD_STEP_SELECTION: si el título de otro paso se pulsa y qué\n * revisa antes; por omisión none (se avanza con «Siguiente», como siempre)\n * @param {(stepKey: string, index: number) => void} [props.onStepChange] cada vez que cambia el paso abierto\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({\n steps, submitLabel, orientation, hideBackOnFirstStep, initialStepKey, stepSelection, onStepChange, texts, ...unknownProps\n}) {\n warnUnknownProps('FormWizard', unknownProps);\n const {trigger, resetField, getFieldState, formState} = useFormContext();\n const {isSubmitting, submitCount} = formState;\n const [skippedKeys, setSkippedKeys] = useState(() => new Set());\n const wasSubmitting = useRef(false);\n\n // revisa SOLO los campos de un paso (trigger los marca, los pinta y pone el foco en el primero con error)\n const isStepValid = async (index) => {\n const stepFields = steps[index].fields ?? [];\n return stepFields.length === 0 || trigger(stepFields, {shouldFocus: true});\n };\n const {activeIndex, setActiveIndex, selectStep} = useWizardSteps({steps, initialStepKey, stepSelection, onStepChange, isStepValid});\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 // el paso tiene errores: se queda ahí para que se corrijan\n if (!(await isStepValid(activeIndex))) {\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} orientation={orientation} hideBackOnFirstStep={hideBackOnFirstStep}\n onBack={() => setActiveIndex((currentIndex) => currentIndex - 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// - para editar un registro (se entra a corregir un dato suelto): initialStepKey abre en el paso que diga el servidor,\n// orientation=\"vertical\" pone los campos dentro de su paso, y stepSelection deja pulsar el título de otro paso para\n// saltar directo (free) o solo si el paso abierto no tiene errores (checked). onStepChange avisa cada cambio de paso.\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 {useWizardSteps} from '@/inputs/useWizardSteps.js';\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[].nextDisabledReason] ya traducido: el paso todavía no se puede dejar («Faltan 2\n * documentos»). «Siguiente» se ve apagado con el motivo en un aviso (escrito, en el teléfono); en el último paso se apaga\n * el de enviar. Con stepSelection checked tampoco se salta a un paso de adelante. Sin él, como siempre\n * @param {import('react').ReactNode} props.steps[].content los campos del paso\n * @param {boolean} [props.steps[].completed] lleva la palomita (cuando los títulos se pulsan, solo si la pantalla lo dice)\n * @param {string} props.submitLabel ya traducido: el botón del último paso\n * @param {'horizontal'|'vertical'} [props.orientation] uno de WIZARD_ORIENTATION; por omisión horizontal\n * @param {boolean} [props.hideBackOnFirstStep] en el primer paso no se pinta «Anterior»; por omisión sale apagado, como antes\n * @param {string} [props.initialStepKey] la llave del paso en el que se abre (la que diga el servidor); por omisión el primero\n * @param {'none'|'free'|'checked'} [props.stepSelection] uno de WIZARD_STEP_SELECTION: si el título de otro paso se pulsa y qué\n * revisa antes; por omisión none (se avanza con «Siguiente», como siempre)\n * @param {(stepKey: string, index: number) => void} [props.onStepChange] cada vez que cambia el paso abierto\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({\n steps, submitLabel, orientation, hideBackOnFirstStep, initialStepKey, stepSelection, onStepChange, texts, ...unknownProps\n}) {\n warnUnknownProps('FormWizard', unknownProps);\n const {trigger, resetField, getFieldState, formState} = useFormContext();\n const {isSubmitting, submitCount} = formState;\n const [skippedKeys, setSkippedKeys] = useState(() => new Set());\n const wasSubmitting = useRef(false);\n\n // revisa SOLO los campos de un paso (trigger los marca, los pinta y pone el foco en el primero con error)\n const isStepValid = async (index) => {\n const stepFields = steps[index].fields ?? [];\n return stepFields.length === 0 || trigger(stepFields, {shouldFocus: true});\n };\n const {activeIndex, setActiveIndex, selectStep} = useWizardSteps({steps, initialStepKey, stepSelection, onStepChange, isStepValid});\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 // el paso tiene errores: se queda ahí para que se corrijan\n if (!(await isStepValid(activeIndex))) {\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 // un paso con su motivo para no avanzar (steps[].nextDisabledReason): «Siguiente» apagado y, en el último, también el de\n // enviar; este sí con disabled de verdad, así un Enter en un campo tampoco envía\n <WizardLayout steps={steps} activeIndex={activeIndex} skippedKeys={skippedKeys} isSubmitting={isSubmitting} orientation={orientation} hideBackOnFirstStep={hideBackOnFirstStep}\n nextDisabledReason={activeStep.nextDisabledReason} onBack={() => setActiveIndex((currentIndex) => currentIndex - 1)}\n onNext={goNext} onSkip={skipStep} onStepSelect={selectStep}\n submitButton={<SubmitButton disabled={Boolean(activeStep.nextDisabledReason)}>{submitLabel}</SubmitButton>}\n problemAlert={<FormProblemAlert />} texts={texts} />\n );\n}\n"],"names":[],"mappings":";;;;;;;;;AAwDA,SAAwB,WAAW;AAAA,EAC/B;AAAA,EAAO;AAAA,EAAa;AAAA,EAAa;AAAA,EAAqB;AAAA,EAAgB;AAAA,EAAe;AAAA,EAAc;AAAA,EAAO,GAAG;AACjH,GAAG;AACC,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,MAAM,oBAAI,KAAK;AAC9D,QAAM,gBAAgB,OAAO,KAAK;AAGlC,QAAM,cAAc,OAAO,UAAU;AACjC,UAAM,aAAa,MAAM,KAAK,EAAE,UAAU,CAAA;AAC1C,WAAO,WAAW,WAAW,KAAK,QAAQ,YAAY,EAAC,aAAa,MAAK;AAAA,EAC7E;AACA,QAAM,EAAC,aAAa,gBAAgB,WAAA,IAAc,eAAe,EAAC,OAAO,gBAAgB,eAAe,cAAc,YAAA,CAAY;AAClI,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;AAEvB,QAAI,CAAE,MAAM,YAAY,WAAW,GAAI;AACnC;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;AAAA;AAAA;AAAA,IAGI;AAAA,MAAC;AAAA,MAAA;AAAA,QAAa;AAAA,QAAc;AAAA,QAA0B;AAAA,QAA0B;AAAA,QAA4B;AAAA,QAA0B;AAAA,QACxH,oBAAoB,WAAW;AAAA,QAAoB,QAAQ,MAAM,eAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,QAClH,QAAQ;AAAA,QAAQ,QAAQ;AAAA,QAAU,cAAc;AAAA,QAChD,kCAAe,cAAA,EAAa,UAAU,QAAQ,WAAW,kBAAkB,GAAI,UAAA,aAAY;AAAA,QAC3F,kCAAe,kBAAA,EAAiB;AAAA,QAAI;AAAA,MAAA;AAAA,IAAA;AAAA;AAE1D;"}
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import { parseMarkedText } from "./ui/labels/markedTextPieces.js";
|
|
|
34
34
|
import { CSS_TOKENS, cssTokenDeclarationsOf, cssTokenOf } from "./theme/cssTokens.js";
|
|
35
35
|
import { FIELD_TONES, GLASS_INSET_PIXELS, GLASS_MENU_RADIUS_PIXELS, GLASS_OPACITY, GLASS_PANEL_RADIUS_PIXELS, accentTextStylesOf, ambientBackgroundOf, canvasFieldStylesOf, fieldPaletteOf, fieldWellStylesOf, fixedBackgroundLayerOf, glassDropOf, glassStylesOf, glassSurfaceOf, isGlassTheme, materialBackgroundOf, materialStylesOf, materialSurfaceOf, scrollEdgeStylesOf } from "./theme/glassSurface.js";
|
|
36
36
|
import { AppLayout, MENU_POSITION } from "./appShell/AppLayout.js";
|
|
37
|
+
import { MENU_LABEL_OVERFLOW } from "./appShell/SideMenuEntry.js";
|
|
37
38
|
import { AuthLayout } from "./appShell/AuthLayout.js";
|
|
38
39
|
import { default as default12 } from "./appShell/BarActions.js";
|
|
39
40
|
import { default as default13 } from "./appShell/BarActionButton.js";
|
|
@@ -121,7 +122,7 @@ import { default as default69 } from "./inputs/ColorInput.js";
|
|
|
121
122
|
import { default as default70 } from "./inputs/FieldFrame.js";
|
|
122
123
|
import { default as default71 } from "./inputs/MaskedInput.js";
|
|
123
124
|
import { INPUT_MASK, isMaskComplete, maskPatternOf, maskedTextOf, rawValueOf } from "./inputs/inputMasks.js";
|
|
124
|
-
import { FORM_LABEL_PLACEMENT, FORM_ROW_CLASS, FormRow, default as default72 } from "./inputs/FormSection.js";
|
|
125
|
+
import { FORM_LABEL_PLACEMENT, FORM_LABEL_WIDTH, FORM_ROW_CLASS, FormRow, default as default72 } from "./inputs/FormSection.js";
|
|
125
126
|
import { default as default73 } from "./inputs/CheckboxGroupInput.js";
|
|
126
127
|
import { default as default74 } from "./inputs/NumberInput.js";
|
|
127
128
|
import { default as default75 } from "./inputs/TransferListInput.js";
|
|
@@ -238,6 +239,7 @@ export {
|
|
|
238
239
|
FILTER_FIELD_KIND,
|
|
239
240
|
FONT_SIZE_LEVELS,
|
|
240
241
|
FORM_LABEL_PLACEMENT,
|
|
242
|
+
FORM_LABEL_WIDTH,
|
|
241
243
|
FORM_ROW_CLASS,
|
|
242
244
|
default70 as FieldFrame,
|
|
243
245
|
default25 as FileDropzone,
|
|
@@ -273,6 +275,7 @@ export {
|
|
|
273
275
|
LANGUAGES,
|
|
274
276
|
default45 as LanguageToggle,
|
|
275
277
|
MAIN_CONTENT_ID,
|
|
278
|
+
MENU_LABEL_OVERFLOW,
|
|
276
279
|
MENU_POSITION,
|
|
277
280
|
MINIMAL_BACKGROUND,
|
|
278
281
|
MINIMAL_TONES,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -11,10 +11,12 @@ import { fieldVariablesOf, accentTextStylesOf, PANEL_RADIUS, fieldWellFromVariab
|
|
|
11
11
|
import { warnUnknownProps } from "../toolkit/unknownProps.js";
|
|
12
12
|
import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
|
|
13
13
|
const FORM_LABEL_PLACEMENT = Object.freeze({ side: "side", topOnPhone: "topOnPhone" });
|
|
14
|
+
const FORM_LABEL_WIDTH = Object.freeze({ auto: "auto" });
|
|
14
15
|
const FORM_ROW_CLASS = "calcita-form-row";
|
|
15
16
|
const ROW_MIN_HEIGHT_PIXELS = 52;
|
|
16
17
|
const ROW_PADDING_BLOCK_PIXELS = 8;
|
|
17
18
|
const LABEL_COLUMN = Object.freeze({ share: "38%", maxPixels: 200 });
|
|
19
|
+
const AUTO_CONTROL_MAX_SHARE = "50%";
|
|
18
20
|
const LABEL_TOP_PIXELS = 8;
|
|
19
21
|
const RING = Object.freeze({ focusPercent: 45, errorPercent: 60, pixels: 2 });
|
|
20
22
|
function rowControlSxOf(theme) {
|
|
@@ -61,25 +63,40 @@ const FormRowRoot = styled(Box, { name: ROW_THEME_NAME, slot: "Root", shouldForw
|
|
|
61
63
|
...isTopOnPhone && { [theme.breakpoints.down("sm")]: { flexDirection: "column", alignItems: "stretch", gap: 0.5 } }
|
|
62
64
|
});
|
|
63
65
|
});
|
|
66
|
+
function labelColumnStylesOf(labelWidth) {
|
|
67
|
+
if (labelWidth === FORM_LABEL_WIDTH.auto) {
|
|
68
|
+
return { flex: "1 1 auto", maxWidth: "none" };
|
|
69
|
+
}
|
|
70
|
+
if (labelWidth !== void 0 && labelWidth !== null) {
|
|
71
|
+
const width = typeof labelWidth === "number" ? `${labelWidth}px` : labelWidth;
|
|
72
|
+
return { flex: `0 0 ${width}`, maxWidth: "none" };
|
|
73
|
+
}
|
|
74
|
+
return { flex: `0 0 ${LABEL_COLUMN.share}`, maxWidth: LABEL_COLUMN.maxPixels };
|
|
75
|
+
}
|
|
64
76
|
const FormRowLabel = styled(Typography, { name: ROW_THEME_NAME, slot: "Label", shouldForwardProp: isPartProp })(({ theme, ownerState }) => theme.unstable_sx({
|
|
65
|
-
|
|
66
|
-
maxWidth: LABEL_COLUMN.maxPixels,
|
|
77
|
+
...labelColumnStylesOf(ownerState?.labelWidth),
|
|
67
78
|
pt: `${LABEL_TOP_PIXELS}px`,
|
|
68
79
|
lineHeight: 1.5,
|
|
69
80
|
color: "text.primary",
|
|
81
|
+
// va después del ancho: en el teléfono, con la etiqueta arriba, la columna ya no cuenta
|
|
70
82
|
...ownerState?.labelPlacement === FORM_LABEL_PLACEMENT.topOnPhone && {
|
|
71
83
|
[theme.breakpoints.down("sm")]: { flex: "none", maxWidth: "none", pt: 0 }
|
|
72
84
|
},
|
|
73
85
|
// el asterisco de lo obligatorio: aquí, debajo de lo que el proyecto ponga en su tema
|
|
74
86
|
...ownerState?.requiredMark && requiredMarkSxOf(ownerState.requiredMark)
|
|
75
87
|
}));
|
|
76
|
-
const FormRowControl = styled(Box, { name: ROW_THEME_NAME, slot: "Control" })({
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
const FormRowControl = styled(Box, { name: ROW_THEME_NAME, slot: "Control", shouldForwardProp: isPartProp })(({ theme, ownerState }) => {
|
|
89
|
+
const isAuto = ownerState?.labelWidth === FORM_LABEL_WIDTH.auto;
|
|
90
|
+
return {
|
|
91
|
+
flex: isAuto ? "0 0 auto" : 1,
|
|
92
|
+
...isAuto && { maxWidth: AUTO_CONTROL_MAX_SHARE },
|
|
93
|
+
...isAuto && ownerState?.labelPlacement === FORM_LABEL_PLACEMENT.topOnPhone && { [theme.breakpoints.down("sm")]: { maxWidth: "none" } },
|
|
94
|
+
minWidth: 0,
|
|
95
|
+
display: "flex",
|
|
96
|
+
flexDirection: "column",
|
|
97
|
+
justifyContent: "center",
|
|
98
|
+
minHeight: 40
|
|
99
|
+
};
|
|
83
100
|
});
|
|
84
101
|
const FormSectionRoot = styled(Box, { name: SECTION_THEME_NAME, slot: "Root" })({});
|
|
85
102
|
const FormSectionTitle = styled(Typography, { name: SECTION_THEME_NAME, slot: "Title" })(({ theme }) => theme.unstable_sx({ px: 2, mb: 0.75, fontWeight: 600 }));
|
|
@@ -104,6 +121,7 @@ function FormRow(props) {
|
|
|
104
121
|
const sectionLayout = useFormSectionLayout();
|
|
105
122
|
const ownerState = {
|
|
106
123
|
labelPlacement: sectionLayout ? sectionLayout.labelPlacement : FORM_LABEL_PLACEMENT.side,
|
|
124
|
+
labelWidth: sectionLayout ? sectionLayout.labelWidth : void 0,
|
|
107
125
|
requiredMark: required ? t("form.requiredMark") : null
|
|
108
126
|
};
|
|
109
127
|
return /* @__PURE__ */ jsxs(FormRowRoot, { className: FORM_ROW_CLASS, ownerState, children: [
|
|
@@ -119,15 +137,23 @@ function FormRow(props) {
|
|
|
119
137
|
children: label
|
|
120
138
|
}
|
|
121
139
|
),
|
|
122
|
-
/* @__PURE__ */ jsx(FormRowControl, { children })
|
|
140
|
+
/* @__PURE__ */ jsx(FormRowControl, { ownerState, children })
|
|
123
141
|
] });
|
|
124
142
|
}
|
|
125
143
|
function FormSection(props) {
|
|
126
|
-
const {
|
|
144
|
+
const {
|
|
145
|
+
title,
|
|
146
|
+
footer,
|
|
147
|
+
labelPlacement = FORM_LABEL_PLACEMENT.side,
|
|
148
|
+
labelWidth,
|
|
149
|
+
sx,
|
|
150
|
+
children,
|
|
151
|
+
...unknownProps
|
|
152
|
+
} = useThemeProps({ props, name: SECTION_THEME_NAME });
|
|
127
153
|
warnUnknownProps(SECTION_THEME_NAME, unknownProps);
|
|
128
154
|
const titleId = useId();
|
|
129
|
-
const layout = useMemo(() => ({ labelPlacement }), [labelPlacement]);
|
|
130
|
-
return /* @__PURE__ */ jsx(FormSectionContext, { value: layout, children: /* @__PURE__ */ jsxs(FormSectionRoot, { component: "section", "aria-labelledby": title ? titleId : void 0, children: [
|
|
155
|
+
const layout = useMemo(() => ({ labelPlacement, labelWidth }), [labelPlacement, labelWidth]);
|
|
156
|
+
return /* @__PURE__ */ jsx(FormSectionContext, { value: layout, children: /* @__PURE__ */ jsxs(FormSectionRoot, { component: "section", "aria-labelledby": title ? titleId : void 0, sx, children: [
|
|
131
157
|
title && /* @__PURE__ */ jsx(FormSectionTitle, { id: titleId, variant: "subtitle2", component: "h2", color: "text.secondary", children: title }),
|
|
132
158
|
/* @__PURE__ */ jsx(FormSectionCard, { variant: "outlined", children: /* @__PURE__ */ jsx(Box, { sx: { mt: "-1px" }, children }) }),
|
|
133
159
|
footer && /* @__PURE__ */ jsx(FormSectionFooter, { variant: "caption", component: "p", color: "text.secondary", children: footer })
|
|
@@ -135,6 +161,7 @@ function FormSection(props) {
|
|
|
135
161
|
}
|
|
136
162
|
export {
|
|
137
163
|
FORM_LABEL_PLACEMENT,
|
|
164
|
+
FORM_LABEL_WIDTH,
|
|
138
165
|
FORM_ROW_CLASS,
|
|
139
166
|
FormRow,
|
|
140
167
|
FormSection as default
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormSection.js","sources":["../../src/inputs/FormSection.jsx"],"sourcesContent":["// El formulario agrupado de Apple (el de Ajustes y el de la hoja «Nuevo evento» de Calendario): una tarjeta redonda con\n// renglones, el título de la sección arriba de la tarjeta y la explicación abajo. Dentro de una FormSection cada campo del\n// framework (TextInput, NumberInput, SelectInput, CatalogInput, ColorInput, DateInput y sus versiones de formik y de\n// react-hook-form) se pinta solo como renglón: la etiqueta a la izquierda y el campo a la derecha.\n//\n// <FormSection title=\"Datos del cliente\" footer=\"Le enviaremos la factura al correo.\">\n// <FormTextField name=\"name\" label=\"Nombre\" />\n// <FormTextField name=\"email\" label=\"Correo\" />\n// <FormRow label=\"Recibir avisos\"><Switch /></FormRow>\n// </FormSection>\n//\n// Las reglas, de la guía de Apple (HIG «Text fields», «Pickers», «Combo boxes» y sus imágenes):\n// - la etiqueta siempre a la vista, a la izquierda (la pista se va al escribir; la etiqueta no);\n// - el campo es un «pozo» parejo con esquinas de 12 px (fieldWellStylesOf): gris tenue en claro y más claro que la\n// tarjeta en oscuro, con la letra clara, como el campo de la imagen de Apple. Antes iba transparente y el relleno solo\n// salía con el cursor encima, en cuadro: no se entendía dónde escribir (lo reportaron);\n// - la fecha es el mismo campo que los demás, a todo lo ancho (en cápsula al final del renglón quedaba descuadrada), con\n// la letra del texto (no del color del tema: en oscuro el naranja ahí «se veía horrible», lo reportaron);\n// - al escribir, un aro tenue del color principal y la etiqueta del color del tema; con error, aro y etiqueta en rojo y\n// el mensaje debajo del campo;\n// - una línea fina entre renglones, que empieza donde empieza el texto.\n//\n// Los campos pueden ir directo o dentro de lo que el proyecto necesite (un Grid de dos columnas, un Stack, un componente\n// suyo): cada renglón dibuja su propia línea ARRIBA y la tarjeta sube su contenido un píxel y lo recorta, así la línea del\n// primero queda escondida. OJO: antes la línea se dibujaba entre dos hermanos seguidos y con un Grid en medio desaparecían\n// todas (lo señalaron en una revisión: obligaba a «la forma Calcita»).\n//\n// En el teléfono la etiqueta se lleva el 38 % del renglón y un nombre largo («Correo adicional (notificaciones)») se parte en\n// tres renglones: con labelPlacement={FORM_LABEL_PLACEMENT.topOnPhone} la etiqueta sube arriba del campo por debajo de `sm`\n// (como en los formularios de iOS y de Android); en pantallas más anchas, igual que siempre. Por omisión `side`, como antes.\n//\n// Un renglón con `required` lleva un asterisco en la etiqueta (requiredMark.js: en el estilo, así la etiqueta sigue diciendo lo\n// mismo y el lector de pantalla no lo lee; el campo ya dice que es obligatorio).\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaFormSection (sus partes root, title, card y\n// footer) y components.CalcitaFormRow (root, label y control; el asterisco es '&[data-required]::after' de label), con\n// styleOverrides y defaultProps. Los colores de los campos,\n// más fácil con createToolkitTheme({fields}).\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useId, useMemo} from 'react';\nimport Box from '@mui/material/Box';\nimport Paper from '@mui/material/Paper';\nimport Typography from '@mui/material/Typography';\nimport {styled, useThemeProps} from '@mui/material/styles';\n\nimport {FormSectionContext, useFormSectionLayout} from '@/inputs/formLayoutContext.js';\nimport {REQUIRED_LABEL_ATTRIBUTE, requiredMarkSxOf} from '@/inputs/requiredMark.js';\nimport {PANEL_RADIUS, accentTextStylesOf, fieldVariablesOf, fieldWellFromVariablesOf} from '@/theme/glassSurface.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\n\n/**\n * Dónde va la etiqueta de cada renglón: a la izquierda siempre (side, lo de siempre) o arriba del campo en el teléfono\n * (topOnPhone: por debajo de `sm` en el tema; más ancho, a la izquierda).\n */\nexport const FORM_LABEL_PLACEMENT = Object.freeze({side: 'side', topOnPhone: 'topOnPhone'});\n\n/** La clase de cada renglón: la sección le dibuja su línea de arriba, esté donde esté adentro de la tarjeta. */\nexport const FORM_ROW_CLASS = 'calcita-form-row';\n\n// el renglón: alto mínimo para el dedo, su aire arriba y abajo, y lo ancho de la columna de las etiquetas\nconst ROW_MIN_HEIGHT_PIXELS = 52;\nconst ROW_PADDING_BLOCK_PIXELS = 8;\nconst LABEL_COLUMN = Object.freeze({share: '38%', maxPixels: 200});\n// lo que la etiqueta baja para quedar en la misma línea que el texto del campo (el campo mide 40 px y su letra 24 px)\nconst LABEL_TOP_PIXELS = 8;\n// el aro al escribir y con error (por ciento del color) y su grosor\nconst RING = Object.freeze({focusPercent: 45, errorPercent: 60, pixels: 2});\n\n/**\n * Los campos de MUI dentro de un renglón: el pozo de Apple (fieldWellStylesOf), sin el borde de MUI ni el vidrio del tema,\n * con su aro al escribir. Los selectores van con «html body &&&» para ganarle a los ajustes del tema dentro de una\n * tarjeta, que con el lente llevan una etiqueta y hasta cuatro clases (con menos, el relleno del tema salía al pasar el\n * cursor, en cuadro: los «hovers espantosos» que reportaron).\n *\n * @param {object} theme\n * @returns {object}\n */\nfunction rowControlSxOf(theme) {\n const palette = (theme.vars ?? theme).palette;\n const strong = (selectors) => selectors.map((selector) => `html body &&& ${selector}`).join(', ');\n const roots = ['.MuiOutlinedInput-root', '.MuiFilledInput-root', '.MuiPickersOutlinedInput-root'];\n const ringOf = (color, percent) => ({outline: `${RING.pixels}px solid color-mix(in srgb, ${color} ${percent}%, transparent)`, outlineOffset: 0});\n return {\n // los tonos van en variables del renglón (fieldVariablesOf, abajo en FormRow): aquí abajo una regla de oscuro no aplica\n [strong(roots)]: {...fieldWellFromVariablesOf(theme), outline: 'none'},\n [strong(roots.map((root) => `${root}.Mui-focused`))]: ringOf(palette.primary.main, RING.focusPercent),\n [strong(roots.map((root) => `${root}.Mui-error`))]: ringOf(palette.error.main, RING.errorPercent),\n [strong(['.MuiOutlinedInput-notchedOutline', '.MuiPickersOutlinedInput-notchedOutline'])]: {display: 'none'},\n [strong(['.MuiFilledInput-root::before', '.MuiFilledInput-root::after'])]: {display: 'none'},\n // la fecha: el mismo campo que los demás, a todo lo ancho de su columna (en cápsula al final del renglón quedaba\n // descuadrada, con su ayuda a la izquierda debajo de nada; lo reportaron), con el ícono del calendario discreto\n [strong(['.MuiPickersOutlinedInput-root'])]: {color: palette.text.primary},\n [strong(['.MuiPickersOutlinedInput-root .MuiInputAdornment-root .MuiSvgIcon-root'])]: {fontSize: '1.125rem', color: palette.text.secondary},\n // la ayuda y el error, debajo del campo; el renglón no guarda lugar para una ayuda vacía\n '& .MuiFormHelperText-root': {marginInline: theme.spacing(0.5), marginTop: '4px', marginBottom: '2px'},\n '& .MuiFormHelperText-root:has(> .notranslate:only-child)': {display: 'none'},\n };\n}\n\n// los nombres de las piezas en el tema (theme.components.CalcitaFormSection y CalcitaFormRow)\nconst SECTION_THEME_NAME = 'CalcitaFormSection';\nconst ROW_THEME_NAME = 'CalcitaFormRow';\n\n// el renglón: la etiqueta a la izquierda y el campo a la derecha, con la línea de arriba y los tonos de los campos\n// ownerState: dónde va la etiqueta (también lo recibe un styleOverrides del tema que sea función); no llega al DOM\nconst isPartProp = (propName) => propName !== 'ownerState';\n\nconst FormRowRoot = styled(Box, {name: ROW_THEME_NAME, slot: 'Root', shouldForwardProp: isPartProp})(({theme, ownerState}) => {\n const palette = (theme.vars ?? theme).palette;\n const isTopOnPhone = ownerState?.labelPlacement === FORM_LABEL_PLACEMENT.topOnPhone;\n return theme.unstable_sx({\n position: 'relative',\n display: 'flex',\n alignItems: 'flex-start',\n gap: 2,\n minHeight: ROW_MIN_HEIGHT_PIXELS,\n px: 2,\n py: `${ROW_PADDING_BLOCK_PIXELS}px`,\n '&:focus-within > label': accentTextStylesOf(theme),\n '&:has(.Mui-error) > label': {color: palette.error.main},\n // los tonos de los campos (los del proyecto con createToolkitTheme fields, o los de Apple) para sus campos\n ...fieldVariablesOf(theme),\n ...rowControlSxOf(theme),\n // en el teléfono, la etiqueta arriba y el campo a todo lo ancho debajo\n ...(isTopOnPhone && {[theme.breakpoints.down('sm')]: {flexDirection: 'column', alignItems: 'stretch', gap: 0.5}}),\n });\n});\n\nconst FormRowLabel = styled(Typography, {name: ROW_THEME_NAME, slot: 'Label', shouldForwardProp: isPartProp})(({theme, ownerState}) => theme.unstable_sx({\n flex: `0 0 ${LABEL_COLUMN.share}`, maxWidth: LABEL_COLUMN.maxPixels, pt: `${LABEL_TOP_PIXELS}px`, lineHeight: 1.5, color: 'text.primary',\n ...(ownerState?.labelPlacement === FORM_LABEL_PLACEMENT.topOnPhone && {\n [theme.breakpoints.down('sm')]: {flex: 'none', maxWidth: 'none', pt: 0},\n }),\n // el asterisco de lo obligatorio: aquí, debajo de lo que el proyecto ponga en su tema\n ...(ownerState?.requiredMark && requiredMarkSxOf(ownerState.requiredMark)),\n}));\n\n\nconst FormRowControl = styled(Box, {name: ROW_THEME_NAME, slot: 'Control'})({\n flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', justifyContent: 'center', minHeight: 40,\n});\n\nconst FormSectionRoot = styled(Box, {name: SECTION_THEME_NAME, slot: 'Root'})({});\n\nconst FormSectionTitle = styled(Typography, {name: SECTION_THEME_NAME, slot: 'Title'})(({theme}) => theme.unstable_sx({px: 2, mb: 0.75, fontWeight: 600}));\n\n// la tarjeta: la línea de arriba de cada renglón, desde donde empieza el texto (la del primero la recorta la tarjeta)\nconst FormSectionCard = styled(Paper, {name: SECTION_THEME_NAME, slot: 'Card'})(({theme}) => ({\n borderRadius: PANEL_RADIUS,\n overflow: 'hidden',\n [`& .${FORM_ROW_CLASS}::before`]: {\n content: '\"\"', position: 'absolute', top: 0, right: 0, left: theme.spacing(2), height: '1px',\n backgroundColor: (theme.vars ?? theme).palette.divider,\n },\n}));\n\nconst FormSectionFooter = styled(Typography, {name: SECTION_THEME_NAME, slot: 'Footer'})(({theme}) => theme.unstable_sx({px: 2, mt: 0.75}));\n\n/**\n * Un renglón del formulario agrupado: la etiqueta a la izquierda y lo que sea a la derecha (un campo, un interruptor, un\n * texto). Los campos del framework lo usan solos dentro de una FormSection; FormRow suelto es para lo demás.\n *\n * @param {object} props\n * @param {import('react').ReactNode} props.label ya traducida\n * @param {string} [props.htmlFor] el id del campo que nombra la etiqueta\n * @param {string} [props.labelId] el id de la etiqueta (para un campo que la nombra con aria-labelledby)\n * @param {boolean} [props.required] el asterisco en la etiqueta (el lector de pantalla no lo lee: el campo ya lo dice)\n * @param {import('react').ReactNode} props.children\n */\nexport function FormRow(props) {\n // lo que el proyecto puso en su tema (components.CalcitaFormRow.defaultProps) y encima lo que llega aquí\n const {label, htmlFor, labelId, required = false, children, ...unknownProps} = useThemeProps({props, name: ROW_THEME_NAME});\n warnUnknownProps(ROW_THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common');\n // dónde va la etiqueta lo pide la sección; un FormRow fuera de una sección, a la izquierda\n const sectionLayout = useFormSectionLayout();\n const ownerState = {\n labelPlacement: sectionLayout ? sectionLayout.labelPlacement : FORM_LABEL_PLACEMENT.side,\n requiredMark: required ? t('form.requiredMark') : null,\n };\n return (\n <FormRowRoot className={FORM_ROW_CLASS} ownerState={ownerState}>\n <FormRowLabel component=\"label\" id={labelId} htmlFor={htmlFor} variant=\"body1\" ownerState={ownerState}\n {...(required && {[REQUIRED_LABEL_ATTRIBUTE]: 'true'})}>\n {label}\n </FormRowLabel>\n <FormRowControl>\n {children}\n </FormRowControl>\n </FormRowRoot>\n );\n}\n\n/**\n * @param {object} props\n * @param {string} [props.title] ya traducido; va arriba de la tarjeta\n * @param {import('react').ReactNode} [props.footer] la explicación, abajo de la tarjeta\n * @param {string} [props.labelPlacement] uno de FORM_LABEL_PLACEMENT: side (por omisión, la etiqueta a la izquierda) o\n * topOnPhone (arriba del campo por debajo de `sm`)\n * @param {import('react').ReactNode} props.children los campos, directo o dentro de un Grid, un Stack o un componente propio\n */\nexport default function FormSection(props) {\n // lo que el proyecto puso en su tema (components.CalcitaFormSection.defaultProps) y encima lo que llega aquí\n const {title, footer, labelPlacement = FORM_LABEL_PLACEMENT.side, children, ...unknownProps} = useThemeProps({props, name: SECTION_THEME_NAME});\n warnUnknownProps(SECTION_THEME_NAME, unknownProps);\n const titleId = useId();\n const layout = useMemo(() => ({labelPlacement}), [labelPlacement]);\n return (\n <FormSectionContext value={layout}>\n <FormSectionRoot component=\"section\" aria-labelledby={title ? titleId : undefined}>\n {title && (\n <FormSectionTitle id={titleId} variant=\"subtitle2\" component=\"h2\" color=\"text.secondary\">\n {title}\n </FormSectionTitle>\n )}\n <FormSectionCard variant=\"outlined\">\n {/* un píxel arriba: la línea de los renglones de hasta arriba queda fuera de la tarjeta (overflow hidden) */}\n <Box sx={{mt: '-1px'}}>{children}</Box>\n </FormSectionCard>\n {footer && (\n <FormSectionFooter variant=\"caption\" component=\"p\" color=\"text.secondary\">{footer}</FormSectionFooter>\n )}\n </FormSectionRoot>\n </FormSectionContext>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FormSection.js","sources":["../../src/inputs/FormSection.jsx"],"sourcesContent":["// El formulario agrupado de Apple (el de Ajustes y el de la hoja «Nuevo evento» de Calendario): una tarjeta redonda con\n// renglones, el título de la sección arriba de la tarjeta y la explicación abajo. Dentro de una FormSection cada campo del\n// framework (TextInput, NumberInput, SelectInput, CatalogInput, ColorInput, DateInput y sus versiones de formik y de\n// react-hook-form) se pinta solo como renglón: la etiqueta a la izquierda y el campo a la derecha.\n//\n// <FormSection title=\"Datos del cliente\" footer=\"Le enviaremos la factura al correo.\">\n// <FormTextField name=\"name\" label=\"Nombre\" />\n// <FormTextField name=\"email\" label=\"Correo\" />\n// <FormRow label=\"Recibir avisos\"><Switch /></FormRow>\n// </FormSection>\n//\n// Las reglas, de la guía de Apple (HIG «Text fields», «Pickers», «Combo boxes» y sus imágenes):\n// - la etiqueta siempre a la vista, a la izquierda (la pista se va al escribir; la etiqueta no);\n// - el campo es un «pozo» parejo con esquinas de 12 px (fieldWellStylesOf): gris tenue en claro y más claro que la\n// tarjeta en oscuro, con la letra clara, como el campo de la imagen de Apple. Antes iba transparente y el relleno solo\n// salía con el cursor encima, en cuadro: no se entendía dónde escribir (lo reportaron);\n// - la fecha es el mismo campo que los demás, a todo lo ancho (en cápsula al final del renglón quedaba descuadrada), con\n// la letra del texto (no del color del tema: en oscuro el naranja ahí «se veía horrible», lo reportaron);\n// - al escribir, un aro tenue del color principal y la etiqueta del color del tema; con error, aro y etiqueta en rojo y\n// el mensaje debajo del campo;\n// - una línea fina entre renglones, que empieza donde empieza el texto.\n//\n// Los campos pueden ir directo o dentro de lo que el proyecto necesite (un Grid de dos columnas, un Stack, un componente\n// suyo): cada renglón dibuja su propia línea ARRIBA y la tarjeta sube su contenido un píxel y lo recorta, así la línea del\n// primero queda escondida. OJO: antes la línea se dibujaba entre dos hermanos seguidos y con un Grid en medio desaparecían\n// todas (lo señalaron en una revisión: obligaba a «la forma Calcita»).\n//\n// En el teléfono la etiqueta se lleva el 38 % del renglón y un nombre largo («Correo adicional (notificaciones)») se parte en\n// tres renglones: con labelPlacement={FORM_LABEL_PLACEMENT.topOnPhone} la etiqueta sube arriba del campo por debajo de `sm`\n// (como en los formularios de iOS y de Android); en pantallas más anchas, igual que siempre. Por omisión `side`, como antes.\n//\n// Lo ancho de la columna de las etiquetas, por sección (labelWidth): por omisión el 38 % del renglón hasta 200 px, lo de\n// siempre. Una lista de documentos lleva nombres de cien caracteres y a la derecha solo sus botones: con 200 px el nombre se\n// partía en ocho renglones. Con labelWidth={FORM_LABEL_WIDTH.auto} el nombre se lleva lo que sobre y el campo solo lo que\n// ocupa; con un ancho ('320px', '50%' o un número en px) la columna mide eso. Las demás secciones de la pantalla no cambian.\n// En el teléfono lo que sobra es poco: con auto, mejor junto con labelPlacement topOnPhone (el nombre arriba, el campo abajo).\n//\n// Un renglón con `required` lleva un asterisco en la etiqueta (requiredMark.js: en el estilo, así la etiqueta sigue diciendo lo\n// mismo y el lector de pantalla no lo lee; el campo ya dice que es obligatorio).\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaFormSection (sus partes root, title, card y\n// footer) y components.CalcitaFormRow (root, label y control; el asterisco es '&[data-required]::after' de label), con\n// styleOverrides y defaultProps. Los colores de los campos,\n// más fácil con createToolkitTheme({fields}).\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useId, useMemo} from 'react';\nimport Box from '@mui/material/Box';\nimport Paper from '@mui/material/Paper';\nimport Typography from '@mui/material/Typography';\nimport {styled, useThemeProps} from '@mui/material/styles';\n\nimport {FormSectionContext, useFormSectionLayout} from '@/inputs/formLayoutContext.js';\nimport {REQUIRED_LABEL_ATTRIBUTE, requiredMarkSxOf} from '@/inputs/requiredMark.js';\nimport {PANEL_RADIUS, accentTextStylesOf, fieldVariablesOf, fieldWellFromVariablesOf} from '@/theme/glassSurface.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\n\n/**\n * Dónde va la etiqueta de cada renglón: a la izquierda siempre (side, lo de siempre) o arriba del campo en el teléfono\n * (topOnPhone: por debajo de `sm` en el tema; más ancho, a la izquierda).\n */\nexport const FORM_LABEL_PLACEMENT = Object.freeze({side: 'side', topOnPhone: 'topOnPhone'});\n\n/**\n * Lo ancho de la columna de las etiquetas que no es un número: auto = la etiqueta se lleva lo que sobre y el campo solo lo\n * que ocupa (una lista de documentos con su cargador).\n */\nexport const FORM_LABEL_WIDTH = Object.freeze({auto: 'auto'});\n\n/** La clase de cada renglón: la sección le dibuja su línea de arriba, esté donde esté adentro de la tarjeta. */\nexport const FORM_ROW_CLASS = 'calcita-form-row';\n\n// el renglón: alto mínimo para el dedo, su aire arriba y abajo, y lo ancho de la columna de las etiquetas\nconst ROW_MIN_HEIGHT_PIXELS = 52;\nconst ROW_PADDING_BLOCK_PIXELS = 8;\nconst LABEL_COLUMN = Object.freeze({share: '38%', maxPixels: 200});\n// con la etiqueta en auto, lo más que se lleva el campo: el nombre del documento conserva al menos la otra mitad\nconst AUTO_CONTROL_MAX_SHARE = '50%';\n// lo que la etiqueta baja para quedar en la misma línea que el texto del campo (el campo mide 40 px y su letra 24 px)\nconst LABEL_TOP_PIXELS = 8;\n// el aro al escribir y con error (por ciento del color) y su grosor\nconst RING = Object.freeze({focusPercent: 45, errorPercent: 60, pixels: 2});\n\n/**\n * Los campos de MUI dentro de un renglón: el pozo de Apple (fieldWellStylesOf), sin el borde de MUI ni el vidrio del tema,\n * con su aro al escribir. Los selectores van con «html body &&&» para ganarle a los ajustes del tema dentro de una\n * tarjeta, que con el lente llevan una etiqueta y hasta cuatro clases (con menos, el relleno del tema salía al pasar el\n * cursor, en cuadro: los «hovers espantosos» que reportaron).\n *\n * @param {object} theme\n * @returns {object}\n */\nfunction rowControlSxOf(theme) {\n const palette = (theme.vars ?? theme).palette;\n const strong = (selectors) => selectors.map((selector) => `html body &&& ${selector}`).join(', ');\n const roots = ['.MuiOutlinedInput-root', '.MuiFilledInput-root', '.MuiPickersOutlinedInput-root'];\n const ringOf = (color, percent) => ({outline: `${RING.pixels}px solid color-mix(in srgb, ${color} ${percent}%, transparent)`, outlineOffset: 0});\n return {\n // los tonos van en variables del renglón (fieldVariablesOf, abajo en FormRow): aquí abajo una regla de oscuro no aplica\n [strong(roots)]: {...fieldWellFromVariablesOf(theme), outline: 'none'},\n [strong(roots.map((root) => `${root}.Mui-focused`))]: ringOf(palette.primary.main, RING.focusPercent),\n [strong(roots.map((root) => `${root}.Mui-error`))]: ringOf(palette.error.main, RING.errorPercent),\n [strong(['.MuiOutlinedInput-notchedOutline', '.MuiPickersOutlinedInput-notchedOutline'])]: {display: 'none'},\n [strong(['.MuiFilledInput-root::before', '.MuiFilledInput-root::after'])]: {display: 'none'},\n // la fecha: el mismo campo que los demás, a todo lo ancho de su columna (en cápsula al final del renglón quedaba\n // descuadrada, con su ayuda a la izquierda debajo de nada; lo reportaron), con el ícono del calendario discreto\n [strong(['.MuiPickersOutlinedInput-root'])]: {color: palette.text.primary},\n [strong(['.MuiPickersOutlinedInput-root .MuiInputAdornment-root .MuiSvgIcon-root'])]: {fontSize: '1.125rem', color: palette.text.secondary},\n // la ayuda y el error, debajo del campo; el renglón no guarda lugar para una ayuda vacía\n '& .MuiFormHelperText-root': {marginInline: theme.spacing(0.5), marginTop: '4px', marginBottom: '2px'},\n '& .MuiFormHelperText-root:has(> .notranslate:only-child)': {display: 'none'},\n };\n}\n\n// los nombres de las piezas en el tema (theme.components.CalcitaFormSection y CalcitaFormRow)\nconst SECTION_THEME_NAME = 'CalcitaFormSection';\nconst ROW_THEME_NAME = 'CalcitaFormRow';\n\n// el renglón: la etiqueta a la izquierda y el campo a la derecha, con la línea de arriba y los tonos de los campos\n// ownerState: dónde va la etiqueta (también lo recibe un styleOverrides del tema que sea función); no llega al DOM\nconst isPartProp = (propName) => propName !== 'ownerState';\n\nconst FormRowRoot = styled(Box, {name: ROW_THEME_NAME, slot: 'Root', shouldForwardProp: isPartProp})(({theme, ownerState}) => {\n const palette = (theme.vars ?? theme).palette;\n const isTopOnPhone = ownerState?.labelPlacement === FORM_LABEL_PLACEMENT.topOnPhone;\n return theme.unstable_sx({\n position: 'relative',\n display: 'flex',\n alignItems: 'flex-start',\n gap: 2,\n minHeight: ROW_MIN_HEIGHT_PIXELS,\n px: 2,\n py: `${ROW_PADDING_BLOCK_PIXELS}px`,\n '&:focus-within > label': accentTextStylesOf(theme),\n '&:has(.Mui-error) > label': {color: palette.error.main},\n // los tonos de los campos (los del proyecto con createToolkitTheme fields, o los de Apple) para sus campos\n ...fieldVariablesOf(theme),\n ...rowControlSxOf(theme),\n // en el teléfono, la etiqueta arriba y el campo a todo lo ancho debajo\n ...(isTopOnPhone && {[theme.breakpoints.down('sm')]: {flexDirection: 'column', alignItems: 'stretch', gap: 0.5}}),\n });\n});\n\n/**\n * Lo ancho de la columna de la etiqueta según la sección: lo de siempre (38 % hasta 200 px), auto (lo que sobre) o el ancho\n * que dijo (un número son px, como en sx).\n *\n * @param {string|number|undefined} labelWidth\n * @returns {object}\n */\nfunction labelColumnStylesOf(labelWidth) {\n if (labelWidth === FORM_LABEL_WIDTH.auto) {\n return {flex: '1 1 auto', maxWidth: 'none'};\n }\n if (labelWidth !== undefined && labelWidth !== null) {\n const width = typeof labelWidth === 'number' ? `${labelWidth}px` : labelWidth;\n return {flex: `0 0 ${width}`, maxWidth: 'none'};\n }\n return {flex: `0 0 ${LABEL_COLUMN.share}`, maxWidth: LABEL_COLUMN.maxPixels};\n}\n\nconst FormRowLabel = styled(Typography, {name: ROW_THEME_NAME, slot: 'Label', shouldForwardProp: isPartProp})(({theme, ownerState}) => theme.unstable_sx({\n ...labelColumnStylesOf(ownerState?.labelWidth), pt: `${LABEL_TOP_PIXELS}px`, lineHeight: 1.5, color: 'text.primary',\n // va después del ancho: en el teléfono, con la etiqueta arriba, la columna ya no cuenta\n ...(ownerState?.labelPlacement === FORM_LABEL_PLACEMENT.topOnPhone && {\n [theme.breakpoints.down('sm')]: {flex: 'none', maxWidth: 'none', pt: 0},\n }),\n // el asterisco de lo obligatorio: aquí, debajo de lo que el proyecto ponga en su tema\n ...(ownerState?.requiredMark && requiredMarkSxOf(ownerState.requiredMark)),\n}));\n\n\n// el campo: lo que queda del renglón; con la etiqueta en auto, solo lo que ocupa (los botones de un cargador), sin encogerse\n// y hasta la mitad del renglón (un archivo de nombre largo se corta con «…» en lugar de aplastar el nombre del documento).\n// OJO: con '0 1 auto' el renglón angosto lo apretaba y, como el cargador alinea a la derecha, sus botones se salían hacia la\n// izquierda, encima del nombre (pasó en el teléfono). Con la etiqueta arriba en el teléfono, el campo va a todo lo ancho\nconst FormRowControl = styled(Box, {name: ROW_THEME_NAME, slot: 'Control', shouldForwardProp: isPartProp})(({theme, ownerState}) => {\n const isAuto = ownerState?.labelWidth === FORM_LABEL_WIDTH.auto;\n return {\n flex: isAuto ? '0 0 auto' : 1,\n ...(isAuto && {maxWidth: AUTO_CONTROL_MAX_SHARE}),\n ...(isAuto && ownerState?.labelPlacement === FORM_LABEL_PLACEMENT.topOnPhone && {[theme.breakpoints.down('sm')]: {maxWidth: 'none'}}),\n minWidth: 0, display: 'flex', flexDirection: 'column', justifyContent: 'center', minHeight: 40,\n };\n});\n\nconst FormSectionRoot = styled(Box, {name: SECTION_THEME_NAME, slot: 'Root'})({});\n\nconst FormSectionTitle = styled(Typography, {name: SECTION_THEME_NAME, slot: 'Title'})(({theme}) => theme.unstable_sx({px: 2, mb: 0.75, fontWeight: 600}));\n\n// la tarjeta: la línea de arriba de cada renglón, desde donde empieza el texto (la del primero la recorta la tarjeta)\nconst FormSectionCard = styled(Paper, {name: SECTION_THEME_NAME, slot: 'Card'})(({theme}) => ({\n borderRadius: PANEL_RADIUS,\n overflow: 'hidden',\n [`& .${FORM_ROW_CLASS}::before`]: {\n content: '\"\"', position: 'absolute', top: 0, right: 0, left: theme.spacing(2), height: '1px',\n backgroundColor: (theme.vars ?? theme).palette.divider,\n },\n}));\n\nconst FormSectionFooter = styled(Typography, {name: SECTION_THEME_NAME, slot: 'Footer'})(({theme}) => theme.unstable_sx({px: 2, mt: 0.75}));\n\n/**\n * Un renglón del formulario agrupado: la etiqueta a la izquierda y lo que sea a la derecha (un campo, un interruptor, un\n * texto). Los campos del framework lo usan solos dentro de una FormSection; FormRow suelto es para lo demás.\n *\n * @param {object} props\n * @param {import('react').ReactNode} props.label ya traducida\n * @param {string} [props.htmlFor] el id del campo que nombra la etiqueta\n * @param {string} [props.labelId] el id de la etiqueta (para un campo que la nombra con aria-labelledby)\n * @param {boolean} [props.required] el asterisco en la etiqueta (el lector de pantalla no lo lee: el campo ya lo dice)\n * @param {import('react').ReactNode} props.children\n */\nexport function FormRow(props) {\n // lo que el proyecto puso en su tema (components.CalcitaFormRow.defaultProps) y encima lo que llega aquí\n const {label, htmlFor, labelId, required = false, children, ...unknownProps} = useThemeProps({props, name: ROW_THEME_NAME});\n warnUnknownProps(ROW_THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common');\n // dónde va la etiqueta lo pide la sección; un FormRow fuera de una sección, a la izquierda\n const sectionLayout = useFormSectionLayout();\n const ownerState = {\n labelPlacement: sectionLayout ? sectionLayout.labelPlacement : FORM_LABEL_PLACEMENT.side,\n labelWidth: sectionLayout ? sectionLayout.labelWidth : undefined,\n requiredMark: required ? t('form.requiredMark') : null,\n };\n return (\n <FormRowRoot className={FORM_ROW_CLASS} ownerState={ownerState}>\n <FormRowLabel component=\"label\" id={labelId} htmlFor={htmlFor} variant=\"body1\" ownerState={ownerState}\n {...(required && {[REQUIRED_LABEL_ATTRIBUTE]: 'true'})}>\n {label}\n </FormRowLabel>\n <FormRowControl ownerState={ownerState}>\n {children}\n </FormRowControl>\n </FormRowRoot>\n );\n}\n\n/**\n * @param {object} props\n * @param {string} [props.title] ya traducido; va arriba de la tarjeta\n * @param {import('react').ReactNode} [props.footer] la explicación, abajo de la tarjeta\n * @param {string} [props.labelPlacement] uno de FORM_LABEL_PLACEMENT: side (por omisión, la etiqueta a la izquierda) o\n * topOnPhone (arriba del campo por debajo de `sm`)\n * @param {'auto'|string|number} [props.labelWidth] lo ancho de la columna de las etiquetas: FORM_LABEL_WIDTH.auto (la etiqueta\n * se lleva lo que sobre y el campo lo que ocupa), un ancho de CSS ('320px', '50%') o un número en px; por omisión el 38 %\n * del renglón hasta 200 px, como siempre\n * @param {import('@mui/material/styles').SxProps<import('@mui/material/styles').Theme>} [props.sx] los estilos de la sección\n * en esta pantalla, encima de los del tema (como en las demás piezas)\n * @param {import('react').ReactNode} props.children los campos, directo o dentro de un Grid, un Stack o un componente propio\n */\nexport default function FormSection(props) {\n // lo que el proyecto puso en su tema (components.CalcitaFormSection.defaultProps) y encima lo que llega aquí\n const {\n title, footer, labelPlacement = FORM_LABEL_PLACEMENT.side, labelWidth, sx, children, ...unknownProps\n } = useThemeProps({props, name: SECTION_THEME_NAME});\n warnUnknownProps(SECTION_THEME_NAME, unknownProps);\n const titleId = useId();\n const layout = useMemo(() => ({labelPlacement, labelWidth}), [labelPlacement, labelWidth]);\n return (\n <FormSectionContext value={layout}>\n <FormSectionRoot component=\"section\" aria-labelledby={title ? titleId : undefined} sx={sx}>\n {title && (\n <FormSectionTitle id={titleId} variant=\"subtitle2\" component=\"h2\" color=\"text.secondary\">\n {title}\n </FormSectionTitle>\n )}\n <FormSectionCard variant=\"outlined\">\n {/* un píxel arriba: la línea de los renglones de hasta arriba queda fuera de la tarjeta (overflow hidden) */}\n <Box sx={{mt: '-1px'}}>{children}</Box>\n </FormSectionCard>\n {footer && (\n <FormSectionFooter variant=\"caption\" component=\"p\" color=\"text.secondary\">{footer}</FormSectionFooter>\n )}\n </FormSectionRoot>\n </FormSectionContext>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from "react";
|
|
3
4
|
import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
|
|
4
5
|
import Box from "@mui/material/Box";
|
|
5
|
-
import Button from "@mui/material/Button";
|
|
6
|
+
import Button, { buttonClasses } from "@mui/material/Button";
|
|
6
7
|
import Stack from "@mui/material/Stack";
|
|
7
8
|
import Step from "@mui/material/Step";
|
|
8
9
|
import StepButton from "@mui/material/StepButton";
|
|
9
10
|
import StepContent from "@mui/material/StepContent";
|
|
10
11
|
import StepLabel from "@mui/material/StepLabel";
|
|
11
12
|
import Stepper from "@mui/material/Stepper";
|
|
13
|
+
import Tooltip from "@mui/material/Tooltip";
|
|
12
14
|
import Typography from "@mui/material/Typography";
|
|
13
15
|
import { WIZARD_ORIENTATION, isStepCompleted } from "./wizardSteps.js";
|
|
14
16
|
import { piecePart, withPieceSlots, usePieceProps } from "../theme/themePieces.js";
|
|
@@ -18,12 +20,14 @@ const THEME_NAME = "CalcitaWizardLayout";
|
|
|
18
20
|
const WizardStepper = piecePart(THEME_NAME, "Stepper", Stepper);
|
|
19
21
|
const WizardPhoneSteps = piecePart(THEME_NAME, "PhoneSteps", Box);
|
|
20
22
|
const WizardActions = piecePart(THEME_NAME, "Actions", Stack);
|
|
23
|
+
const WizardNextReason = piecePart(THEME_NAME, "NextReason", Typography);
|
|
21
24
|
function WizardLayoutContent(props) {
|
|
22
25
|
const {
|
|
23
26
|
steps,
|
|
24
27
|
activeIndex,
|
|
25
28
|
skippedKeys = NO_SKIPPED_KEYS,
|
|
26
29
|
isSubmitting = false,
|
|
30
|
+
nextDisabledReason = null,
|
|
27
31
|
orientation = WIZARD_ORIENTATION.horizontal,
|
|
28
32
|
hideBackOnFirstStep = false,
|
|
29
33
|
onBack,
|
|
@@ -37,11 +41,13 @@ function WizardLayoutContent(props) {
|
|
|
37
41
|
} = usePieceProps(props, THEME_NAME);
|
|
38
42
|
warnUnknownProps(THEME_NAME, unknownProps);
|
|
39
43
|
const { t } = useToolkitTranslation("common", texts);
|
|
44
|
+
const reasonId = useId();
|
|
40
45
|
const activeStep = steps[activeIndex];
|
|
41
46
|
const isLastStep = activeIndex === steps.length - 1;
|
|
42
47
|
const isVertical = orientation === WIZARD_ORIENTATION.vertical;
|
|
43
48
|
const isSelectable = typeof onStepSelect === "function";
|
|
44
49
|
const isBackHidden = hideBackOnFirstStep && activeIndex === 0;
|
|
50
|
+
const isNextBlocked = Boolean(nextDisabledReason);
|
|
45
51
|
const stepTitleOf = (step, index) => {
|
|
46
52
|
let caption;
|
|
47
53
|
if (skippedKeys.has(step.key)) {
|
|
@@ -55,17 +61,32 @@ function WizardLayoutContent(props) {
|
|
|
55
61
|
}
|
|
56
62
|
return /* @__PURE__ */ jsx(StepLabel, { optional, children: step.label });
|
|
57
63
|
};
|
|
58
|
-
const
|
|
64
|
+
const nextButton = isLastStep ? submitButton : /* @__PURE__ */ jsx(
|
|
65
|
+
Button,
|
|
66
|
+
{
|
|
67
|
+
variant: "contained",
|
|
68
|
+
size: "large",
|
|
69
|
+
onClick: isNextBlocked ? void 0 : onNext,
|
|
70
|
+
...isNextBlocked && { "aria-disabled": true, "aria-describedby": reasonId, className: buttonClasses.disabled },
|
|
71
|
+
children: t("wizard.next")
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
const forwardButton = isNextBlocked ? /* @__PURE__ */ jsx(Tooltip, { title: nextDisabledReason, describeChild: true, disableTouchListener: true, children: /* @__PURE__ */ jsx(Box, { component: "span", sx: { display: "flex", flexDirection: "column" }, children: nextButton }) }) : nextButton;
|
|
75
|
+
const buttons = (
|
|
59
76
|
// sin «Anterior», los de seguir se quedan a la derecha (solos, space-between los mandaba a la izquierda; un margen
|
|
60
77
|
// automático no sirve: Stack les pone margin 0 a sus hijos con más fuerza)
|
|
61
78
|
/* @__PURE__ */ jsxs(WizardActions, { direction: { xs: "column-reverse", sm: "row" }, spacing: 1.5, ownSx: { justifyContent: isBackHidden ? "flex-end" : "space-between" }, children: [
|
|
62
79
|
!isBackHidden && /* @__PURE__ */ jsx(Button, { variant: "text", color: "inherit", disabled: activeIndex === 0 || isSubmitting, onClick: onBack, children: t("wizard.back") }),
|
|
63
80
|
/* @__PURE__ */ jsxs(Stack, { direction: { xs: "column-reverse", sm: "row" }, spacing: 1.5, children: [
|
|
64
81
|
activeStep.optional && !isLastStep && onSkip && /* @__PURE__ */ jsx(Button, { variant: "text", onClick: onSkip, children: t("wizard.skip") }),
|
|
65
|
-
|
|
82
|
+
forwardButton
|
|
66
83
|
] })
|
|
67
84
|
] })
|
|
68
85
|
);
|
|
86
|
+
const actions = isNextBlocked ? /* @__PURE__ */ jsxs(Stack, { spacing: 1, useFlexGap: true, children: [
|
|
87
|
+
/* @__PURE__ */ jsx(WizardNextReason, { id: reasonId, variant: "body2", color: "text.secondary", ownSx: { display: { xs: "block", sm: "none" } }, children: nextDisabledReason }),
|
|
88
|
+
buttons
|
|
89
|
+
] }) : buttons;
|
|
69
90
|
const completedOf = (step, index) => isStepCompleted(step, { index, activeIndex, skippedKeys, isSelectable });
|
|
70
91
|
if (isVertical) {
|
|
71
92
|
return (
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WizardLayout.js","sources":["../../src/inputs/WizardLayout.jsx"],"sourcesContent":["// Lo que se ve de un formulario por pasos: los pasos, los campos del paso abierto y los botones (Anterior · Omitir este paso ·\n// Siguiente o Enviar). No sabe de formik ni de react-hook-form: el FormWizard de cada uno (/forms y /react-hook-form) pone la\n// lógica (qué revisa «Siguiente», qué hace «Omitir», si se puede saltar a otro paso) y le pasa su botón de enviar y su aviso de\n// errores. Suelto sirve para armar un asistente propio con otra librería.\n//\n// Dos acomodos (WIZARD_ORIENTATION):\n// horizontal los pasos en fila arriba y los campos abajo; en el teléfono no caben los nombres: «Paso 2 de 4» y su nombre;\n// vertical uno debajo del otro, como un acordeón: los campos y los botones van dentro del paso abierto (también en el\n// teléfono). Es el de editar un expediente: se ve de un vistazo qué pasos hay y se entra al que hace falta.\n//\n// Con onStepSelect los títulos de los pasos se pulsan (con teclado también: son botones) y avisan qué paso se pidió; quien lo\n// usa decide si salta. Sin él, los títulos solo informan (lo de siempre). Omitir es opcional: sin onSkip no sale el botón.\n//\n// Los textos se cambian con `texts`, con la forma de `common` en los textos del paquete: {wizard: {next: 'Continuar'}}.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaWizardLayout con defaultProps y\n// styleOverrides de sus partes: stepper, phoneSteps, actions.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Stack from '@mui/material/Stack';\nimport Step from '@mui/material/Step';\nimport StepButton from '@mui/material/StepButton';\nimport StepContent from '@mui/material/StepContent';\nimport StepLabel from '@mui/material/StepLabel';\nimport Stepper from '@mui/material/Stepper';\nimport Typography from '@mui/material/Typography';\nimport {WIZARD_ORIENTATION, isStepCompleted} from '@/inputs/wizardSteps.js';\nimport {piecePart, usePieceProps, withPieceSlots} from '@/theme/themePieces.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n// sin pasos omitidos (quien no usa «Omitir» no tiene que mandar nada)\nconst NO_SKIPPED_KEYS = new Set();\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaWizardLayout)\nconst THEME_NAME = 'CalcitaWizardLayout';\nconst WizardStepper = piecePart(THEME_NAME, 'Stepper', Stepper);\nconst WizardPhoneSteps = piecePart(THEME_NAME, 'PhoneSteps', Box);\nconst WizardActions = piecePart(THEME_NAME, 'Actions', Stack);\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 {boolean} [props.steps[].optional] se puede omitir\n * @param {boolean} [props.steps[].completed] lleva la palomita; por omisión, en fila y sin pulsar, los de antes del abierto\n * @param {import('react').ReactNode} props.steps[].content los campos del paso\n * @param {number} props.activeIndex el paso que se ve\n * @param {Set<string>} [props.skippedKeys] los pasos omitidos\n * @param {boolean} [props.isSubmitting] mientras se envía, «Anterior» se apaga\n * @param {'horizontal'|'vertical'} [props.orientation] uno de WIZARD_ORIENTATION; por omisión horizontal\n * @param {boolean} [props.hideBackOnFirstStep] en el primer paso no se pinta «Anterior» (no hay a dónde volver); por omisión\n * sale apagado, como antes\n * @param {() => void} props.onBack\n * @param {() => void} props.onNext\n * @param {() => void} [props.onSkip] sin él no sale «Omitir este paso»\n * @param {(index: number) => void} [props.onStepSelect] con él, los títulos de los pasos se pulsan y avisan cuál se pidió\n * @param {import('react').ReactNode} props.submitButton el botón de enviar del último paso\n * @param {import('react').ReactNode} [props.problemAlert] el aviso de errores del servidor, arriba de los botones 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 */\nfunction WizardLayoutContent(props) {\n // lo que el proyecto puso en su tema (components.CalcitaWizardLayout.defaultProps) y encima lo que llega aquí\n const {\n steps, activeIndex, skippedKeys = NO_SKIPPED_KEYS, isSubmitting = false, orientation = WIZARD_ORIENTATION.horizontal,\n hideBackOnFirstStep = false, onBack, onNext, onSkip, onStepSelect, submitButton, problemAlert, texts,\n ...unknownProps\n } = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n const activeStep = steps[activeIndex];\n const isLastStep = activeIndex === steps.length - 1;\n const isVertical = orientation === WIZARD_ORIENTATION.vertical;\n const isSelectable = typeof onStepSelect === 'function';\n const isBackHidden = hideBackOnFirstStep && activeIndex === 0;\n\n // el título de un paso: con onStepSelect es un botón (se pulsa y se llega con Tab); sin él, solo informa\n const stepTitleOf = (step, index) => {\n let caption;\n if (skippedKeys.has(step.key)) {\n caption = t('wizard.skipped');\n } else if (step.optional) {\n caption = t('wizard.optional');\n }\n const optional = caption && <Typography variant=\"caption\" color=\"text.secondary\">{caption}</Typography>;\n if (isSelectable) {\n return <StepButton optional={optional} onClick={() => onStepSelect(index)}>{step.label}</StepButton>;\n }\n return <StepLabel optional={optional}>{step.label}</StepLabel>;\n };\n\n // los botones del paso abierto: en fila van abajo de los campos; en vertical, dentro del paso\n const actions = (\n // sin «Anterior», los de seguir se quedan a la derecha (solos, space-between los mandaba a la izquierda; un margen\n // automático no sirve: Stack les pone margin 0 a sus hijos con más fuerza)\n <WizardActions direction={{xs: 'column-reverse', sm: 'row'}} spacing={1.5} ownSx={{justifyContent: isBackHidden ? 'flex-end' : 'space-between'}}>\n {!isBackHidden && (\n <Button variant=\"text\" color=\"inherit\" disabled={activeIndex === 0 || isSubmitting} onClick={onBack}>\n {t('wizard.back')}\n </Button>\n )}\n <Stack direction={{xs: 'column-reverse', sm: 'row'}} spacing={1.5}>\n {activeStep.optional && !isLastStep && onSkip && (\n <Button variant=\"text\" onClick={onSkip}>{t('wizard.skip')}</Button>\n )}\n {isLastStep\n ? submitButton\n : <Button variant=\"contained\" size=\"large\" onClick={onNext}>{t('wizard.next')}</Button>}\n </Stack>\n </WizardActions>\n );\n const completedOf = (step, index) => isStepCompleted(step, {index, activeIndex, skippedKeys, isSelectable});\n\n if (isVertical) {\n return (\n // nonLinear: con los títulos que se pulsan, MUI no debe apagar los pasos de adelante\n <WizardStepper activeStep={activeIndex} orientation=\"vertical\" nonLinear={isSelectable}>\n {steps.map((step, index) => (\n <Step key={step.key} completed={completedOf(step, index)}>\n {stepTitleOf(step, index)}\n {/* solo el paso abierto tiene sus campos montados: al cerrarse se desmontan (unmountOnExit). Los botones\n van solo en el abierto: el que se cierra (la animación dura un momento) ya no es el paso de esos\n botones, y un «Guardar» repetido confunde */}\n <StepContent slotProps={{transition: {unmountOnExit: true}}}>\n <Stack spacing={3} sx={{pt: 1}}>\n <Box>{step.content}</Box>\n {index === activeIndex && isLastStep && problemAlert}\n {index === activeIndex && actions}\n </Stack>\n </StepContent>\n </Step>\n ))}\n </WizardStepper>\n );\n }\n\n return (\n <Stack spacing={3}>\n {/* en el teléfono los nombres de los pasos no caben: se dice en qué paso va */}\n <WizardStepper activeStep={activeIndex} alternativeLabel nonLinear={isSelectable} ownSx={{display: {xs: 'none', sm: 'flex'}}}>\n {steps.map((step, index) => (\n <Step key={step.key} completed={completedOf(step, index)}>\n {stepTitleOf(step, index)}\n </Step>\n ))}\n </WizardStepper>\n <WizardPhoneSteps ownSx={{display: {xs: 'block', sm: 'none'}}}>\n <Typography variant=\"overline\" color=\"text.secondary\">{t('wizard.stepOf', {current: activeIndex + 1, total: steps.length})}</Typography>\n <Typography variant=\"h6\">{activeStep.label}</Typography>\n </WizardPhoneSteps>\n\n <Box>{activeStep.content}</Box>\n\n {isLastStep && problemAlert}\n\n {actions}\n </Stack>\n );\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst WizardLayout = withPieceSlots(WizardLayoutContent, THEME_NAME);\nexport default WizardLayout;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WizardLayout.js","sources":["../../src/inputs/WizardLayout.jsx"],"sourcesContent":["// Lo que se ve de un formulario por pasos: los pasos, los campos del paso abierto y los botones (Anterior · Omitir este paso ·\n// Siguiente o Enviar). No sabe de formik ni de react-hook-form: el FormWizard de cada uno (/forms y /react-hook-form) pone la\n// lógica (qué revisa «Siguiente», qué hace «Omitir», si se puede saltar a otro paso) y le pasa su botón de enviar y su aviso de\n// errores. Suelto sirve para armar un asistente propio con otra librería.\n//\n// Dos acomodos (WIZARD_ORIENTATION):\n// horizontal los pasos en fila arriba y los campos abajo; en el teléfono no caben los nombres: «Paso 2 de 4» y su nombre;\n// vertical uno debajo del otro, como un acordeón: los campos y los botones van dentro del paso abierto (también en el\n// teléfono). Es el de editar un expediente: se ve de un vistazo qué pasos hay y se entra al que hace falta.\n//\n// Con onStepSelect los títulos de los pasos se pulsan (con teclado también: son botones) y avisan qué paso se pidió; quien lo\n// usa decide si salta. Sin él, los títulos solo informan (lo de siempre). Omitir es opcional: sin onSkip no sale el botón.\n//\n// Un paso que todavía no se puede dejar (faltan documentos obligatorios) lo dice con nextDisabledReason, ya traducido: «Siguiente»\n// se ve apagado y no avanza, y el motivo sale en un aviso al pasar el cursor o al llegar con Tab. OJO: el botón NO lleva\n// `disabled`: un botón apagado así no recibe el foco y con el teclado no habría cómo saber por qué; va con aria-disabled y la\n// clase de apagado de MUI (se ve igual que uno apagado, también con el tema). En el teléfono un aviso no sale con el dedo: el\n// motivo va escrito arriba de los botones. En el último paso el botón de enviar es de quien lo pasa en submitButton: él lo\n// apaga (el FormWizard de formik y el de react-hook-form ya lo hacen); el motivo sale igual.\n//\n// Los textos se cambian con `texts`, con la forma de `common` en los textos del paquete: {wizard: {next: 'Continuar'}}.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaWizardLayout con defaultProps y\n// styleOverrides de sus partes: stepper, phoneSteps, actions, nextReason (el motivo escrito en el teléfono).\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useId} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Box from '@mui/material/Box';\nimport Button, {buttonClasses} from '@mui/material/Button';\nimport Stack from '@mui/material/Stack';\nimport Step from '@mui/material/Step';\nimport StepButton from '@mui/material/StepButton';\nimport StepContent from '@mui/material/StepContent';\nimport StepLabel from '@mui/material/StepLabel';\nimport Stepper from '@mui/material/Stepper';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport {WIZARD_ORIENTATION, isStepCompleted} from '@/inputs/wizardSteps.js';\nimport {piecePart, usePieceProps, withPieceSlots} from '@/theme/themePieces.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n// sin pasos omitidos (quien no usa «Omitir» no tiene que mandar nada)\nconst NO_SKIPPED_KEYS = new Set();\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaWizardLayout)\nconst THEME_NAME = 'CalcitaWizardLayout';\nconst WizardStepper = piecePart(THEME_NAME, 'Stepper', Stepper);\nconst WizardPhoneSteps = piecePart(THEME_NAME, 'PhoneSteps', Box);\nconst WizardActions = piecePart(THEME_NAME, 'Actions', Stack);\nconst WizardNextReason = piecePart(THEME_NAME, 'NextReason', Typography);\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 {boolean} [props.steps[].optional] se puede omitir\n * @param {boolean} [props.steps[].completed] lleva la palomita; por omisión, en fila y sin pulsar, los de antes del abierto\n * @param {import('react').ReactNode} props.steps[].content los campos del paso\n * @param {number} props.activeIndex el paso que se ve\n * @param {Set<string>} [props.skippedKeys] los pasos omitidos\n * @param {boolean} [props.isSubmitting] mientras se envía, «Anterior» se apaga\n * @param {import('react').ReactNode} [props.nextDisabledReason] ya traducido: el paso abierto todavía no se puede dejar\n * («Faltan 2 documentos»). «Siguiente» se ve apagado y no avanza, y el motivo sale en un aviso (escrito, en el teléfono).\n * En el último paso, apague usted su submitButton. Sin él, como siempre\n * @param {'horizontal'|'vertical'} [props.orientation] uno de WIZARD_ORIENTATION; por omisión horizontal\n * @param {boolean} [props.hideBackOnFirstStep] en el primer paso no se pinta «Anterior» (no hay a dónde volver); por omisión\n * sale apagado, como antes\n * @param {() => void} props.onBack\n * @param {() => void} props.onNext\n * @param {() => void} [props.onSkip] sin él no sale «Omitir este paso»\n * @param {(index: number) => void} [props.onStepSelect] con él, los títulos de los pasos se pulsan y avisan cuál se pidió\n * @param {import('react').ReactNode} props.submitButton el botón de enviar del último paso\n * @param {import('react').ReactNode} [props.problemAlert] el aviso de errores del servidor, arriba de los botones 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 */\nfunction WizardLayoutContent(props) {\n // lo que el proyecto puso en su tema (components.CalcitaWizardLayout.defaultProps) y encima lo que llega aquí\n const {\n steps, activeIndex, skippedKeys = NO_SKIPPED_KEYS, isSubmitting = false, nextDisabledReason = null,\n orientation = WIZARD_ORIENTATION.horizontal, hideBackOnFirstStep = false, onBack, onNext, onSkip, onStepSelect, submitButton,\n problemAlert, texts, ...unknownProps\n } = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n const reasonId = useId();\n const activeStep = steps[activeIndex];\n const isLastStep = activeIndex === steps.length - 1;\n const isVertical = orientation === WIZARD_ORIENTATION.vertical;\n const isSelectable = typeof onStepSelect === 'function';\n const isBackHidden = hideBackOnFirstStep && activeIndex === 0;\n const isNextBlocked = Boolean(nextDisabledReason);\n\n // el título de un paso: con onStepSelect es un botón (se pulsa y se llega con Tab); sin él, solo informa\n const stepTitleOf = (step, index) => {\n let caption;\n if (skippedKeys.has(step.key)) {\n caption = t('wizard.skipped');\n } else if (step.optional) {\n caption = t('wizard.optional');\n }\n const optional = caption && <Typography variant=\"caption\" color=\"text.secondary\">{caption}</Typography>;\n if (isSelectable) {\n return <StepButton optional={optional} onClick={() => onStepSelect(index)}>{step.label}</StepButton>;\n }\n return <StepLabel optional={optional}>{step.label}</StepLabel>;\n };\n\n // el botón de seguir: «Siguiente», o el de enviar en el último paso. Con un motivo para no avanzar, «Siguiente» se ve\n // apagado pero se sigue alcanzando con Tab (aria-disabled, no disabled: ver el encabezado) y no hace nada al pulsarlo\n const nextButton = isLastStep ? submitButton : (\n <Button variant=\"contained\" size=\"large\" onClick={isNextBlocked ? undefined : onNext}\n {...(isNextBlocked && {'aria-disabled': true, 'aria-describedby': reasonId, className: buttonClasses.disabled})}>\n {t('wizard.next')}\n </Button>\n );\n // el aviso va sobre una caja alrededor del botón: un botón que se ve apagado no recibe el cursor (MUI le quita los eventos)\n // y la caja sí. En el teléfono no se abre con el dedo: ahí el motivo va escrito\n const forwardButton = isNextBlocked ? (\n <Tooltip title={nextDisabledReason} describeChild disableTouchListener>\n <Box component=\"span\" sx={{display: 'flex', flexDirection: 'column'}}>{nextButton}</Box>\n </Tooltip>\n ) : nextButton;\n\n // los botones del paso abierto: en fila van abajo de los campos; en vertical, dentro del paso\n const buttons = (\n // sin «Anterior», los de seguir se quedan a la derecha (solos, space-between los mandaba a la izquierda; un margen\n // automático no sirve: Stack les pone margin 0 a sus hijos con más fuerza)\n <WizardActions direction={{xs: 'column-reverse', sm: 'row'}} spacing={1.5} ownSx={{justifyContent: isBackHidden ? 'flex-end' : 'space-between'}}>\n {!isBackHidden && (\n <Button variant=\"text\" color=\"inherit\" disabled={activeIndex === 0 || isSubmitting} onClick={onBack}>\n {t('wizard.back')}\n </Button>\n )}\n <Stack direction={{xs: 'column-reverse', sm: 'row'}} spacing={1.5}>\n {activeStep.optional && !isLastStep && onSkip && (\n <Button variant=\"text\" onClick={onSkip}>{t('wizard.skip')}</Button>\n )}\n {forwardButton}\n </Stack>\n </WizardActions>\n );\n // con un motivo, escrito arriba de los botones en el teléfono (en pantallas anchas lo dice el aviso). Sin motivo, los\n // botones solos, como siempre. useFlexGap: el motivo escondido no deja hueco arriba de los botones\n const actions = isNextBlocked ? (\n <Stack spacing={1} useFlexGap>\n <WizardNextReason id={reasonId} variant=\"body2\" color=\"text.secondary\" ownSx={{display: {xs: 'block', sm: 'none'}}}>\n {nextDisabledReason}\n </WizardNextReason>\n {buttons}\n </Stack>\n ) : buttons;\n const completedOf = (step, index) => isStepCompleted(step, {index, activeIndex, skippedKeys, isSelectable});\n\n if (isVertical) {\n return (\n // nonLinear: con los títulos que se pulsan, MUI no debe apagar los pasos de adelante\n <WizardStepper activeStep={activeIndex} orientation=\"vertical\" nonLinear={isSelectable}>\n {steps.map((step, index) => (\n <Step key={step.key} completed={completedOf(step, index)}>\n {stepTitleOf(step, index)}\n {/* solo el paso abierto tiene sus campos montados: al cerrarse se desmontan (unmountOnExit). Los botones\n van solo en el abierto: el que se cierra (la animación dura un momento) ya no es el paso de esos\n botones, y un «Guardar» repetido confunde */}\n <StepContent slotProps={{transition: {unmountOnExit: true}}}>\n <Stack spacing={3} sx={{pt: 1}}>\n <Box>{step.content}</Box>\n {index === activeIndex && isLastStep && problemAlert}\n {index === activeIndex && actions}\n </Stack>\n </StepContent>\n </Step>\n ))}\n </WizardStepper>\n );\n }\n\n return (\n <Stack spacing={3}>\n {/* en el teléfono los nombres de los pasos no caben: se dice en qué paso va */}\n <WizardStepper activeStep={activeIndex} alternativeLabel nonLinear={isSelectable} ownSx={{display: {xs: 'none', sm: 'flex'}}}>\n {steps.map((step, index) => (\n <Step key={step.key} completed={completedOf(step, index)}>\n {stepTitleOf(step, index)}\n </Step>\n ))}\n </WizardStepper>\n <WizardPhoneSteps ownSx={{display: {xs: 'block', sm: 'none'}}}>\n <Typography variant=\"overline\" color=\"text.secondary\">{t('wizard.stepOf', {current: activeIndex + 1, total: steps.length})}</Typography>\n <Typography variant=\"h6\">{activeStep.label}</Typography>\n </WizardPhoneSteps>\n\n <Box>{activeStep.content}</Box>\n\n {isLastStep && problemAlert}\n\n {actions}\n </Stack>\n );\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst WizardLayout = withPieceSlots(WizardLayoutContent, THEME_NAME);\nexport default WizardLayout;\n"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { createContext, useContext } from "react";
|
|
3
3
|
const FormSectionContext = createContext(
|
|
4
|
-
/** @type {false|
|
|
4
|
+
/** @type {false|FormSectionLayout} */
|
|
5
5
|
false
|
|
6
6
|
);
|
|
7
7
|
const useIsInsideFormSection = () => Boolean(useContext(FormSectionContext));
|