innoboxrr-react-form-elements 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +125 -0
  2. package/index.js +42 -0
  3. package/package.json +71 -0
  4. package/src/AvatarInputComponent.jsx +78 -0
  5. package/src/ButtonComponent.jsx +44 -0
  6. package/src/CheckboxInputComponent.jsx +55 -0
  7. package/src/ClickToEditComponent.jsx +65 -0
  8. package/src/CodeInputComponent.jsx +108 -0
  9. package/src/CodeMirrorComponent.jsx +62 -0
  10. package/src/ColorPickerInputComponent.jsx +85 -0
  11. package/src/CountrySelectInputComponent.jsx +89 -0
  12. package/src/DynamicGroupInputComponent.jsx +216 -0
  13. package/src/EditorInputComponent.jsx +52 -0
  14. package/src/FileDropInputComponent.jsx +51 -0
  15. package/src/FileInputComponent.jsx +186 -0
  16. package/src/FqsInputComponent.jsx +72 -0
  17. package/src/InputErrorComponent.jsx +26 -0
  18. package/src/ModelSearchInputComponent.jsx +107 -0
  19. package/src/MultiCheckboxInputComponent.jsx +35 -0
  20. package/src/PolymorphicInputComponent.jsx +181 -0
  21. package/src/RadioInputComponent.jsx +38 -0
  22. package/src/SelectInputComponent.jsx +51 -0
  23. package/src/SelectSearchInputComponent.jsx +102 -0
  24. package/src/SimpleFileInputComponent.jsx +40 -0
  25. package/src/SingleCheckboxInputComponent.jsx +33 -0
  26. package/src/StarsInputComponent.jsx +49 -0
  27. package/src/SwitchComponent.jsx +25 -0
  28. package/src/TagsInputComponent.jsx +88 -0
  29. package/src/TextEditorMonoStyleInputComponent.jsx +27 -0
  30. package/src/TextInputComponent.jsx +117 -0
  31. package/src/TextareaInputComponent.jsx +50 -0
  32. package/src/TimezoneSelectInputComponent.jsx +32 -0
  33. package/src/css/form-elements.css +187 -0
  34. package/src/internal/Field.jsx +21 -0
  35. package/src/internal/FieldLabel.jsx +29 -0
  36. package/src/internal/useControlled.js +38 -0
  37. package/src/internal/useThemeClass.js +22 -0
@@ -0,0 +1,72 @@
1
+ import TextInputComponent from './TextInputComponent.jsx'
2
+ import TextareaInputComponent from './TextareaInputComponent.jsx'
3
+ import useControlled from './internal/useControlled.js'
4
+
5
+ const DEFAULT_LABELS = {
6
+ title: 'Add frequency asked questions',
7
+ question: 'Question',
8
+ answer: 'Answer',
9
+ add: 'Add Question',
10
+ remove: 'Remove question',
11
+ }
12
+
13
+ /**
14
+ * Gemelo de FqsInputComponent.vue: la lista de preguntas frecuentes.
15
+ *
16
+ * El valor es un array de `{ question, answer }` y nunca se muta en sitio: se
17
+ * emite uno nuevo, como ya hace la versión Vue corregida.
18
+ */
19
+ export default function FqsInputComponent({
20
+ value,
21
+ onChange,
22
+ inputClass = 'fe-input ',
23
+ labels = DEFAULT_LABELS,
24
+ name = 'fqs',
25
+ }) {
26
+ const [current, set] = useControlled(value, onChange, [])
27
+
28
+ const items = current ?? []
29
+ const text = { ...DEFAULT_LABELS, ...labels }
30
+
31
+ const updateAt = (index, field, next) => set(
32
+ items.map((item, position) => (position === index ? { ...item, [field]: next } : item))
33
+ )
34
+
35
+ return (
36
+ <div>
37
+ <h4>{text.title}</h4>
38
+
39
+ {items.map((item, index) => (
40
+ <div key={index} className="fe-card fe-card-sm fe-card-body fe-mb">
41
+ <TextInputComponent
42
+ type="text"
43
+ name={`${name}[${index}][question]`}
44
+ label={text.question}
45
+ customClass={inputClass}
46
+ value={item.question ?? ''}
47
+ onChange={(next) => updateAt(index, 'question', next)} />
48
+
49
+ <TextareaInputComponent
50
+ name={`${name}[${index}][answer]`}
51
+ label={text.answer}
52
+ value={item.answer ?? ''}
53
+ onChange={(next) => updateAt(index, 'answer', next)} />
54
+
55
+ <button
56
+ type="button"
57
+ className="fe-button fe-button-danger fe-button-sm"
58
+ onClick={() => set(items.filter((_, position) => position !== index))}>
59
+ {text.remove}
60
+ </button>
61
+ </div>
62
+ ))}
63
+
64
+ <button
65
+ type="button"
66
+ className="fe-button"
67
+ onClick={() => set([...items, { question: '', answer: '' }])}>
68
+ {text.add}
69
+ </button>
70
+ </div>
71
+ )
72
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Gemelo de InputErrorComponent.vue.
3
+ *
4
+ * `errors` es el objeto que devuelve Laravel en un 422 (`{ campo: [...] }`) y
5
+ * `type` la clave que mira este control.
6
+ */
7
+ import { useSyncExternalStore } from 'react'
8
+ import { getTheme, onThemeChange } from 'innoboxrr-form-core'
9
+
10
+ export default function InputErrorComponent({ errors, type }) {
11
+ const theme = useSyncExternalStore(onThemeChange, () => getTheme(), () => getTheme())
12
+
13
+ const messages = errors?.[type]
14
+
15
+ if (! messages?.length) {
16
+ return null
17
+ }
18
+
19
+ return (
20
+ <div>
21
+ {messages.map((error) => (
22
+ <p key={error} className={theme.error}>{error}</p>
23
+ ))}
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,107 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+ import SelectSearchInputComponent from './SelectSearchInputComponent.jsx'
3
+
4
+ /**
5
+ * Gemelo de ModelSearchInputComponent.vue: busca contra un endpoint y deja
6
+ * elegir un registro.
7
+ *
8
+ * `onSubmit` recibe el id elegido y `onSelected` el registro completo, igual
9
+ * que los eventos `submit` y `selected` de la versión Vue.
10
+ */
11
+ export default function ModelSearchInputComponent({
12
+ customClass = null,
13
+ hideOnEmit = false,
14
+ labelStr,
15
+ placeholderStr,
16
+ route,
17
+ method = 'get',
18
+ q = 'id',
19
+ externalFilters = {},
20
+ reduce = (option) => option.id,
21
+ optionLabel = 'name',
22
+ multiple = false,
23
+ minLength = 1,
24
+ debounce = 300,
25
+ value,
26
+ onSubmit,
27
+ onSelected,
28
+ }) {
29
+ const [options, setOptions] = useState([])
30
+ const [selected, setSelected] = useState(value ?? null)
31
+ const [visible, setVisible] = useState(true)
32
+ const timer = useRef(null)
33
+
34
+ const search = useCallback(async (term) => {
35
+ if (term.length < minLength) {
36
+ setOptions([])
37
+
38
+ return
39
+ }
40
+
41
+ const params = { ...externalFilters, [q]: term }
42
+
43
+ try {
44
+ if (method.toLowerCase() === 'get') {
45
+ const url = new URL(route, globalThis.location?.origin ?? 'http://localhost')
46
+
47
+ Object.entries(params).forEach(([key, item]) => url.searchParams.set(key, item))
48
+
49
+ const response = await fetch(url, { headers: { Accept: 'application/json' } })
50
+
51
+ setOptions((await response.json())?.data ?? [])
52
+
53
+ return
54
+ }
55
+
56
+ const response = await fetch(route, {
57
+ method: method.toUpperCase(),
58
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
59
+ body: JSON.stringify(params),
60
+ })
61
+
62
+ setOptions((await response.json())?.data ?? [])
63
+ } catch {
64
+ setOptions([])
65
+ }
66
+ }, [route, method, q, externalFilters, minLength])
67
+
68
+ // Un fetch por tecla satura el endpoint; la version Vue lo dejaba al
69
+ // criterio de quien montara el componente.
70
+ useEffect(() => () => window.clearTimeout(timer.current), [])
71
+
72
+ if (! visible) {
73
+ return null
74
+ }
75
+
76
+ return (
77
+ <SelectSearchInputComponent
78
+ customClass={customClass ?? undefined}
79
+ name={q}
80
+ inputLabel={labelStr}
81
+ placeholder={placeholderStr}
82
+ options={options}
83
+ label={optionLabel}
84
+ reduce={reduce}
85
+ value={selected}
86
+ onSearch={(term) => {
87
+ window.clearTimeout(timer.current)
88
+ timer.current = window.setTimeout(() => search(term), debounce)
89
+ }}
90
+ onChange={(next) => {
91
+ setSelected(next)
92
+ onSubmit?.(next)
93
+
94
+ if (! multiple && next != null) {
95
+ const record = options.find((option) => reduce(option) === next)
96
+
97
+ if (record) {
98
+ onSelected?.(record)
99
+ }
100
+
101
+ if (hideOnEmit) {
102
+ setVisible(false)
103
+ }
104
+ }
105
+ }} />
106
+ )
107
+ }
@@ -0,0 +1,35 @@
1
+ import SingleCheckboxInputComponent from './SingleCheckboxInputComponent.jsx'
2
+
3
+ /**
4
+ * Gemelo de MultiCheckboxInputComponent.vue.
5
+ *
6
+ * La versión Vue recalculaba la selección leyendo el DOM con
7
+ * `document.querySelectorAll`, lo que ataba el componente a que sus inputs
8
+ * estuvieran montados en el documento y rompía con dos grupos del mismo `id`.
9
+ * Aquí la selección sale del propio valor, que es de donde tenía que salir.
10
+ *
11
+ * @param {{id?: string, value: Array, options: Array<{id: any, name: string}>, onChange?: (value: Array) => void}} props
12
+ */
13
+ export default function MultiCheckboxInputComponent({ id = '', value, options, onChange }) {
14
+ const selected = value ?? []
15
+
16
+ const toggle = (optionId, checked) => {
17
+ onChange?.(checked
18
+ ? [...selected, optionId]
19
+ : selected.filter((item) => item !== optionId))
20
+ }
21
+
22
+ return (
23
+ <div>
24
+ {options.map((option) => (
25
+ <SingleCheckboxInputComponent
26
+ key={option.id}
27
+ id={id}
28
+ label={option.name}
29
+ value={option.id}
30
+ checked={selected.includes(option.id)}
31
+ onCheckedChange={(checked) => toggle(option.id, checked)} />
32
+ ))}
33
+ </div>
34
+ )
35
+ }
@@ -0,0 +1,181 @@
1
+ import { useId, useState } from 'react'
2
+ import CheckboxInputComponent from './CheckboxInputComponent.jsx'
3
+ import EditorInputComponent from './EditorInputComponent.jsx'
4
+ import RadioInputComponent from './RadioInputComponent.jsx'
5
+ import SelectInputComponent from './SelectInputComponent.jsx'
6
+ import SimpleFileInputComponent from './SimpleFileInputComponent.jsx'
7
+ import SwitchComponent from './SwitchComponent.jsx'
8
+ import TextInputComponent from './TextInputComponent.jsx'
9
+ import TextareaInputComponent from './TextareaInputComponent.jsx'
10
+ import useControlled from './internal/useControlled.js'
11
+
12
+ const TEXT_TYPES = ['text', 'number', 'date', 'time', 'url', 'email']
13
+
14
+ /**
15
+ * Gemelo de PolymorphicInputComponent.vue: un solo componente que decide qué
16
+ * control pintar a partir de `config.type`.
17
+ *
18
+ * En Vue el prop se llama literalmente `props`; aquí se llama `config`, que es
19
+ * lo que significa, y se acepta `props` como alias para no romper a quien ya
20
+ * lo pasaba así.
21
+ */
22
+ export default function PolymorphicInputComponent({
23
+ config,
24
+ props: legacyConfig,
25
+ value,
26
+ onChange,
27
+ onSave,
28
+ onEnter,
29
+ onInput,
30
+ onFocus,
31
+ onBlur,
32
+ }) {
33
+ const settings = config ?? legacyConfig ?? {}
34
+ const uid = useId()
35
+ const [current, set] = useControlled(value, onChange, '')
36
+ const [dirty, setDirty] = useState(false)
37
+
38
+ const update = (next) => {
39
+ setDirty(true)
40
+ set(next)
41
+ }
42
+
43
+ const saveButton = onSave && dirty ? (
44
+ <button
45
+ type="button"
46
+ className="fe-button fe-button-sm"
47
+ onClick={() => {
48
+ setDirty(false)
49
+ onSave(current)
50
+ }}>
51
+ &check;
52
+ </button>
53
+ ) : null
54
+
55
+ const control = () => {
56
+ if (TEXT_TYPES.includes(settings.type)) {
57
+ return (
58
+ <TextInputComponent
59
+ label={settings.label}
60
+ icon={settings.icon}
61
+ customClass={settings.customClass}
62
+ type={settings.type}
63
+ name={settings.name}
64
+ placeholder={settings.placeholder}
65
+ validators={settings.validators}
66
+ minLength={settings.minLength}
67
+ maxLength={settings.maxLength}
68
+ readOnly={settings.readonly}
69
+ value={current}
70
+ onChange={update}
71
+ onEnter={onEnter}
72
+ onInput={onInput}
73
+ onFocus={onFocus}
74
+ onBlur={onBlur} />
75
+ )
76
+ }
77
+
78
+ switch (settings.type) {
79
+ case 'textarea':
80
+ return (
81
+ <TextareaInputComponent
82
+ label={settings.label}
83
+ customClass={settings.customClass}
84
+ name={settings.name}
85
+ placeholder={settings.placeholder}
86
+ validators={settings.validators}
87
+ minLength={settings.minLength}
88
+ maxLength={settings.maxLength}
89
+ value={current}
90
+ onChange={update} />
91
+ )
92
+
93
+ case 'radio':
94
+ return (
95
+ <div>
96
+ {settings.label ? <div className="fe-mb"><label>{settings.label}</label></div> : null}
97
+ {(settings.options ?? []).map((option) => (
98
+ <RadioInputComponent
99
+ key={option}
100
+ customClass={settings.customClass}
101
+ name={settings.name}
102
+ validators={settings.validators}
103
+ text={option}
104
+ val={option}
105
+ value={current}
106
+ onChange={update} />
107
+ ))}
108
+ </div>
109
+ )
110
+
111
+ case 'checkbox':
112
+ return (
113
+ <div>
114
+ {settings.label ? <div className="fe-mb"><label>{settings.label}</label></div> : null}
115
+ {(settings.options ?? []).map((option) => (
116
+ <CheckboxInputComponent
117
+ key={option}
118
+ customClass={settings.customClass}
119
+ name={settings.name}
120
+ validators={settings.validators}
121
+ text={option}
122
+ val={option}
123
+ value={Array.isArray(current) ? current : []}
124
+ onChange={update} />
125
+ ))}
126
+ </div>
127
+ )
128
+
129
+ case 'select':
130
+ return (
131
+ <SelectInputComponent
132
+ label={settings.label}
133
+ customClass={settings.customClass}
134
+ name={settings.name}
135
+ validators={settings.validators}
136
+ value={current}
137
+ onChange={update}>
138
+ <option value="">{settings.placeholder ?? ''}</option>
139
+ {(settings.options ?? []).map((option) => (
140
+ <option key={option.value ?? option} value={option.value ?? option}>
141
+ {option.label ?? option}
142
+ </option>
143
+ ))}
144
+ </SelectInputComponent>
145
+ )
146
+
147
+ case 'switch':
148
+ return <SwitchComponent value={Boolean(current)} onChange={update} />
149
+
150
+ case 'editor':
151
+ return (
152
+ <EditorInputComponent
153
+ id={uid}
154
+ name={settings.name}
155
+ label={settings.label}
156
+ value={current}
157
+ onChange={update} />
158
+ )
159
+
160
+ case 'file':
161
+ return (
162
+ <SimpleFileInputComponent
163
+ inputName={settings.name}
164
+ label={settings.label}
165
+ onInput={(file) => update(file)} />
166
+ )
167
+
168
+ default:
169
+ return <div>{`Tipo de campo desconocido: ${settings.type}`}</div>
170
+ }
171
+ }
172
+
173
+ return (
174
+ <div>
175
+ <div className="fe-grid-sm" fe-grid="">
176
+ <div className="fe-w-expand">{control()}</div>
177
+ {saveButton ? <div className="fe-w-auto">{saveButton}</div> : null}
178
+ </div>
179
+ </div>
180
+ )
181
+ }
@@ -0,0 +1,38 @@
1
+ import useControlled from './internal/useControlled.js'
2
+ import useThemeClass from './internal/useThemeClass.js'
3
+
4
+ /**
5
+ * Gemelo de RadioInputComponent.vue. Seleccionado es `value === val`.
6
+ */
7
+ export default function RadioInputComponent({
8
+ customClass = undefined,
9
+ name,
10
+ validators = null,
11
+ text = '',
12
+ val,
13
+ value,
14
+ onChange,
15
+ children,
16
+ ...rest
17
+ }) {
18
+ const [current, set] = useControlled(value, onChange, '')
19
+ const radioClass = useThemeClass('radio', customClass)
20
+
21
+ return (
22
+ <div className="fe-mb">
23
+ <label className="ml-2 text-sm font-medium text-gray-900 dark:text-white">
24
+ <input
25
+ className={radioClass}
26
+ type="radio"
27
+ name={name}
28
+ data-validators={validators ?? undefined}
29
+ value={val}
30
+ checked={current === val}
31
+ onChange={() => set(val)}
32
+ {...rest} />
33
+ {text}
34
+ {children}
35
+ </label>
36
+ </div>
37
+ )
38
+ }
@@ -0,0 +1,51 @@
1
+ import { useId } from 'react'
2
+ import Field from './internal/Field.jsx'
3
+ import useThemeClass from './internal/useThemeClass.js'
4
+ import useControlled from './internal/useControlled.js'
5
+
6
+ /**
7
+ * Gemelo de SelectInputComponent.vue. Las opciones van como hijos, igual que
8
+ * el slot por defecto en Vue.
9
+ */
10
+ export default function SelectInputComponent({
11
+ id: providedId = undefined,
12
+ label = '',
13
+ help = null,
14
+ customClass = undefined,
15
+ name,
16
+ multiple = false,
17
+ size = null,
18
+ validators = null,
19
+ value,
20
+ onChange,
21
+ children,
22
+ ...rest
23
+ }) {
24
+ const generatedId = useId()
25
+ const uid = providedId ?? generatedId
26
+ const [current, set] = useControlled(value, onChange, multiple ? [] : '')
27
+ const selectClass = useThemeClass('select', customClass)
28
+
29
+ return (
30
+ <Field label={label} help={help} htmlFor={uid}>
31
+ <select
32
+ id={uid}
33
+ className={selectClass}
34
+ name={name}
35
+ multiple={multiple}
36
+ data-validators={validators ?? undefined}
37
+ size={size ?? undefined}
38
+ value={current ?? (multiple ? [] : '')}
39
+ onChange={(event) => {
40
+ // Un select múltiple entrega un array, como el v-model de
41
+ // Vue: quien lo consume no debería notar la diferencia.
42
+ set(multiple
43
+ ? Array.from(event.target.selectedOptions, (option) => option.value)
44
+ : event.target.value)
45
+ }}
46
+ {...rest}>
47
+ {children}
48
+ </select>
49
+ </Field>
50
+ )
51
+ }
@@ -0,0 +1,102 @@
1
+ import { useCallback, useId, useMemo } from 'react'
2
+ import Select from 'react-select'
3
+ import Field from './internal/Field.jsx'
4
+ import useControlled from './internal/useControlled.js'
5
+
6
+ /**
7
+ * Gemelo de SelectSearchInputComponent.vue.
8
+ *
9
+ * La versión Vue envuelve vue-select; aquí va react-select, que es su
10
+ * equivalente en React: la misma búsqueda, el mismo multiselección, el mismo
11
+ * `appendToBody` (aquí `menuPortalTarget`) y accesibilidad de teclado de
12
+ * serie.
13
+ *
14
+ * El contrato público es el del gemelo Vue: `options`, `label` como nombre de
15
+ * la propiedad que se muestra, `reduce` para quedarse con el valor, y
16
+ * `value` + `onChange(valor)`.
17
+ */
18
+ export default function SelectSearchInputComponent({
19
+ id: providedId = undefined,
20
+ inputLabel = '',
21
+ help = null,
22
+ customClass = null,
23
+ name,
24
+ options = [],
25
+ label = 'label',
26
+ placeholder = '',
27
+ clearable = true,
28
+ disabled = false,
29
+ multiple = false,
30
+ appendToBody = false,
31
+ loading = false,
32
+ reduce = (option) => option,
33
+ validators = null,
34
+ value,
35
+ onChange,
36
+ onSearch,
37
+ ...rest
38
+ }) {
39
+ const generatedId = useId()
40
+ const uid = providedId ?? generatedId
41
+ const [current, set] = useControlled(value, onChange, multiple ? [] : null)
42
+
43
+ const getOptionLabel = useCallback(
44
+ (option) => (typeof option === 'object' && option !== null ? String(option[label] ?? '') : String(option)),
45
+ [label]
46
+ )
47
+
48
+ // react-select trabaja con la opcion entera; el contrato guarda solo lo
49
+ // que devuelve reduce. Esta es la traduccion entre los dos, y es la razon
50
+ // de que quien consume el componente no note el cambio de libreria.
51
+ const selected = useMemo(() => {
52
+ if (multiple) {
53
+ const wanted = Array.isArray(current) ? current : []
54
+
55
+ return options.filter((option) => wanted.includes(reduce(option)))
56
+ }
57
+
58
+ return options.find((option) => reduce(option) === current) ?? null
59
+ }, [options, current, multiple, reduce])
60
+
61
+ return (
62
+ <Field label={inputLabel} help={help} htmlFor={uid} inline={false}>
63
+ <Select
64
+ inputId={uid}
65
+ className={customClass ?? undefined}
66
+ classNamePrefix="fe-select"
67
+ options={options}
68
+ getOptionLabel={getOptionLabel}
69
+ getOptionValue={(option) => String(reduce(option))}
70
+ placeholder={placeholder}
71
+ isClearable={clearable}
72
+ isDisabled={disabled}
73
+ isMulti={multiple}
74
+ isLoading={loading}
75
+ aria-label={inputLabel || name}
76
+ // Con `appendToBody` el menu se saca del flujo, que es lo que
77
+ // resuelve los recortes por overflow y los z-index.
78
+ menuPortalTarget={appendToBody && typeof document !== 'undefined' ? document.body : undefined}
79
+ styles={appendToBody ? { menuPortal: (base) => ({ ...base, zIndex: 1015 }) } : undefined}
80
+ value={selected}
81
+ onInputChange={(term, meta) => {
82
+ if (meta.action === 'input-change') {
83
+ onSearch?.(term)
84
+ }
85
+
86
+ return term
87
+ }}
88
+ onChange={(option) => {
89
+ set(multiple
90
+ ? (option ?? []).map((item) => reduce(item))
91
+ : (option ? reduce(option) : null))
92
+ }}
93
+ {...rest} />
94
+
95
+ {/* El validador del proyecto lee data-validators del DOM, y
96
+ react-select no expone un input donde ponerlo. */}
97
+ <input type="hidden" name={name} data-validators={validators ?? undefined} value={
98
+ multiple ? (Array.isArray(current) ? current.join(',') : '') : (current ?? '')
99
+ } readOnly />
100
+ </Field>
101
+ )
102
+ }
@@ -0,0 +1,40 @@
1
+ import { useId, useState } from 'react'
2
+
3
+ /**
4
+ * Gemelo de SimpleFileInputComponent.vue: un botón que abre el diálogo de
5
+ * archivos y muestra el nombre del elegido. `onInput` recibe el File o null.
6
+ */
7
+ export default function SimpleFileInputComponent({
8
+ customClass = null,
9
+ inputName = 'file',
10
+ label = 'Seleccionar archivo',
11
+ accept = undefined,
12
+ onInput,
13
+ }) {
14
+ const uid = useId()
15
+ const [file, setFile] = useState(null)
16
+
17
+ return (
18
+ <div className={['file-select', customClass].filter(Boolean).join(' ')}>
19
+ <label htmlFor={uid} className="select-button" style={{ cursor: 'pointer' }}>
20
+ {file?.name ?? label}
21
+ </label>
22
+
23
+ <input
24
+ id={uid}
25
+ type="file"
26
+ name={inputName}
27
+ accept={accept}
28
+ style={{ display: 'none' }}
29
+ onChange={(event) => {
30
+ // Cancelar el dialogo deja la lista vacia: hay que
31
+ // devolver null, no undefined, o quien lo consume revienta
32
+ // al leer file.size.
33
+ const selected = event.target.files?.[0] ?? null
34
+
35
+ setFile(selected)
36
+ onInput?.(selected)
37
+ }} />
38
+ </div>
39
+ )
40
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Gemelo de SingleCheckboxInputComponent.vue.
3
+ *
4
+ * Es el bloque que usa MultiCheckboxInputComponent para cada opción: el estado
5
+ * lo lleva el padre, aquí solo se avisa del cambio.
6
+ */
7
+ export default function SingleCheckboxInputComponent({
8
+ id,
9
+ label = '',
10
+ checked = false,
11
+ value = null,
12
+ onCheckedChange,
13
+ ...rest
14
+ }) {
15
+ const htmlId = `${id}_${label}`
16
+
17
+ return (
18
+ <div className="fe-mb">
19
+ <label htmlFor={htmlId} className="ml-2 text-sm font-medium text-gray-900 dark:text-white">
20
+ <input
21
+ id={htmlId}
22
+ className="fe-checkbox"
23
+ type="checkbox"
24
+ name={`input_${id}`}
25
+ checked={checked}
26
+ value={value ?? undefined}
27
+ onChange={(event) => onCheckedChange?.(event.target.checked)}
28
+ {...rest} />
29
+ &nbsp;{label}
30
+ </label>
31
+ </div>
32
+ )
33
+ }