nexa-ui-kit 0.6.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 (114) hide show
  1. package/dist/NBadge.nexa +40 -0
  2. package/dist/NBottomSheet.nexa +124 -0
  3. package/dist/NButton.nexa +123 -0
  4. package/dist/NCard.nexa +74 -0
  5. package/dist/NInput.nexa +116 -0
  6. package/dist/NModal.nexa +165 -0
  7. package/dist/NSelect.nexa +169 -0
  8. package/dist/NToastContainer.nexa +86 -0
  9. package/dist/NTooltip.nexa +115 -0
  10. package/dist/components/NAlert.js +134 -0
  11. package/dist/components/NAlert.nexa +115 -0
  12. package/dist/components/NAutocomplete.js +94 -0
  13. package/dist/components/NAutocomplete.nexa +58 -0
  14. package/dist/components/NAvatar.js +75 -0
  15. package/dist/components/NAvatar.nexa +67 -0
  16. package/dist/components/NBadge.js +74 -0
  17. package/dist/components/NBadge.nexa +61 -0
  18. package/dist/components/NBottomSheet.js +149 -0
  19. package/dist/components/NBottomSheet.nexa +145 -0
  20. package/dist/components/NButton.js +284 -0
  21. package/dist/components/NButton.nexa +275 -0
  22. package/dist/components/NCard.js +117 -0
  23. package/dist/components/NCard.nexa +100 -0
  24. package/dist/components/NCheckbox.js +108 -0
  25. package/dist/components/NCheckbox.nexa +90 -0
  26. package/dist/components/NChips.js +72 -0
  27. package/dist/components/NChips.nexa +57 -0
  28. package/dist/components/NDataTable.js +252 -0
  29. package/dist/components/NDataTable.nexa +186 -0
  30. package/dist/components/NDatepicker.js +379 -0
  31. package/dist/components/NDatepicker.nexa +367 -0
  32. package/dist/components/NForm.js +132 -0
  33. package/dist/components/NForm.nexa +133 -0
  34. package/dist/components/NFormField.js +173 -0
  35. package/dist/components/NFormField.nexa +171 -0
  36. package/dist/components/NInput.js +311 -0
  37. package/dist/components/NInput.nexa +311 -0
  38. package/dist/components/NInputNumber.js +202 -0
  39. package/dist/components/NInputNumber.nexa +199 -0
  40. package/dist/components/NModal.js +221 -0
  41. package/dist/components/NModal.nexa +221 -0
  42. package/dist/components/NMultiSelect.js +156 -0
  43. package/dist/components/NMultiSelect.nexa +77 -0
  44. package/dist/components/NPaginator.js +117 -0
  45. package/dist/components/NPaginator.nexa +77 -0
  46. package/dist/components/NPassword.js +193 -0
  47. package/dist/components/NPassword.nexa +178 -0
  48. package/dist/components/NProgressBar.js +127 -0
  49. package/dist/components/NProgressBar.nexa +111 -0
  50. package/dist/components/NRadio.js +96 -0
  51. package/dist/components/NRadio.nexa +81 -0
  52. package/dist/components/NSelect.js +468 -0
  53. package/dist/components/NSelect.nexa +452 -0
  54. package/dist/components/NSkeleton.js +98 -0
  55. package/dist/components/NSkeleton.nexa +74 -0
  56. package/dist/components/NSwitch.js +92 -0
  57. package/dist/components/NSwitch.nexa +76 -0
  58. package/dist/components/NTabs.js +129 -0
  59. package/dist/components/NTabs.nexa +113 -0
  60. package/dist/components/NTag.js +108 -0
  61. package/dist/components/NTag.nexa +93 -0
  62. package/dist/components/NToastContainer.js +242 -0
  63. package/dist/components/NToastContainer.nexa +221 -0
  64. package/dist/components/NTooltip.js +163 -0
  65. package/dist/components/NTooltip.nexa +166 -0
  66. package/dist/components/NTreeMenu.js +151 -0
  67. package/dist/components/NTreeMenu.nexa +142 -0
  68. package/dist/index.d.ts +32 -0
  69. package/dist/index.js +34 -0
  70. package/dist/services/FloatingOverlay.d.ts +27 -0
  71. package/dist/services/FloatingOverlay.js +98 -0
  72. package/dist/services/FormValidation.d.ts +8 -0
  73. package/dist/services/FormValidation.js +46 -0
  74. package/dist/services/ToastService.d.ts +16 -0
  75. package/dist/services/ToastService.js +26 -0
  76. package/dist/styles/theme.d.ts +1 -0
  77. package/dist/styles/theme.js +144 -0
  78. package/package.json +32 -0
  79. package/src/components/NAlert.nexa +115 -0
  80. package/src/components/NAutocomplete.nexa +58 -0
  81. package/src/components/NAvatar.nexa +67 -0
  82. package/src/components/NBadge.nexa +61 -0
  83. package/src/components/NBottomSheet.nexa +145 -0
  84. package/src/components/NButton.nexa +275 -0
  85. package/src/components/NCard.nexa +100 -0
  86. package/src/components/NCheckbox.nexa +90 -0
  87. package/src/components/NChips.nexa +57 -0
  88. package/src/components/NDataTable.nexa +186 -0
  89. package/src/components/NDatepicker.nexa +367 -0
  90. package/src/components/NForm.nexa +133 -0
  91. package/src/components/NFormField.nexa +171 -0
  92. package/src/components/NInput.nexa +311 -0
  93. package/src/components/NInputNumber.nexa +199 -0
  94. package/src/components/NModal.nexa +221 -0
  95. package/src/components/NMultiSelect.nexa +77 -0
  96. package/src/components/NPaginator.nexa +77 -0
  97. package/src/components/NPassword.nexa +178 -0
  98. package/src/components/NProgressBar.nexa +111 -0
  99. package/src/components/NRadio.nexa +81 -0
  100. package/src/components/NSelect.nexa +452 -0
  101. package/src/components/NSkeleton.nexa +74 -0
  102. package/src/components/NSwitch.nexa +76 -0
  103. package/src/components/NTabs.nexa +113 -0
  104. package/src/components/NTag.nexa +93 -0
  105. package/src/components/NToastContainer.nexa +221 -0
  106. package/src/components/NTooltip.nexa +166 -0
  107. package/src/components/NTreeMenu.nexa +142 -0
  108. package/src/index.ts +36 -0
  109. package/src/services/FloatingOverlay.ts +133 -0
  110. package/src/services/FormValidation.ts +44 -0
  111. package/src/services/ToastService.ts +41 -0
  112. package/src/shims.d.ts +5 -0
  113. package/src/styles/theme.ts +146 -0
  114. package/src/styles/tokens.css +170 -0
@@ -0,0 +1,367 @@
1
+ <script setup>
2
+ import { signal, computed, onBeforeUnmount } from 'nexa-framework'
3
+ import NInput from './NInput.nexa'
4
+ import { trackFloatingOverlay } from '../services/FloatingOverlay.js'
5
+
6
+ const props = defineProps({
7
+ modelValue: { type: String, default: '' },
8
+ placeholder: { type: String, default: 'Seleccionar fecha' },
9
+ disabled: { type: Boolean, default: false },
10
+ min: { type: String, default: '' },
11
+ max: { type: String, default: '' },
12
+ placement: { type: String, default: 'auto' }
13
+ })
14
+
15
+ const emit = defineEmits(['update:modelValue'])
16
+
17
+ const isOpen = signal(false)
18
+ const viewDate = signal(new Date())
19
+ const instanceId = `n-dp-${Math.random().toString(16).slice(2)}`
20
+ const popupStyle = signal({})
21
+ const resolvedPlacement = signal('bottom')
22
+ let stopTracking = null
23
+
24
+ const today = new Date()
25
+ today.setHours(0, 0, 0, 0)
26
+
27
+ const year = computed(() => viewDate.value.getFullYear())
28
+ const month = computed(() => viewDate.value.getMonth())
29
+
30
+ const daysInMonth = computed(() => new Date(year.value, month.value + 1, 0).getDate())
31
+ const firstDayOfWeek = computed(() => new Date(year.value, month.value, 1).getDay())
32
+
33
+ const calendarDays = computed(() => {
34
+ const days = []
35
+ const totalCells = Math.ceil((firstDayOfWeek.value + daysInMonth.value) / 7) * 7
36
+ for (let i = 0; i < totalCells; i++) {
37
+ const day = i - firstDayOfWeek.value + 1
38
+ if (day > 0 && day <= daysInMonth.value) {
39
+ days.push(day)
40
+ } else {
41
+ days.push(null)
42
+ }
43
+ }
44
+ return days
45
+ })
46
+
47
+ const formatDate = (d) => {
48
+ const y = d.getFullYear()
49
+ const m = String(d.getMonth() + 1).padStart(2, '0')
50
+ const day = String(d.getDate()).padStart(2, '0')
51
+ return `${y}-${m}-${day}`
52
+ }
53
+
54
+ const parseDate = (value) => {
55
+ if (!value) return null
56
+ const parts = String(value).split('-')
57
+ if (parts.length !== 3) return null
58
+ const y = Number(parts[0])
59
+ const m = Number(parts[1])
60
+ const d = Number(parts[2])
61
+ if (!Number.isFinite(y) || !Number.isFinite(m) || !Number.isFinite(d)) return null
62
+ const date = new Date(y, m - 1, d)
63
+ if (date.getFullYear() !== y || date.getMonth() !== m - 1 || date.getDate() !== d) return null
64
+ date.setHours(0, 0, 0, 0)
65
+ return date
66
+ }
67
+
68
+ const minDate = computed(() => parseDate(props.min))
69
+ const maxDate = computed(() => parseDate(props.max))
70
+
71
+ const isDisabled = (day) => {
72
+ if (day == null) return true
73
+ const d = new Date(year.value, month.value, day)
74
+ d.setHours(0, 0, 0, 0)
75
+ const min = minDate.value
76
+ if (min && d < min) return true
77
+ const max = maxDate.value
78
+ if (max && d > max) return true
79
+ return false
80
+ }
81
+
82
+ const closePopup = () => {
83
+ if (!isOpen.value) return
84
+ isOpen.value = false
85
+ if (closeHandler) {
86
+ document.removeEventListener('click', closeHandler)
87
+ closeHandler = null
88
+ }
89
+ if (stopTracking) {
90
+ stopTracking()
91
+ stopTracking = null
92
+ }
93
+ rootEl.value = null
94
+ }
95
+
96
+ const syncViewDateFromModel = () => {
97
+ const selected = parseDate(props.modelValue)
98
+ if (!selected) return
99
+ viewDate.value = new Date(selected.getFullYear(), selected.getMonth(), 1)
100
+ }
101
+
102
+ const selectDate = (day) => {
103
+ if (day == null) return
104
+ if (isDisabled(day)) return
105
+ const d = new Date(year.value, month.value, day)
106
+ const formatted = formatDate(d)
107
+ emit('update:modelValue', formatted)
108
+ closePopup()
109
+ }
110
+
111
+ const prevMonth = () => {
112
+ viewDate.value = new Date(year.value, month.value - 1, 1)
113
+ }
114
+
115
+ const nextMonth = () => {
116
+ viewDate.value = new Date(year.value, month.value + 1, 1)
117
+ }
118
+
119
+ const monthNames = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre']
120
+ const dayNames = ['Do', 'Lu', 'Ma', 'Mi', 'Ju', 'Vi', 'Sa']
121
+
122
+ const isSelected = (day) => {
123
+ if (!props.modelValue) return false
124
+ const d = new Date(year.value, month.value, day)
125
+ return props.modelValue === formatDate(d)
126
+ }
127
+
128
+ const isToday = (day) => {
129
+ const d = new Date(year.value, month.value, day)
130
+ return formatDate(d) === formatDate(today)
131
+ }
132
+
133
+ let closeHandler = null
134
+ const rootEl = signal(null)
135
+
136
+ const openPopup = (e) => {
137
+ if (props.disabled) return
138
+ if (isOpen.value) {
139
+ closePopup()
140
+ return
141
+ }
142
+ isOpen.value = true
143
+ syncViewDateFromModel()
144
+ const target = e?.currentTarget || e?.target
145
+ rootEl.value = target?.closest ? target.closest(`[data-datepicker-root="${instanceId}"]`) : null
146
+ stopTracking = trackFloatingOverlay({
147
+ isOpen: () => isOpen.value,
148
+ getAnchor: () => {
149
+ const root = rootEl.value
150
+ return root ? root.querySelector('.n-datepicker-input') : null
151
+ },
152
+ getPopup: () => document.querySelector(`[data-datepicker-popup="${instanceId}"]`),
153
+ placement: props.placement,
154
+ align: 'start',
155
+ matchWidth: true,
156
+ minWidth: 240,
157
+ gap: 6,
158
+ margin: 8,
159
+ zIndex: 9999,
160
+ onUpdate: (result) => {
161
+ popupStyle.value = result.style
162
+ resolvedPlacement.value = result.placement
163
+ },
164
+ isEventInside: (event) => {
165
+ const t = event.target
166
+ if (!t || typeof t.closest !== 'function') return false
167
+ if (t.closest(`[data-datepicker-root="${instanceId}"]`)) return true
168
+ if (t.closest(`[data-datepicker-popup="${instanceId}"]`)) return true
169
+ return false
170
+ },
171
+ onOutside: () => closePopup(),
172
+ })
173
+ if (closeHandler) document.removeEventListener('click', closeHandler)
174
+ closeHandler = (e) => {
175
+ const target = e.target
176
+ if (target && typeof target.closest === 'function') {
177
+ if (target.closest(`[data-datepicker-root="${instanceId}"]`)) return
178
+ if (target.closest(`[data-datepicker-popup="${instanceId}"]`)) return
179
+ }
180
+ closePopup()
181
+ }
182
+ setTimeout(() => document.addEventListener('click', closeHandler), 0)
183
+ }
184
+
185
+ onBeforeUnmount(() => {
186
+ closePopup()
187
+ })
188
+ </script>
189
+
190
+ <template>
191
+ <div class="n-datepicker" :data-datepicker-root="instanceId">
192
+ <div class="n-datepicker-input" @click="openPopup">
193
+ <NInput
194
+ :modelValue="modelValue"
195
+ :placeholder="placeholder"
196
+ :disabled="disabled"
197
+ readonly
198
+ />
199
+ <span class="n-datepicker-icon">📅</span>
200
+ </div>
201
+
202
+ <Teleport to="body">
203
+ <div v-if="isOpen.value" class="n-datepicker-dropdown" :class="{ 'is-top': resolvedPlacement.value === 'top' }" :data-datepicker-popup="instanceId" :style="popupStyle.value">
204
+ <div class="n-datepicker-header">
205
+ <button type="button" class="n-datepicker-nav" @click="prevMonth">‹</button>
206
+ <span class="n-datepicker-title">{{ monthNames[month.value] }} {{ year.value }}</span>
207
+ <button type="button" class="n-datepicker-nav" @click="nextMonth">›</button>
208
+ </div>
209
+
210
+ <div class="n-datepicker-grid">
211
+ <div v-for="d in dayNames" :key="d" class="n-datepicker-day-header">{{ d }}</div>
212
+ <div
213
+ v-for="(day, i) in calendarDays.value"
214
+ :key="i"
215
+ :class="{
216
+ 'n-datepicker-day': true,
217
+ 'is-empty': day === null,
218
+ 'is-disabled': day !== null && isDisabled(day),
219
+ 'is-selected': day !== null && isSelected(day),
220
+ 'is-today': day !== null && isToday(day)
221
+ }"
222
+ @click="selectDate(day)"
223
+ >{{ day ?? '' }}</div>
224
+ </div>
225
+ </div>
226
+ </Teleport>
227
+ </div>
228
+ </template>
229
+
230
+ <style scoped>
231
+ .n-datepicker {
232
+ position: relative;
233
+ display: inline-block;
234
+ font-family: var(--n-font-sans);
235
+ }
236
+
237
+ .n-datepicker-input {
238
+ position: relative;
239
+ cursor: pointer;
240
+ }
241
+
242
+ .n-datepicker-input .n-input {
243
+ cursor: pointer;
244
+ }
245
+
246
+ .n-datepicker-icon {
247
+ position: absolute;
248
+ right: 0.75rem;
249
+ top: 50%;
250
+ transform: translateY(-50%);
251
+ font-size: 1rem;
252
+ pointer-events: none;
253
+ }
254
+
255
+ .n-datepicker-dropdown {
256
+ position: absolute;
257
+ top: calc(100% + 4px);
258
+ left: 0;
259
+ z-index: var(--n-z-dropdown);
260
+ background: var(--n-color-surface-elevated);
261
+ border: 1px solid var(--n-color-border);
262
+ border-radius: var(--n-radius-lg);
263
+ box-shadow: var(--n-shadow-lg);
264
+ padding: 1rem;
265
+ width: 280px;
266
+ animation: fade-in 0.15s ease;
267
+ }
268
+
269
+ @keyframes fade-in {
270
+ from { opacity: 0; transform: translateY(-4px); }
271
+ to { opacity: 1; transform: translateY(0); }
272
+ }
273
+
274
+ .n-datepicker-dropdown.is-top {
275
+ animation: fade-in-top 0.15s ease;
276
+ }
277
+
278
+ @keyframes fade-in-top {
279
+ from { opacity: 0; transform: translateY(4px); }
280
+ to { opacity: 1; transform: translateY(0); }
281
+ }
282
+
283
+ .n-datepicker-header {
284
+ display: flex;
285
+ align-items: center;
286
+ justify-content: space-between;
287
+ margin-bottom: 0.75rem;
288
+ }
289
+
290
+ .n-datepicker-title {
291
+ font-weight: var(--n-weight-semibold);
292
+ font-size: var(--n-text-sm);
293
+ color: var(--n-color-text);
294
+ }
295
+
296
+ .n-datepicker-nav {
297
+ background: none;
298
+ border: 1px solid var(--n-color-border);
299
+ color: var(--n-color-text-secondary);
300
+ width: 28px;
301
+ height: 28px;
302
+ border-radius: var(--n-radius-sm);
303
+ cursor: pointer;
304
+ font-size: 1.1rem;
305
+ display: flex;
306
+ align-items: center;
307
+ justify-content: center;
308
+ transition: all var(--n-transition-fast);
309
+ }
310
+
311
+ .n-datepicker-nav:hover {
312
+ background: var(--n-color-surface-hover);
313
+ color: var(--n-color-text);
314
+ }
315
+
316
+ .n-datepicker-grid {
317
+ display: grid;
318
+ grid-template-columns: repeat(7, 1fr);
319
+ gap: 2px;
320
+ }
321
+
322
+ .n-datepicker-day-header {
323
+ text-align: center;
324
+ font-size: var(--n-text-xs);
325
+ color: var(--n-color-text-muted);
326
+ font-weight: var(--n-weight-semibold);
327
+ padding: 0.35rem 0;
328
+ }
329
+
330
+ .n-datepicker-day {
331
+ text-align: center;
332
+ padding: 0.4rem 0;
333
+ font-size: var(--n-text-sm);
334
+ color: var(--n-color-text);
335
+ border-radius: var(--n-radius-sm);
336
+ cursor: pointer;
337
+ transition: all var(--n-transition-fast);
338
+ }
339
+
340
+ .n-datepicker-day:hover:not(.is-empty) {
341
+ background: var(--n-color-primary-light);
342
+ }
343
+
344
+ .n-datepicker-day.is-empty {
345
+ cursor: default;
346
+ }
347
+
348
+ .n-datepicker-day.is-disabled {
349
+ opacity: 0.4;
350
+ cursor: not-allowed;
351
+ }
352
+
353
+ .n-datepicker-day.is-disabled:hover {
354
+ background: transparent;
355
+ }
356
+
357
+ .n-datepicker-day.is-today {
358
+ font-weight: var(--n-weight-bold);
359
+ color: var(--n-color-primary);
360
+ }
361
+
362
+ .n-datepicker-day.is-selected {
363
+ background: var(--n-color-primary);
364
+ color: white;
365
+ font-weight: var(--n-weight-semibold);
366
+ }
367
+ </style>
@@ -0,0 +1,132 @@
1
+ import { signal, computed, provide, h, hText, effect, defineComponent, registerComponent, reloadComponent, injectStyle } from 'nexa-framework'
2
+
3
+ const _sfc_main = defineComponent({
4
+ __scopeId: 'data-v-38e99ca2',
5
+ __hmrId: 'NForm_nexa',
6
+ props: {
7
+ validateOn: { type: String, default: 'submit' },
8
+ disabled: { type: Boolean, default: false }
9
+ },
10
+ emits: ['submit', 'invalid'],
11
+ setup(props, setupContext) {
12
+ const { emit, slots, slots: $slots } = setupContext
13
+ const fieldNames = signal([])
14
+ const fields = Object.create(null)
15
+ const registerField = (name, field) => {
16
+ if (!name) return () => {}
17
+ fields[name] = field
18
+ if (!fieldNames.value.includes(name)) fieldNames.value = [...fieldNames.value, name]
19
+ return () => {
20
+ delete fields[name]
21
+ if (fieldNames.value.includes(name)) {
22
+ fieldNames.value = fieldNames.value.filter(n => n !== name)
23
+ }
24
+ }
25
+ }
26
+ const getValues = () => {
27
+ const out = {}
28
+ for (const name of fieldNames.value) {
29
+ const field = fields[name]
30
+ if (!field) continue
31
+ out[name] = field.value.value
32
+ }
33
+ return out
34
+ }
35
+ const values = computed(() => {
36
+ const names = fieldNames.value
37
+ const out = {}
38
+ for (const name of names) {
39
+ const field = fields[name]
40
+ if (!field) continue
41
+ out[name] = field.value.value
42
+ }
43
+ return out
44
+ })
45
+ const errors = computed(() => {
46
+ const names = fieldNames.value
47
+ const out = {}
48
+ for (const name of names) {
49
+ const field = fields[name]
50
+ if (!field) continue
51
+ out[name] = field.errors.value
52
+ }
53
+ return out
54
+ })
55
+ const pending = computed(() => {
56
+ const names = fieldNames.value
57
+ for (const name of names) {
58
+ const field = fields[name]
59
+ if (!field) continue
60
+ if (field.pending.value) return true
61
+ }
62
+ return false
63
+ })
64
+ const valid = computed(() => {
65
+ const names = fieldNames.value
66
+ for (const name of names) {
67
+ const field = fields[name]
68
+ if (!field) continue
69
+ if (field.errors.value.length > 0) return false
70
+ }
71
+ return true
72
+ })
73
+ const validateAll = async () => {
74
+ const tasks = fieldNames.value.map(async (name) => {
75
+ const field = fields[name]
76
+ if (!field) return true
77
+ const errs = await field.validate('submit')
78
+ return errs.length === 0
79
+ })
80
+ const results = await Promise.all(tasks)
81
+ return results.every(Boolean)
82
+ }
83
+ const reset = () => {
84
+ for (const name of fieldNames.value) {
85
+ const field = fields[name]
86
+ if (!field) continue
87
+ field.reset()
88
+ }
89
+ }
90
+ const submit = async () => {
91
+ if (props.disabled) return
92
+ const ok = await validateAll()
93
+ if (!ok) {
94
+ emit('invalid', errors.value)
95
+ return
96
+ }
97
+ emit('submit', values.value)
98
+ }
99
+ const onSubmit = (e) => {
100
+ if (e && typeof e.preventDefault === 'function') e.preventDefault()
101
+ submit()
102
+ }
103
+ provide('nexa-ui:form', {
104
+ validateOn: props.validateOn,
105
+ disabled: props.disabled,
106
+ registerField,
107
+ getValues,
108
+ })
109
+ return { fieldNames, fields, registerField, getValues, values, errors, pending, valid, validateAll, reset, submit, onSubmit, provide, $slots, emit }
110
+ }
111
+ })
112
+ // Injected render function
113
+ _sfc_main.render = function(ctx) {
114
+ const { fieldNames, fields, registerField, getValues, values, errors, pending, valid, validateAll, reset, submit, onSubmit, provide, $slots, emit, validateOn, disabled, Fragment: _ntc_Fragment } = ctx
115
+ return h('form', { class: "n-form", onSubmit: onSubmit, "data-v-38e99ca2": "" }, [
116
+ "\n ",
117
+ ctx.$slots.default ? ctx.$slots.default({ values: values, errors: errors, valid: valid, pending: pending, submit: submit, reset: reset }) : null,
118
+ "\n "
119
+ ])
120
+ }
121
+ _sfc_main.__scopeId = 'data-v-38e99ca2'
122
+ _sfc_main.__hmrId = 'NForm_nexa'
123
+
124
+ export default _sfc_main
125
+
126
+ const __style = `.n-form[data-v-38e99ca2]{
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: var(--n-space-4);
130
+ width: 100%;
131
+ }`
132
+ injectStyle('data-v-38e99ca2', __style)
@@ -0,0 +1,133 @@
1
+ <script setup>
2
+ import { signal, computed, provide } from 'nexa-framework'
3
+
4
+ const props = defineProps({
5
+ validateOn: { type: String, default: 'submit' },
6
+ disabled: { type: Boolean, default: false }
7
+ })
8
+
9
+ const emit = defineEmits(['submit', 'invalid'])
10
+
11
+ const fieldNames = signal([])
12
+ const fields = Object.create(null)
13
+
14
+ const registerField = (name, field) => {
15
+ if (!name) return () => {}
16
+ fields[name] = field
17
+ if (!fieldNames.value.includes(name)) fieldNames.value = [...fieldNames.value, name]
18
+ return () => {
19
+ delete fields[name]
20
+ if (fieldNames.value.includes(name)) {
21
+ fieldNames.value = fieldNames.value.filter(n => n !== name)
22
+ }
23
+ }
24
+ }
25
+
26
+ const getValues = () => {
27
+ const out = {}
28
+ for (const name of fieldNames.value) {
29
+ const field = fields[name]
30
+ if (!field) continue
31
+ out[name] = field.value.value
32
+ }
33
+ return out
34
+ }
35
+
36
+ const values = computed(() => {
37
+ const names = fieldNames.value
38
+ const out = {}
39
+ for (const name of names) {
40
+ const field = fields[name]
41
+ if (!field) continue
42
+ out[name] = field.value.value
43
+ }
44
+ return out
45
+ })
46
+
47
+ const errors = computed(() => {
48
+ const names = fieldNames.value
49
+ const out = {}
50
+ for (const name of names) {
51
+ const field = fields[name]
52
+ if (!field) continue
53
+ out[name] = field.errors.value
54
+ }
55
+ return out
56
+ })
57
+
58
+ const pending = computed(() => {
59
+ const names = fieldNames.value
60
+ for (const name of names) {
61
+ const field = fields[name]
62
+ if (!field) continue
63
+ if (field.pending.value) return true
64
+ }
65
+ return false
66
+ })
67
+
68
+ const valid = computed(() => {
69
+ const names = fieldNames.value
70
+ for (const name of names) {
71
+ const field = fields[name]
72
+ if (!field) continue
73
+ if (field.errors.value.length > 0) return false
74
+ }
75
+ return true
76
+ })
77
+
78
+ const validateAll = async () => {
79
+ const tasks = fieldNames.value.map(async (name) => {
80
+ const field = fields[name]
81
+ if (!field) return true
82
+ const errs = await field.validate('submit')
83
+ return errs.length === 0
84
+ })
85
+ const results = await Promise.all(tasks)
86
+ return results.every(Boolean)
87
+ }
88
+
89
+ const reset = () => {
90
+ for (const name of fieldNames.value) {
91
+ const field = fields[name]
92
+ if (!field) continue
93
+ field.reset()
94
+ }
95
+ }
96
+
97
+ const submit = async () => {
98
+ if (props.disabled) return
99
+ const ok = await validateAll()
100
+ if (!ok) {
101
+ emit('invalid', errors.value)
102
+ return
103
+ }
104
+ emit('submit', values.value)
105
+ }
106
+
107
+ const onSubmit = (e) => {
108
+ if (e && typeof e.preventDefault === 'function') e.preventDefault()
109
+ submit()
110
+ }
111
+
112
+ provide('nexa-ui:form', {
113
+ validateOn: props.validateOn,
114
+ disabled: props.disabled,
115
+ registerField,
116
+ getValues,
117
+ })
118
+ </script>
119
+
120
+ <template>
121
+ <form class="n-form" @submit="onSubmit">
122
+ <slot :values="values" :errors="errors" :valid="valid" :pending="pending" :submit="submit" :reset="reset" />
123
+ </form>
124
+ </template>
125
+
126
+ <style scoped>
127
+ .n-form {
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: var(--n-space-4);
131
+ width: 100%;
132
+ }
133
+ </style>