ywana-core8 0.1.81 → 0.1.82

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/src/html/list.js CHANGED
@@ -25,6 +25,7 @@ export const List = (props) => {
25
25
  searchable = false,
26
26
  searchPlaceholder = "Search...",
27
27
  searchBy = ['line1', 'line2'],
28
+ searchPosition = 'top',
28
29
  sortable = false,
29
30
  sortBy,
30
31
  sortDirection = 'asc',
@@ -32,6 +33,7 @@ export const List = (props) => {
32
33
  multiSelect = false,
33
34
  onMultiSelect,
34
35
  dense = false,
36
+ outlined = false,
35
37
  disabled = false,
36
38
  animated = true,
37
39
  virtualized = false,
@@ -79,7 +81,7 @@ export const List = (props) => {
79
81
  }, [disabled, multiSelect, selected, onSelect, onMultiSelect])
80
82
 
81
83
  // Handle search
82
- const handleSearch = useCallback((searchId, value) => {
84
+ const handleSearch = useCallback((_, value) => {
83
85
  setSearchTerm(value)
84
86
  }, [])
85
87
 
@@ -123,10 +125,28 @@ export const List = (props) => {
123
125
  if (onSort) onSort(newConfig)
124
126
  }, [sortable, sortConfig, onSort])
125
127
 
128
+ // Search component
129
+ const SearchComponent = () => (
130
+ searchable && (
131
+ <div className="list__search">
132
+ <TextField
133
+ id="list-search"
134
+ placeholder={searchPlaceholder}
135
+ value={searchTerm}
136
+ onChange={handleSearch}
137
+ icon="search"
138
+ outlined={true}
139
+ size="small"
140
+ />
141
+ </div>
142
+ )
143
+ )
144
+
126
145
  // Generate CSS classes
127
146
  const cssClasses = [
128
147
  'list',
129
148
  dense && 'list--dense',
149
+ outlined && 'list--outlined',
130
150
  disabled && 'list--disabled',
131
151
  animated && 'list--animated',
132
152
  loading && 'list--loading',
@@ -158,23 +178,12 @@ export const List = (props) => {
158
178
  if (empty || sortedItems.length === 0) {
159
179
  return (
160
180
  <div className={cssClasses} style={style} {...ariaAttributes} {...restProps}>
161
- {searchable && (
162
- <div className="list__search">
163
- <TextField
164
- id="list-search"
165
- placeholder={searchPlaceholder}
166
- value={searchTerm}
167
- onChange={handleSearch}
168
- icon="search"
169
- outlined={true}
170
- size="small"
171
- />
172
- </div>
173
- )}
181
+ {searchPosition === 'top' && <SearchComponent />}
174
182
  <div className="list__empty">
175
183
  <Icon icon={emptyIcon} size="large" />
176
184
  <Text>{emptyMessage}</Text>
177
185
  </div>
186
+ {searchPosition === 'bottom' && <SearchComponent />}
178
187
  {children}
179
188
  </div>
180
189
  )
@@ -195,19 +204,7 @@ export const List = (props) => {
195
204
  />
196
205
  ) : (
197
206
  <div className={cssClasses} style={style} ref={listRef} {...ariaAttributes} {...restProps}>
198
- {searchable && (
199
- <div className="list__search">
200
- <TextField
201
- id="list-search"
202
- placeholder={searchPlaceholder}
203
- value={searchTerm}
204
- onChange={handleSearch}
205
- icon="search"
206
- outlined={true}
207
- size="small"
208
- />
209
- </div>
210
- )}
207
+ {searchPosition === 'top' && <SearchComponent />}
211
208
 
212
209
  {sortable && sortBy && (
213
210
  <div className="list__sort">
@@ -244,6 +241,7 @@ export const List = (props) => {
244
241
  />
245
242
  ))}
246
243
  </ul>
244
+ {searchPosition === 'bottom' && <SearchComponent />}
247
245
  {children}
248
246
  </div>
249
247
  )
@@ -264,6 +262,7 @@ const GroupedList = (props) => {
264
262
  onSearch,
265
263
  searchable = false,
266
264
  searchPlaceholder = "Search...",
265
+ searchPosition = 'top',
267
266
  multiSelect = false,
268
267
  dense = false,
269
268
  disabled = false,
@@ -302,21 +301,26 @@ const GroupedList = (props) => {
302
301
  })
303
302
  }, [])
304
303
 
304
+ // Search component for grouped list
305
+ const GroupedSearchComponent = () => (
306
+ searchable && (
307
+ <div className="list__search">
308
+ <TextField
309
+ id="grouped-list-search"
310
+ placeholder={searchPlaceholder}
311
+ value={searchTerm}
312
+ onChange={onSearch}
313
+ icon="search"
314
+ outlined={true}
315
+ size="small"
316
+ />
317
+ </div>
318
+ )
319
+ )
320
+
305
321
  return (
306
322
  <div className={`${cssClasses} grouped`} style={style} {...ariaAttributes} {...restProps}>
307
- {searchable && (
308
- <div className="list__search">
309
- <TextField
310
- id="grouped-list-search"
311
- placeholder={searchPlaceholder}
312
- value={searchTerm}
313
- onChange={onSearch}
314
- icon="search"
315
- outlined={true}
316
- size="small"
317
- />
318
- </div>
319
- )}
323
+ {searchPosition === 'top' && <GroupedSearchComponent />}
320
324
 
321
325
  {groups.map(group => {
322
326
  const isCollapsed = collapsedGroups.has(group.name)
@@ -373,6 +377,7 @@ const GroupedList = (props) => {
373
377
  </Fragment>
374
378
  )
375
379
  })}
380
+ {searchPosition === 'bottom' && <GroupedSearchComponent />}
376
381
  {children}
377
382
  </div>
378
383
  )
@@ -509,19 +514,19 @@ const ListItem = ({
509
514
  )}
510
515
  </main>
511
516
 
512
- {/* Meta information */}
513
- {meta && (
514
- <div className="list__item-meta">
515
- {typeof meta === 'string' ? <Text size="small">{meta}</Text> : meta}
516
- </div>
517
- )}
518
-
519
517
  {/* Actions */}
520
518
  {actions && (
521
519
  <div className="list__item-actions" role="toolbar">
522
520
  {actions}
523
521
  </div>
524
522
  )}
523
+
524
+ {/* Meta information */}
525
+ {meta && (
526
+ <div className="list__item-meta">
527
+ {typeof meta === 'string' ? <Text size="small">{meta}</Text> : meta}
528
+ </div>
529
+ )}
525
530
  </li>
526
531
  )
527
532
  }
@@ -569,6 +574,8 @@ List.propTypes = {
569
574
  searchPlaceholder: PropTypes.string,
570
575
  /** Properties to search by */
571
576
  searchBy: PropTypes.arrayOf(PropTypes.string),
577
+ /** Search position */
578
+ searchPosition: PropTypes.oneOf(['top', 'bottom']),
572
579
  /** Enable sorting */
573
580
  sortable: PropTypes.bool,
574
581
  /** Property to sort by */
@@ -583,6 +590,8 @@ List.propTypes = {
583
590
  onMultiSelect: PropTypes.func,
584
591
  /** Dense layout */
585
592
  dense: PropTypes.bool,
593
+ /** Outlined variant with borders */
594
+ outlined: PropTypes.bool,
586
595
  /** Disabled state */
587
596
  disabled: PropTypes.bool,
588
597
  /** Enable animations */
@@ -1415,22 +1415,22 @@ body.datatable2-resizing {
1415
1415
  }
1416
1416
 
1417
1417
  .datatable2__filter-cell {
1418
- padding: 8px 12px;
1418
+ padding: .5rem;
1419
1419
  }
1420
1420
 
1421
1421
  .datatable2__filter-field {
1422
1422
  width: 100% !important;
1423
1423
  height: 32px !important;
1424
- border: 1px solid var(--divider-color, #e0e0e0) !important;
1425
1424
  border-radius: 4px !important;
1426
- padding: 4px 8px !important;
1427
1425
  font-size: 0.75rem !important;
1428
1426
  background: white !important;
1427
+ padding-top: 0px !important;
1428
+ }
1429
+
1430
+ .datatable2__filter-field>input {
1429
1431
  }
1430
1432
 
1431
1433
  .datatable2__filter-field:focus {
1432
- border-color: var(--primary-color, #2196f3) !important;
1433
- box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2) !important;
1434
1434
  }
1435
1435
 
1436
1436
  .datatable2__filter-actions-cell {
@@ -97,11 +97,19 @@
97
97
  .textfield2-clear,
98
98
  .textfield2-password-toggle {
99
99
  position: absolute;
100
- top: 1.5rem;
101
- right: 0.2rem;
100
+ top: 50%;
101
+ transform: translateY(-50%);
102
+ right: 0.75rem; /* Más espacio desde el borde */
102
103
  color: var(--text-color-light, #666);
103
104
  cursor: pointer;
104
105
  transition: color 0.2s ease;
106
+ z-index: 2;
107
+ }
108
+
109
+ /* Icons positioning for outlined variant */
110
+ .textfield2-outlined .textfield2-clear,
111
+ .textfield2-outlined .textfield2-password-toggle {
112
+ right: 0.75rem; /* Alineado con el padding del input */
105
113
  }
106
114
 
107
115
  .textfield2-clear:hover,
@@ -109,13 +117,28 @@
109
117
  color: var(--text-color, #333);
110
118
  }
111
119
 
120
+ /* For fields without labels, the centering still works with transform */
112
121
  .textfield2.no-label .textfield2-clear,
113
122
  .textfield2.no-label .textfield2-password-toggle {
114
- top: 0.5rem;
123
+ /* Remove top override - let transform handle centering */
115
124
  }
116
125
 
117
126
  .textfield2-password-toggle {
118
- right: 2.2rem;
127
+ right: 2.5rem; /* Más espacio para el clear button */
128
+ }
129
+
130
+ /* When both clear and password toggle are present */
131
+ .textfield2.textfield2-password .textfield2-clear {
132
+ right: 2.5rem;
133
+ }
134
+
135
+ /* Outlined variant - icon spacing */
136
+ .textfield2-outlined .textfield2-password-toggle {
137
+ right: 2.5rem;
138
+ }
139
+
140
+ .textfield2-outlined.textfield2-password .textfield2-clear {
141
+ right: 2.5rem;
119
142
  }
120
143
 
121
144
  .textfield2.textfield2-date .textfield2-clear,
@@ -161,6 +184,32 @@
161
184
  transform: scale(0.9);
162
185
  }
163
186
 
187
+ /* Outlined variant - label positioning when active */
188
+ .textfield2-outlined > input:focus ~ label,
189
+ .textfield2-outlined > input:valid ~ label,
190
+ .textfield2-outlined > input[value]:not([value=""]) ~ label,
191
+ .textfield2-outlined > textarea:focus ~ label,
192
+ .textfield2-outlined > textarea:valid ~ label,
193
+ .textfield2-outlined > textarea[value]:not([value=""]) ~ label,
194
+ .textfield2-outlined.focused > label,
195
+ .textfield2-outlined.has-placeholder > label {
196
+ top: -0.5rem; /* Flotando sobre el borde */
197
+ left: 0.75rem;
198
+ font-size: 0.875rem; /* Tamaño más legible */
199
+ color: var(--primary-color, #2196f3);
200
+ transform: none; /* Sin escala adicional */
201
+ }
202
+
203
+ /* Error state for outlined labels */
204
+ .textfield2-outlined.error > input:focus ~ label,
205
+ .textfield2-outlined.error > textarea:focus ~ label,
206
+ .textfield2-outlined.invalid > input:focus ~ label,
207
+ .textfield2-outlined.invalid > textarea:focus ~ label,
208
+ .textfield2-outlined.error.focused > label,
209
+ .textfield2-outlined.invalid.focused > label {
210
+ color: var(--error-color, #f44336);
211
+ }
212
+
164
213
  .textfield2.error > input:focus ~ label,
165
214
  .textfield2.error > textarea:focus ~ label,
166
215
  .textfield2.invalid > input:focus ~ label,
@@ -228,12 +277,18 @@
228
277
  display: flex;
229
278
  align-items: center;
230
279
  gap: 0.25rem;
231
- margin-top: 0.25rem;
280
+ margin-top: 0.5rem; /* Más espacio desde el input */
232
281
  font-size: 0.75rem;
233
282
  line-height: 1.2;
234
283
  min-height: 1rem;
235
284
  }
236
285
 
286
+ /* Helper text for outlined variant */
287
+ .textfield2-outlined .textfield2-helper {
288
+ margin-top: 0.5rem;
289
+ margin-left: 0.75rem; /* Alineado con el input */
290
+ }
291
+
237
292
  .textfield2-helper.helper {
238
293
  color: var(--text-color-light, #666);
239
294
  }
@@ -246,39 +301,71 @@
246
301
  flex-shrink: 0;
247
302
  }
248
303
 
249
- /* Outlined variant */
304
+ /* Outlined variant - Container structure */
250
305
  .textfield2-outlined {
251
- border: 1px solid var(--divider-color, #e0e0e0);
252
- border-radius: 4px;
253
- padding: 0.5rem;
254
- background-color: var(--paper-color, #fff);
306
+ position: relative;
307
+ margin-bottom: 1.5rem; /* Espacio para helper text */
255
308
  }
256
309
 
310
+ /* Outlined input container - solo el input tiene borde */
257
311
  .textfield2-outlined > input,
258
312
  .textfield2-outlined > textarea {
259
- border: none;
260
- padding: 0.5rem;
313
+ width: 100%;
314
+ border: 1px solid var(--divider-color, #e0e0e0);
315
+ border-radius: 4px;
316
+ padding: 1rem 3rem 1rem 0.75rem; /* Espacio para iconos a la derecha */
317
+ background-color: var(--paper-color, #fff);
318
+ font-size: 1rem;
319
+ line-height: 1.5;
320
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
321
+ outline: none;
261
322
  }
262
323
 
263
- .textfield2-outlined > label {
264
- background-color: var(--paper-color, #fff);
265
- padding: 0 0.25rem;
266
- left: 0.75rem;
267
- top: -0.5rem;
324
+ /* Adjust padding when no icons are present */
325
+ .textfield2-outlined:not(.has-icons) > input,
326
+ .textfield2-outlined:not(.has-icons) > textarea {
327
+ padding-right: 0.75rem;
268
328
  }
269
329
 
270
- .textfield2-outlined.focused,
330
+ /* Focus states for outlined inputs */
271
331
  .textfield2-outlined > input:focus,
272
- .textfield2-outlined > textarea:focus {
332
+ .textfield2-outlined > textarea:focus,
333
+ .textfield2-outlined.focused > input,
334
+ .textfield2-outlined.focused > textarea {
273
335
  border-color: var(--primary-color, #2196f3);
274
- outline: none;
336
+ box-shadow: 0 0 0 1px var(--primary-color, #2196f3);
275
337
  }
276
338
 
277
- .textfield2-outlined.error,
278
- .textfield2-outlined.invalid {
339
+ /* Error states for outlined inputs */
340
+ .textfield2-outlined.error > input,
341
+ .textfield2-outlined.error > textarea,
342
+ .textfield2-outlined.invalid > input,
343
+ .textfield2-outlined.invalid > textarea {
279
344
  border-color: var(--error-color, #f44336);
280
345
  }
281
346
 
347
+ .textfield2-outlined.error > input:focus,
348
+ .textfield2-outlined.error > textarea:focus,
349
+ .textfield2-outlined.invalid > input:focus,
350
+ .textfield2-outlined.invalid > textarea:focus {
351
+ box-shadow: 0 0 0 1px var(--error-color, #f44336);
352
+ }
353
+
354
+ /* Outlined label positioning */
355
+ .textfield2-outlined > label {
356
+ position: absolute;
357
+ left: 0.75rem;
358
+ top: 1rem; /* Centrado en el input */
359
+ background-color: var(--paper-color, #fff);
360
+ padding: 0 0.25rem;
361
+ font-size: 1rem;
362
+ color: var(--text-color-light, #666);
363
+ transition: all 0.2s ease;
364
+ pointer-events: none;
365
+ z-index: 1;
366
+ }
367
+
368
+ /* Hide the focus bar for outlined variant */
282
369
  .textfield2-outlined .textfield2-bar {
283
370
  display: none;
284
371
  }
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from 'react'
2
- import { TextField2, TextArea2, PasswordField2, DropDown2, DateRange2 } from '
3
- import { ExampleLayout, ExampleSection, ExampleSubsection, CodeSnippet } from './ExampleLayout'./textfield2'
2
+ import { TextField2, TextArea2, PasswordField2, DropDown2, DateRange2 } from './textfield2'
3
+ import { ExampleLayout, ExampleSection, ExampleSubsection, CodeSnippet } from './ExampleLayout'
4
4
 
5
5
  /**
6
6
  * Ejemplos de uso de los componentes TextField2 mejorados
@@ -51,6 +51,7 @@ export const TextField2 = (props) => {
51
51
  const [isFocused, setIsFocused] = useState(false)
52
52
  const [internalError, setInternalError] = useState('')
53
53
  const [isValid, setIsValid] = useState(true)
54
+ const [hasBeenTouched, setHasBeenTouched] = useState(false)
54
55
  const inputRef = useRef(null)
55
56
  const debounceRef = useRef(null)
56
57
 
@@ -59,8 +60,15 @@ export const TextField2 = (props) => {
59
60
  console.warn('TextField2 component: id prop is required')
60
61
  }
61
62
 
62
- // Validate value and set error states
63
+ // Validate value and set error states - only after user interaction
63
64
  useEffect(() => {
65
+ // Don't validate required fields until user has interacted with the field
66
+ if (!hasBeenTouched && required && !value) {
67
+ setIsValid(true)
68
+ setInternalError('')
69
+ return
70
+ }
71
+
64
72
  if (validation && value !== undefined) {
65
73
  const validationResult = validation(value)
66
74
  const valid = typeof validationResult === 'boolean' ? validationResult : validationResult.valid
@@ -72,27 +80,32 @@ export const TextField2 = (props) => {
72
80
  if (onValidation) {
73
81
  onValidation(id, valid, errorMessage)
74
82
  }
75
- } else if (required && !value) {
83
+ } else if (required && !value && hasBeenTouched) {
76
84
  setIsValid(false)
77
85
  setInternalError('This field is required')
78
86
  } else {
79
87
  setIsValid(true)
80
88
  setInternalError('')
81
89
  }
82
- }, [value, required, id]) // Removed validation and onValidation from dependencies to prevent infinite loops
90
+ }, [value, required, id, hasBeenTouched]) // Added hasBeenTouched to dependencies
83
91
 
84
92
  // Handle input changes with debouncing
85
93
  const handleChange = useCallback((event) => {
86
94
  if (disabled || readOnly) return
87
-
95
+
88
96
  event.stopPropagation()
89
97
  const newValue = event.target.value
90
-
98
+
99
+ // Mark field as touched on first change
100
+ if (!hasBeenTouched) {
101
+ setHasBeenTouched(true)
102
+ }
103
+
91
104
  // Clear previous debounce
92
105
  if (debounceRef.current) {
93
106
  clearTimeout(debounceRef.current)
94
107
  }
95
-
108
+
96
109
  if (debounceMs > 0) {
97
110
  debounceRef.current = setTimeout(() => {
98
111
  if (onChange) onChange(id, newValue, event)
@@ -100,7 +113,7 @@ export const TextField2 = (props) => {
100
113
  } else {
101
114
  if (onChange) onChange(id, newValue, event)
102
115
  }
103
- }, [disabled, readOnly, id, onChange, debounceMs])
116
+ }, [disabled, readOnly, id, onChange, debounceMs, hasBeenTouched])
104
117
 
105
118
  // Handle key press events
106
119
  const handleKeyPress = useCallback((event) => {
@@ -131,10 +144,16 @@ export const TextField2 = (props) => {
131
144
  // Handle blur events
132
145
  const handleBlur = useCallback((event) => {
133
146
  if (disabled) return
134
-
147
+
135
148
  setIsFocused(false)
149
+
150
+ // Mark field as touched on blur if it hasn't been touched yet
151
+ if (!hasBeenTouched) {
152
+ setHasBeenTouched(true)
153
+ }
154
+
136
155
  if (onBlur) onBlur(event)
137
- }, [disabled, onBlur])
156
+ }, [disabled, onBlur, hasBeenTouched])
138
157
 
139
158
  // Handle clear action
140
159
  const handleClear = useCallback(() => {
@@ -2,7 +2,7 @@
2
2
  background-color: var(--paper-color);
3
3
  }
4
4
 
5
- .login-box > main {
5
+ .login-box>main {
6
6
  padding: 1rem 1rem 0 1rem;
7
7
  display: flex;
8
8
  flex-direction: column;
@@ -34,24 +34,26 @@
34
34
  }
35
35
 
36
36
  @keyframes rotation-counterclock {
37
- from {
38
- transform: rotate(359deg);
39
- }
40
- to {
41
- transform: rotate(0deg);
42
- }
37
+ from {
38
+ transform: rotate(359deg);
39
+ }
40
+
41
+ to {
42
+ transform: rotate(0deg);
43
43
  }
44
+ }
44
45
 
45
46
  @keyframes rotation {
46
47
  from {
47
48
  transform: rotate(0deg);
48
49
  }
50
+
49
51
  to {
50
52
  transform: rotate(359deg);
51
53
  }
52
54
  }
53
55
 
54
- .login-box > footer {
56
+ .login-box>footer {
55
57
  grid-area: footer;
56
58
  padding: 1rem;
57
59
  display: flex;
@@ -63,6 +65,7 @@
63
65
  from {
64
66
  opacity: 0;
65
67
  }
68
+
66
69
  to {
67
70
  opacity: 1;
68
71
  }
@@ -74,7 +77,7 @@
74
77
  }
75
78
 
76
79
  .login-box .forgot-button {
77
- text-align: right;
78
- font-weight: 500;
79
- max-width: 15rem;
80
+ text-align: right;
81
+ font-weight: 500;
82
+ max-width: 15rem;
80
83
  }
@@ -5,7 +5,7 @@ import './LoginBox.css'
5
5
  /**
6
6
  * Login Box
7
7
  */
8
- export const LoginBox = ({
8
+ export const LoginBox = ({
9
9
  userLabel = "User", userValue = "",
10
10
  passwordLabel = "Password", passwordValue = '',
11
11
  loginLabel = "Log In",