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,186 @@
1
+ <script setup>
2
+ import { signal, computed, effect } from 'nexa-framework'
3
+ import NPaginator from './NPaginator.nexa'
4
+ const props = defineProps({ value: { type: Array, default: () => [] }, columns: { type: Array, default: () => [] }, stripedRows: { type: Boolean, default: false }, hoverRows: { type: Boolean, default: true }, size: { type: String, default: 'md' }, scrollable: { type: Boolean, default: true }, paginator: { type: Boolean, default: true }, rows: { type: Number, default: 10 }, first: { type: Number, default: 0 }, rowsPerPageOptions: { type: Array, default: () => [10, 20, 50] }, selectionMode: { type: String, default: '' }, selection: { type: null, default: null }, dataKey: { type: String, default: '' }, sortField: { type: String, default: '' }, sortOrder: { type: Number, default: 1 }, resizableColumns: { type: Boolean, default: true }, columnResizeMode: { type: String, default: 'fit' }, filterDisplay: { type: String, default: 'row' }, globalFilter: { type: String, default: '' }, globalFilterFields: { type: Array, default: null }, filters: { type: null, default: null }, lazy: { type: Boolean, default: false }, totalRecords: { type: Number, default: 0 }, emptyMessage: { type: String, default: 'No records found' }, showGridlines: { type: Boolean, default: false } })
5
+ const emit = defineEmits(['update:first', 'update:rows', 'update:selection', 'update:sortField', 'update:sortOrder', 'update:globalFilter', 'update:filters', 'updateFirst', 'updateRows', 'updateSelection', 'updateSortField', 'updateSortOrder', 'updateGlobalFilter', 'updateFilters', 'page', 'rowSelect', 'rowUnselect', 'sort', 'filter'])
6
+ const internalFirst = signal(props.first || 0)
7
+ const internalRows = signal(props.rows || 10)
8
+ const internalSortField = signal(props.sortField || '')
9
+ const internalSortOrder = signal(props.sortOrder || 1)
10
+ const internalFilters = signal((props.filters && typeof props.filters === 'object') ? props.filters : { __global: props.globalFilter || '' })
11
+ const effectiveFilters = computed(() => (props.filters && typeof props.filters === 'object') ? props.filters : internalFilters.value)
12
+ const columnWidths = signal({})
13
+ const resizing = signal(null)
14
+ const internalSelection = signal(props.selection)
15
+
16
+ effect(() => { internalFirst.value = props.first || 0 })
17
+ effect(() => { internalRows.value = props.rows || 10 })
18
+ effect(() => { internalSortField.value = props.sortField || '' })
19
+ effect(() => { internalSortOrder.value = props.sortOrder || 1 })
20
+ let lastPropGlobal = props.globalFilter || ''
21
+ effect(() => {
22
+ const next = props.globalFilter || ''
23
+ if (next === lastPropGlobal) return
24
+ lastPropGlobal = next
25
+ if (props.filters && typeof props.filters === 'object') return
26
+ internalFilters.value = { ...internalFilters.value, __global: next }
27
+ })
28
+
29
+ let lastPropSelection = props.selection
30
+ effect(() => {
31
+ const next = props.selection
32
+ if (next === lastPropSelection) return
33
+ lastPropSelection = next
34
+ internalSelection.value = next
35
+ })
36
+ const normalizeColumns = computed(() => (props.columns || []).map((c, idx) => { const field = c.field || `col_${idx}`; const header = c.header ?? field; const sortable = !!c.sortable; const filterable = c.filterable !== false; const type = c.type || 'text'; const width = c.width || ''; const minWidth = c.minWidth || '120px'; const align = c.align || (type === 'number' ? 'right' : 'left'); const filterMatchMode = c.filterMatchMode || 'contains'; return { ...c, field, header, sortable, filterable, type, width, minWidth, align, filterMatchMode } }))
37
+ const getRowKey = (row, index) => (props.dataKey && row && typeof row === 'object' && props.dataKey in row) ? String(row[props.dataKey]) : String(index)
38
+ const effectiveSelection = computed(() => internalSelection.value)
39
+ const isRowSelected = (row, index) => {
40
+ if (!props.selectionMode) return false
41
+ const selection = effectiveSelection.value
42
+ const key = getRowKey(row, index)
43
+ if (props.selectionMode === 'single') {
44
+ if (!selection) return false
45
+ if (props.dataKey && typeof selection === 'object' && selection && props.dataKey in selection) return String(selection[props.dataKey]) === key
46
+ return selection === row
47
+ }
48
+ const current = Array.isArray(selection) ? selection : []
49
+ if (props.dataKey) return current.some(s => s && typeof s === 'object' && props.dataKey in s && String(s[props.dataKey]) === key)
50
+ return current.includes(row)
51
+ }
52
+ const toggleRowSelection = (row, index) => {
53
+ if (!props.selectionMode) return
54
+ const selected = isRowSelected(row, index)
55
+ if (props.selectionMode === 'single') {
56
+ const next = selected ? null : row
57
+ internalSelection.value = next
58
+ emit('update:selection', next)
59
+ emit('updateSelection', next)
60
+ emit(selected ? 'rowUnselect' : 'rowSelect', { data: row })
61
+ return
62
+ }
63
+ const current = Array.isArray(effectiveSelection.value) ? effectiveSelection.value : []
64
+ const key = getRowKey(row, index)
65
+ const next = selected
66
+ ? (props.dataKey ? current.filter(r => String(r?.[props.dataKey]) !== key) : current.filter(r => r !== row))
67
+ : [...current, row]
68
+ internalSelection.value = next
69
+ emit('update:selection', next)
70
+ emit('updateSelection', next)
71
+ emit(selected ? 'rowUnselect' : 'rowSelect', { data: row })
72
+ }
73
+
74
+ const allVisibleSelected = computed(() => {
75
+ if (props.selectionMode !== 'multiple') return false
76
+ const rows = visibleRows.value
77
+ if (rows.length === 0) return false
78
+ for (let i = 0; i < rows.length; i++) {
79
+ if (!isRowSelected(rows[i], i + (internalFirst.value || 0))) return false
80
+ }
81
+ return true
82
+ })
83
+
84
+ const toggleAllVisible = () => {
85
+ if (props.selectionMode !== 'multiple') return
86
+ const current = Array.isArray(effectiveSelection.value) ? effectiveSelection.value : []
87
+ const rows = visibleRows.value
88
+ const startIndex = internalFirst.value || 0
89
+ if (rows.length === 0) return
90
+ if (allVisibleSelected.value) {
91
+ if (!props.dataKey) {
92
+ const next = current.filter(r => !rows.includes(r))
93
+ internalSelection.value = next
94
+ emit('update:selection', next)
95
+ emit('updateSelection', next)
96
+ return
97
+ }
98
+ const keys = rows.map((r, i) => getRowKey(r, startIndex + i))
99
+ const next = current.filter(r => !keys.includes(String(r?.[props.dataKey])))
100
+ internalSelection.value = next
101
+ emit('update:selection', next)
102
+ emit('updateSelection', next)
103
+ return
104
+ }
105
+ if (!props.dataKey) {
106
+ const next = [...current, ...rows.filter(r => !current.includes(r))]
107
+ internalSelection.value = next
108
+ emit('update:selection', next)
109
+ emit('updateSelection', next)
110
+ return
111
+ }
112
+ const existing = new Set(current.map(r => String(r?.[props.dataKey])))
113
+ const next = [...current, ...rows.filter((r) => !existing.has(String(r?.[props.dataKey])))]
114
+ internalSelection.value = next
115
+ emit('update:selection', next)
116
+ emit('updateSelection', next)
117
+ }
118
+ const getHeaderContent = (col) => (col.headerTemplate && typeof col.headerTemplate === 'function') ? col.headerTemplate(col) : col.header
119
+ const getRawValue = (row, col) => { if (!row) return ''; if (col.field && typeof row === 'object' && col.field in row) return row[col.field]; return '' }
120
+ const getCellContent = (row, col, index) => { const raw = getRawValue(row, col); if (col.body && typeof col.body === 'function') return col.body(row, { index, column: col, field: col.field, value: raw }); return raw }
121
+ const matchFilter = (value, query, mode) => { const q = String(query ?? '').toLowerCase().trim(); if (!q) return true; const v = String(value ?? '').toLowerCase(); if (mode === 'equals') return v === q; if (mode === 'startsWith') return v.startsWith(q); if (mode === 'endsWith') return v.endsWith(q); return v.includes(q) }
122
+ const filteredRows = computed(() => { const rows = props.value || []; if (props.lazy) return rows; const cols = normalizeColumns.value; const filters = effectiveFilters.value || {}; const global = String(filters.__global || '').trim(); const keys = Object.keys(filters).filter(k => k !== '__global'); if (!global && keys.length === 0) return rows; const globalFields = Array.isArray(props.globalFilterFields) && props.globalFilterFields.length ? props.globalFilterFields : cols.map(c => c.field); return rows.filter((row) => { if (global) { const ok = globalFields.some((f) => matchFilter(getRawValue(row, { field: f }), global, 'contains')); if (!ok) return false } for (const c of cols) { const f = filters[c.field]; if (!f) continue; if (!matchFilter(getRawValue(row, c), f.value, f.matchMode || c.filterMatchMode || 'contains')) return false } return true }) })
123
+ const sortedRows = computed(() => { if (props.lazy) return filteredRows.value; const rows = [...filteredRows.value]; const field = internalSortField.value; if (!field) return rows; const col = normalizeColumns.value.find(c => c.field === field); const order = internalSortOrder.value === -1 ? -1 : 1; const type = col?.type || 'text'; rows.sort((a, b) => { const av = getCellContent(a, { field }, -1); const bv = getCellContent(b, { field }, -1); if (type === 'number') return (Number(av) - Number(bv)) * order; return String(av ?? '').localeCompare(String(bv ?? ''), undefined, { numeric: true, sensitivity: 'base' }) * order }); return rows })
124
+ const totalRecords = computed(() => props.lazy ? (props.totalRecords || (props.value || []).length) : sortedRows.value.length)
125
+ const visibleRows = computed(() => props.lazy ? (props.value || []) : (!props.paginator ? sortedRows.value : sortedRows.value.slice(internalFirst.value || 0, (internalFirst.value || 0) + (internalRows.value || 10))))
126
+ const setSort = (col) => {
127
+ if (!col.sortable) return
128
+ const nextField = col.field
129
+ let nextOrder = 1
130
+ if (internalSortField.value === nextField) nextOrder = internalSortOrder.value === 1 ? -1 : 1
131
+ internalSortField.value = nextField
132
+ internalSortOrder.value = nextOrder
133
+ internalFirst.value = 0
134
+ emit('update:sortField', nextField)
135
+ emit('update:sortOrder', nextOrder)
136
+ emit('updateSortField', nextField)
137
+ emit('updateSortOrder', nextOrder)
138
+ emit('sort', { sortField: nextField, sortOrder: nextOrder })
139
+ }
140
+ const onSortClick = (e) => { const field = e.currentTarget?.dataset?.field; if (!field) return; const col = normalizeColumns.value.find(c => c.field === field); if (!col) return; setSort(col) }
141
+ const setGlobal = (e) => {
142
+ const next = e.target.value
143
+ const nextFilters = { ...(effectiveFilters.value || {}), __global: next }
144
+ if (props.filters && typeof props.filters === 'object') { emit('update:filters', nextFilters); emit('updateFilters', nextFilters) } else { internalFilters.value = nextFilters }
145
+ internalFirst.value = 0
146
+ emit('update:globalFilter', next)
147
+ emit('updateGlobalFilter', next)
148
+ emit('filter', { global: next, filters: nextFilters })
149
+ }
150
+ const setColumnFilter = (field, e) => { const next = e.target.value; const current = { ...(effectiveFilters.value || {}) }; const col = normalizeColumns.value.find(c => c.field === field); if (!next) delete current[field]; else current[field] = { value: next, matchMode: col?.filterMatchMode || current[field]?.matchMode || 'contains' }; if (props.filters && typeof props.filters === 'object') { emit('update:filters', current); emit('updateFilters', current) } else { internalFilters.value = current } internalFirst.value = 0; emit('filter', { field, value: next, filters: current }) }
151
+ const onColumnFilterInput = (e) => { const field = e.target?.dataset?.field; if (!field) return; setColumnFilter(field, e) }
152
+ const onPage = ({ first, rows }) => {
153
+ internalFirst.value = first
154
+ internalRows.value = rows
155
+ emit('update:first', first)
156
+ emit('update:rows', rows)
157
+ emit('updateFirst', first)
158
+ emit('updateRows', rows)
159
+ emit('page', { first, rows })
160
+ }
161
+ const getWidth = (col) => columnWidths.value[col.field] || col.width || ''
162
+ const startResize = (e) => { if (!props.resizableColumns) return; const field = e.currentTarget?.dataset?.field; if (!field) return; e.preventDefault(); e.stopPropagation(); const th = e.currentTarget?.parentElement; const base = th?.getBoundingClientRect?.().width || parseFloat(th?.style?.width || '') || 160; resizing.value = { field, startX: e.clientX, startW: base }; const onMove = (ev) => { if (!resizing.value) return; const delta = ev.clientX - resizing.value.startX; const next = Math.max(80, resizing.value.startW + delta); columnWidths.value = { ...columnWidths.value, [resizing.value.field]: `${Math.round(next)}px` } }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); resizing.value = null }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp) }
163
+ </script>
164
+
165
+ <template>
166
+ <div class="n-dt" :class="[`n-dt-${size}`, showGridlines ? 'is-grid' : '', scrollable ? 'is-scroll' : '']">
167
+ <div class="n-dt-toolbar"><div class="n-dt-global"><input class="n-dt-global-input" :value="effectiveFilters.value.__global || ''" placeholder="Search..." @input="setGlobal" /></div></div>
168
+ <div class="n-dt-wrapper">
169
+ <table class="n-dt-table" :class="{ 'is-striped': stripedRows, 'is-hover': hoverRows }">
170
+ <thead class="n-dt-thead">
171
+ <tr class="n-dt-head-row"><th v-if="selectionMode" class="n-dt-th is-selection"><input v-if="selectionMode === 'multiple'" class="n-dt-selectbox" type="checkbox" :checked="allVisibleSelected.value" @click.stop="toggleAllVisible" /></th><th v-for="col in normalizeColumns.value" :key="col.field" class="n-dt-th" :class="[`is-${col.align}`, col.sortable ? 'is-sortable' : '']" :style="{ width: getWidth(col) || undefined, minWidth: col.minWidth }" :data-field="col.field" @click="onSortClick"><div class="n-dt-th-content"><span class="n-dt-th-text">{{ getHeaderContent(col) }}</span><span v-if="col.sortable" class="n-dt-sort" :class="{ 'is-active': internalSortField.value === col.field }"><span v-if="internalSortField.value !== col.field" class="n-dt-sort-icon">↕</span><span v-else class="n-dt-sort-icon">{{ internalSortOrder.value === 1 ? '↑' : '↓' }}</span></span></div><span v-if="resizableColumns" class="n-dt-resizer" :data-field="col.field" @mousedown="startResize"></span></th></tr>
172
+ <tr v-if="filterDisplay === 'row'" class="n-dt-filter-row"><th v-if="selectionMode" class="n-dt-th is-selection"></th><th v-for="col in normalizeColumns.value" :key="col.field" class="n-dt-th" :class="`is-${col.align}`" :style="{ width: getWidth(col) || undefined, minWidth: col.minWidth }"><input v-if="col.filterable" class="n-dt-filter" :data-field="col.field" :value="(effectiveFilters.value[col.field]?.value) || ''" placeholder="Filter" @input="onColumnFilterInput" /></th></tr>
173
+ </thead>
174
+ <tbody class="n-dt-tbody">
175
+ <tr v-for="(row, i) in visibleRows.value" :key="getRowKey(row, i + internalFirst.value)" class="n-dt-row" :class="{ 'is-selected': isRowSelected(row, i + internalFirst.value) }" @click="toggleRowSelection(row, i + internalFirst.value)"><td v-if="selectionMode" class="n-dt-td is-selection"><input class="n-dt-selectbox" type="checkbox" :checked="isRowSelected(row, i + internalFirst.value)" @click.stop="toggleRowSelection(row, i + internalFirst.value)" /></td><td v-for="col in normalizeColumns.value" :key="col.field" class="n-dt-td" :class="`is-${col.align}`" :style="{ width: getWidth(col) || undefined, minWidth: col.minWidth }">{{ getCellContent(row, col, i + internalFirst.value) }}</td></tr>
176
+ <tr v-if="visibleRows.value.length === 0" class="n-dt-empty-row"><td :colspan="normalizeColumns.value.length + (selectionMode ? 1 : 0)" class="n-dt-empty">{{ emptyMessage }}</td></tr>
177
+ </tbody>
178
+ </table>
179
+ </div>
180
+ <NPaginator v-if="paginator" :first="internalFirst.value" :rows="internalRows.value" :totalRecords="totalRecords.value" :rowsPerPageOptions="rowsPerPageOptions" @page="onPage" />
181
+ </div>
182
+ </template>
183
+
184
+ <style scoped>
185
+ .n-dt{border:1px solid var(--n-color-border);border-radius:var(--n-radius-lg);background:var(--n-color-surface);overflow:hidden;font-family:var(--n-font-sans)}.n-dt-toolbar{display:flex;align-items:center;justify-content:space-between;padding:var(--n-space-3) var(--n-space-4);border-bottom:1px solid var(--n-color-border);background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(0,0,0,.08))}.n-dt-global{display:flex;align-items:center;gap:var(--n-space-2)}.n-dt-global-input{width:280px;max-width:100%;background:var(--n-color-bg);border:1px solid var(--n-color-border);border-radius:var(--n-radius-md);padding:0.55rem 0.75rem;color:var(--n-color-text);font-size:var(--n-text-sm);outline:none;box-sizing:border-box}.n-dt-global-input:focus{border-color:var(--n-color-primary);box-shadow:0 0 0 3px var(--n-color-primary-light)}.n-dt-wrapper{width:100%;overflow:auto}.n-dt-table{width:100%;border-collapse:separate;border-spacing:0;table-layout:fixed}.n-dt-thead{background:var(--n-color-surface)}.n-dt-th,.n-dt-td{padding:0.75rem 0.9rem;border-bottom:1px solid var(--n-color-border);color:var(--n-color-text);font-size:var(--n-text-sm);vertical-align:middle}.n-dt-th{position:relative;overflow:hidden;background:rgba(0,0,0,.10);color:var(--n-color-text-secondary);font-weight:var(--n-weight-semibold);user-select:none}.n-dt-th.is-sortable{cursor:pointer}.n-dt-th-content{display:flex;align-items:center;justify-content:space-between;gap:0.5rem}.n-dt-sort{display:inline-flex;align-items:center;gap:0.25rem;color:var(--n-color-text-muted)}.n-dt-sort.is-active{color:var(--n-color-primary)}.n-dt-resizer{position:absolute;right:0;top:0;bottom:0;width:8px;cursor:col-resize}.n-dt-filter-row .n-dt-th{background:rgba(0,0,0,.06)}.n-dt-filter{width:100%;max-width:100%;display:block;background:var(--n-color-bg);border:1px solid var(--n-color-border);border-radius:var(--n-radius-sm);padding:0.35rem 0.5rem;color:var(--n-color-text);font-size:var(--n-text-xs);outline:none;box-sizing:border-box}.n-dt-filter:focus{border-color:var(--n-color-primary)}.n-dt-td{background:transparent;color:var(--n-color-text)}.n-dt-row.is-selected .n-dt-td{background:rgba(59,130,246,.12)}.n-dt-table.is-striped .n-dt-row:nth-child(even) .n-dt-td{background:rgba(0,0,0,.06)}.n-dt-table.is-hover .n-dt-row:hover .n-dt-td{background:rgba(255,255,255,.06)}.n-dt-empty{text-align:center;color:var(--n-color-text-muted);padding:1.25rem}.is-left{text-align:left}.is-right{text-align:right}.is-center{text-align:center}.is-selection{width:44px;min-width:44px;max-width:44px;text-align:center}.n-dt-selectbox{width:16px;height:16px;accent-color:var(--n-color-primary)}.n-dt-check{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:4px;border:1px solid var(--n-color-border);color:var(--n-color-primary)}.is-grid .n-dt-td,.is-grid .n-dt-th{border-right:1px solid var(--n-color-border)}.is-grid .n-dt-td:last-child,.is-grid .n-dt-th:last-child{border-right:none}.n-dt-sm .n-dt-th,.n-dt-sm .n-dt-td{padding:0.55rem 0.7rem}.n-dt-lg .n-dt-th,.n-dt-lg .n-dt-td{padding:0.9rem 1rem}
186
+ </style>
@@ -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>