innoboxrr-react-form-elements 3.4.0 → 3.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "innoboxrr-react-form-elements",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Gemelo React de innoboxrr-form-elements: los mismos componentes, los mismos nombres y el mismo contrato, con diálogos, drawers, menús, avisos y paleta de comandos.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -53,7 +53,7 @@
53
53
  "@tinymce/tinymce-react": "^6.3.0",
54
54
  "@uiw/react-codemirror": "^4.25.11",
55
55
  "@yaireo/tagify": "^4.38.0",
56
- "innoboxrr-form-core": "^2.5.0",
56
+ "innoboxrr-form-core": "^2.7.0",
57
57
  "innoboxrr-maskjs": "^2.0.0",
58
58
  "react-colorful": "^5.8.1",
59
59
  "react-phone-number-input": "^3.4.18",
@@ -21,13 +21,14 @@ export default function CheckboxInputComponent({
21
21
  }) {
22
22
  const [current, set] = useControlled(value, onChange, '')
23
23
  const boxClass = useThemeClass('checkbox', customClass)
24
+ const labelClass = useThemeClass('label')
24
25
 
25
26
  const isGroup = Array.isArray(current)
26
27
  const checked = isGroup ? current.includes(val) : Boolean(current)
27
28
 
28
29
  return (
29
30
  <div className="fe-mb">
30
- <label className="ml-2 text-sm font-medium text-gray-900 dark:text-white">
31
+ <label className={labelClass}>
31
32
  <input
32
33
  className={boxClass}
33
34
  type="checkbox"
@@ -1,5 +1,6 @@
1
1
  import { useRef, useState } from 'react'
2
2
  import useControlled from './internal/useControlled.js'
3
+ import useTheme, { joinClasses } from './internal/useTheme.js'
3
4
 
4
5
  /**
5
6
  * Gemelo de CodeInputComponent.vue: las casillas de un código de un solo uso.
@@ -20,6 +21,7 @@ export default function CodeInputComponent({
20
21
  onChange,
21
22
  onComplete,
22
23
  }) {
24
+ const theme = useTheme()
23
25
  const [code, setCode] = useControlled(value, onChange, '')
24
26
  const inputs = useRef([])
25
27
  const [chars, setChars] = useState(() => Array.from({ length: fields }, (_, i) => (code ?? '')[i] ?? ''))
@@ -45,12 +47,12 @@ export default function CodeInputComponent({
45
47
  <div className={['code-input-container', className].filter(Boolean).join(' ')}>
46
48
  {title ? <p className="title">{title}</p> : null}
47
49
 
48
- <div className="code-input fe-code-input">
50
+ <div className={joinClasses('code-input', theme.codeInput)}>
49
51
  {chars.map((char, index) => (
50
52
  <input
51
53
  key={index}
52
54
  ref={(element) => { inputs.current[index] = element }}
53
- className="w-14 h-14 rounded-lg border border-gray outline-none focus:outline-none focus:border-primary focus:ring-0 text-center transition-all"
55
+ className={theme.codeCell}
54
56
  type="text"
55
57
  inputMode="numeric"
56
58
  pattern="[0-9]*"
@@ -80,7 +80,7 @@ export default function CountrySelectInputComponent({
80
80
  }}
81
81
  {...rest} />
82
82
 
83
- {help ? <p className="text-sm text-gray-500">{help}</p> : null}
83
+ {help ? <p className="fe-text-muted">{help}</p> : null}
84
84
  </div>
85
85
  </div>
86
86
  )
@@ -21,6 +21,7 @@ import SelectInputComponent from './SelectInputComponent.jsx'
21
21
  import TextInputComponent from './TextInputComponent.jsx'
22
22
  import TextareaInputComponent from './TextareaInputComponent.jsx'
23
23
  import useControlled from './internal/useControlled.js'
24
+ import useTheme, { joinClasses } from './internal/useTheme.js'
24
25
 
25
26
  const COMPONENTS = {
26
27
  text: TextInputComponent,
@@ -38,15 +39,20 @@ const COMPONENTS = {
38
39
  * por su cuenta y trae ordenación **por teclado**, que ni SortableJS ni la API
39
40
  * nativa de arrastre dan. Aquí no es un detalle: un formulario que solo se
40
41
  * puede reordenar con el ratón no es accesible.
42
+ *
43
+ * El aspecto sale del tema, con el mismo marcado que la rama Vue: cada grupo es
44
+ * una superficie con su barra. Antes eran clases de Tailwind y colores escritos
45
+ * para el modo oscuro que el paquete no declara.
41
46
  */
42
47
  function SortableGroup({ id, children }) {
48
+ const theme = useTheme()
43
49
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id })
44
50
 
45
51
  return (
46
52
  <div
47
53
  ref={setNodeRef}
48
54
  style={{ transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1 }}
49
- className="border rounded-lg bg-white dark:bg-slate-800 shadow-sm relative dark:border-slate-600">
55
+ className={theme.surface}>
50
56
  {children({ attributes, listeners })}
51
57
  </div>
52
58
  )
@@ -61,6 +67,7 @@ export default function DynamicGroupInputComponent({
61
67
  removeButtonLabel = 'Eliminar',
62
68
  itemLabel = 'Item',
63
69
  }) {
70
+ const theme = useTheme()
64
71
  const [current, set] = useControlled(value, onChange, [])
65
72
  const nextKey = useRef(0)
66
73
 
@@ -113,67 +120,66 @@ export default function DynamicGroupInputComponent({
113
120
  return (
114
121
  <div>
115
122
  {label ? (
116
- <label className="block mb-4 ml-2 text-sm font-medium text-slate-900 dark:text-slate-100">
123
+ <label className={theme.label}>
117
124
  {label}
118
125
  </label>
119
126
  ) : null}
120
127
 
121
128
  <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
122
129
  <SortableContext items={keyed.map((group) => group.__key)} strategy={verticalListSortingStrategy}>
123
- <div className="space-y-2 rounded-lg">
130
+ <div className={theme.group}>
124
131
  {keyed.map((group, index) => (
125
132
  <SortableGroup key={group.__key} id={group.__key}>
126
133
  {({ attributes, listeners }) => (
127
134
  <>
128
- <div className="flex justify-between items-center px-4 py-3 border-b bg-slate-50 dark:bg-slate-700 dark:border-slate-600 rounded-t-lg">
129
- <div className="flex items-center gap-2">
130
- <button
131
- type="button"
132
- aria-label={`Mover ${itemLabel} ${index + 1}`}
133
- className="cursor-move drag-handle text-slate-400"
134
- {...attributes}
135
- {...listeners}>
136
- <IconComponent name="drag" />
137
- </button>
138
- <h4 className="text-md font-semibold text-slate-800 dark:text-slate-100">
139
- {itemLabel} #{index + 1}
140
- </h4>
141
- </div>
142
-
143
- <div className="flex items-center space-x-4 text-slate-400">
144
- <button
145
- type="button"
146
- aria-label={`Duplicar ${itemLabel} ${index + 1}`}
147
- className="hover:text-blue-500 transition mr-2"
148
- onClick={() => set([
149
- ...keyed.slice(0, index + 1),
150
- { ...group, __key: `grupo-${nextKey.current++}` },
151
- ...keyed.slice(index + 1),
152
- ])}>
153
- <IconComponent name="copy" />
154
- </button>
155
-
156
- <button
157
- type="button"
158
- aria-label={`${removeButtonLabel} ${index + 1}`}
159
- className="text-red-800 dark:text-red-400 text-sm"
160
- onClick={() => set(keyed.filter((_, position) => position !== index))}>
161
- <IconComponent name="delete" />
162
- </button>
163
-
164
- <button
165
- type="button"
166
- aria-label={`Expandir ${itemLabel} ${index + 1}`}
167
- aria-expanded={! group._collapsed}
168
- className="hover:text-slate-600 dark:hover:text-slate-300 transition"
169
- onClick={() => updateAt(index, '_collapsed', ! group._collapsed)}>
170
- <IconComponent name={group._collapsed ? 'up' : 'down'} />
171
- </button>
172
- </div>
135
+ <div className={theme.toolbar}>
136
+ <button
137
+ type="button"
138
+ aria-label={`Mover ${itemLabel} ${index + 1}`}
139
+ className={joinClasses(theme.iconButton, theme.dragHandle, 'drag-handle')}
140
+ {...attributes}
141
+ {...listeners}>
142
+ <IconComponent name="drag" />
143
+ </button>
144
+
145
+ <h4 className={theme.groupTitle}>
146
+ {itemLabel} #{index + 1}
147
+ </h4>
148
+
149
+ <span className={theme.toolbarSpacer} />
150
+
151
+ <button
152
+ type="button"
153
+ aria-label={`Duplicar ${itemLabel} ${index + 1}`}
154
+ className={theme.iconButton}
155
+ onClick={() => set([
156
+ ...keyed.slice(0, index + 1),
157
+ { ...group, __key: `grupo-${nextKey.current++}` },
158
+ ...keyed.slice(index + 1),
159
+ ])}>
160
+ <IconComponent name="copy" />
161
+ </button>
162
+
163
+ <button
164
+ type="button"
165
+ aria-label={`${removeButtonLabel} ${index + 1}`}
166
+ className={joinClasses(theme.iconButton, theme.iconButtonDanger)}
167
+ onClick={() => set(keyed.filter((_, position) => position !== index))}>
168
+ <IconComponent name="delete" />
169
+ </button>
170
+
171
+ <button
172
+ type="button"
173
+ aria-label={`Expandir ${itemLabel} ${index + 1}`}
174
+ aria-expanded={! group._collapsed}
175
+ className={theme.iconButton}
176
+ onClick={() => updateAt(index, '_collapsed', ! group._collapsed)}>
177
+ <IconComponent name={group._collapsed ? 'up' : 'down'} />
178
+ </button>
173
179
  </div>
174
180
 
175
181
  {! group._collapsed ? (
176
- <div className="p-4">
182
+ <div className="fe-card-body">
177
183
  {inputsConfig.map((field) => {
178
184
  const Component = COMPONENTS[field.type] ?? TextInputComponent
179
185
 
@@ -208,7 +214,7 @@ export default function DynamicGroupInputComponent({
208
214
 
209
215
  <button
210
216
  type="button"
211
- className="fe-button fe-mt"
217
+ className={joinClasses(theme.button, 'fe-mt')}
212
218
  onClick={() => set([...keyed, emptyGroup()])}>
213
219
  {addButtonLabel}
214
220
  </button>
@@ -1,8 +1,14 @@
1
1
  import { useRef, useState } from 'react'
2
+ import IconComponent from './IconComponent.jsx'
3
+ import useTheme from './internal/useTheme.js'
2
4
 
3
5
  /**
4
6
  * Gemelo de FileDropInputComponent.vue. `onFilesChange` recibe un array de
5
7
  * File, venga de arrastrar o del diálogo.
8
+ *
9
+ * `fe-file-drop` no lo definía ninguna hoja, así que la zona salía sin borde ni
10
+ * resalte. Ahora sale del tema, con el mismo icono y el mismo texto secundario
11
+ * que la rama Vue.
6
12
  */
7
13
  export default function FileDropInputComponent({
8
14
  multiple = false,
@@ -11,18 +17,20 @@ export default function FileDropInputComponent({
11
17
  subText = 'o haz clic para seleccionar los archivos.',
12
18
  onFilesChange,
13
19
  }) {
20
+ const theme = useTheme()
14
21
  const input = useRef(null)
15
22
  const [dragging, setDragging] = useState(false)
16
23
 
17
24
  return (
18
25
  <div
19
- className="fe-file-drop"
26
+ className={theme.fileDrop}
20
27
  data-dragging={dragging}
21
28
  role="button"
22
29
  tabIndex={0}
23
30
  onClick={() => input.current?.click()}
24
31
  onKeyDown={(event) => {
25
32
  if (event.key === 'Enter' || event.key === ' ') {
33
+ event.preventDefault()
26
34
  input.current?.click()
27
35
  }
28
36
  }}
@@ -36,8 +44,10 @@ export default function FileDropInputComponent({
36
44
  setDragging(false)
37
45
  onFilesChange?.(Array.from(event.dataTransfer.files))
38
46
  }}>
47
+ <IconComponent name="media" size={32} />
48
+
39
49
  <p>{mainText}</p>
40
- <p>{subText}</p>
50
+ <p className={theme.fileDropHint}>{subText}</p>
41
51
 
42
52
  <input
43
53
  ref={input}
@@ -167,14 +167,14 @@ export default function FileInputComponent({
167
167
  </ul>
168
168
 
169
169
  {errors.length ? (
170
- <ul className="fe-list text-red-600">
170
+ <ul className="fe-list fe-error">
171
171
  {errors.map((entry, index) => (
172
172
  <li key={`${entry.name}-error-${index}`}>{entry.name}: {entry.errors?.join(', ')}</li>
173
173
  ))}
174
174
  </ul>
175
175
  ) : null}
176
176
 
177
- {overTotal ? <p className="text-red-600">El tamaño total supera el máximo permitido.</p> : null}
177
+ {overTotal ? <p className="fe-error">El tamaño total supera el máximo permitido.</p> : null}
178
178
 
179
179
  {! autoUpload && files.length ? (
180
180
  <button type="button" className="fe-button" disabled={uploading || overTotal} onClick={upload}>
@@ -34,7 +34,7 @@ export default function FqsInputComponent({
34
34
 
35
35
  return (
36
36
  <div>
37
- <h4>{text.title}</h4>
37
+ <h4 className="fe-group-title fe-mb-sm">{text.title}</h4>
38
38
 
39
39
  {items.map((item, index) => (
40
40
  <div key={index} className="fe-card fe-card-sm fe-card-body fe-mb">
@@ -17,10 +17,11 @@ export default function RadioInputComponent({
17
17
  }) {
18
18
  const [current, set] = useControlled(value, onChange, '')
19
19
  const radioClass = useThemeClass('radio', customClass)
20
+ const labelClass = useThemeClass('label')
20
21
 
21
22
  return (
22
23
  <div className="fe-mb">
23
- <label className="ml-2 text-sm font-medium text-gray-900 dark:text-white">
24
+ <label className={labelClass}>
24
25
  <input
25
26
  className={radioClass}
26
27
  type="radio"
@@ -1,3 +1,5 @@
1
+ import useThemeClass from './internal/useThemeClass.js'
2
+
1
3
  /**
2
4
  * Gemelo de SingleCheckboxInputComponent.vue.
3
5
  *
@@ -13,10 +15,11 @@ export default function SingleCheckboxInputComponent({
13
15
  ...rest
14
16
  }) {
15
17
  const htmlId = `${id}_${label}`
18
+ const labelClass = useThemeClass('label')
16
19
 
17
20
  return (
18
21
  <div className="fe-mb">
19
- <label htmlFor={htmlId} className="ml-2 text-sm font-medium text-gray-900 dark:text-white">
22
+ <label htmlFor={htmlId} className={labelClass}>
20
23
  <input
21
24
  id={htmlId}
22
25
  className="fe-checkbox"