nexa-ui-kit 0.10.0 → 0.11.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 (73) hide show
  1. package/dist/components/NAlert.js +49 -28
  2. package/dist/components/NAlert.nexa +12 -6
  3. package/dist/components/NAutocomplete.js +25 -20
  4. package/dist/components/NAutocomplete.nexa +3 -3
  5. package/dist/components/NAvatar.js +1 -1
  6. package/dist/components/NBadge.js +1 -1
  7. package/dist/components/NBottomSheet.js +17 -17
  8. package/dist/components/NBottomSheet.nexa +2 -2
  9. package/dist/components/NButton.js +59 -59
  10. package/dist/components/NButton.nexa +10 -9
  11. package/dist/components/NCard.js +2 -2
  12. package/dist/components/NCard.nexa +1 -1
  13. package/dist/components/NCheckbox.js +23 -19
  14. package/dist/components/NCheckbox.nexa +2 -1
  15. package/dist/components/NChips.js +13 -10
  16. package/dist/components/NChips.nexa +1 -1
  17. package/dist/components/NDataTable.js +360 -42
  18. package/dist/components/NDataTable.nexa +318 -10
  19. package/dist/components/NDatepicker.js +52 -43
  20. package/dist/components/NDatepicker.nexa +3 -3
  21. package/dist/components/NForm.js +1 -1
  22. package/dist/components/NFormField.js +1 -1
  23. package/dist/components/NInput.js +51 -41
  24. package/dist/components/NInput.nexa +4 -3
  25. package/dist/components/NInputNumber.js +18 -13
  26. package/dist/components/NInputNumber.nexa +2 -2
  27. package/dist/components/NModal.js +34 -28
  28. package/dist/components/NModal.nexa +4 -4
  29. package/dist/components/NMultiSelect.js +47 -38
  30. package/dist/components/NMultiSelect.nexa +3 -3
  31. package/dist/components/NPaginator.js +29 -21
  32. package/dist/components/NPaginator.nexa +4 -4
  33. package/dist/components/NPassword.js +61 -47
  34. package/dist/components/NPassword.nexa +5 -4
  35. package/dist/components/NProgressBar.js +5 -5
  36. package/dist/components/NProgressBar.nexa +4 -4
  37. package/dist/components/NRadio.js +1 -1
  38. package/dist/components/NScrollView.js +1 -1
  39. package/dist/components/NSelect.js +68 -63
  40. package/dist/components/NSelect.nexa +4 -4
  41. package/dist/components/NSkeleton.js +1 -1
  42. package/dist/components/NSwitch.js +1 -1
  43. package/dist/components/NTabs.js +1 -1
  44. package/dist/components/NTag.js +27 -24
  45. package/dist/components/NTag.nexa +6 -6
  46. package/dist/components/NToastContainer.js +65 -49
  47. package/dist/components/NToastContainer.nexa +6 -3
  48. package/dist/components/NTooltip.js +1 -1
  49. package/dist/components/NTreeMenu.js +24 -22
  50. package/dist/components/NTreeMenu.nexa +1 -1
  51. package/dist/styles/tokens.css +18 -0
  52. package/package.json +4 -4
  53. package/src/components/NAlert.nexa +12 -6
  54. package/src/components/NAutocomplete.nexa +3 -3
  55. package/src/components/NBottomSheet.nexa +2 -2
  56. package/src/components/NButton.nexa +10 -9
  57. package/src/components/NCard.nexa +1 -1
  58. package/src/components/NCheckbox.nexa +2 -1
  59. package/src/components/NChips.nexa +1 -1
  60. package/src/components/NDataTable.nexa +318 -10
  61. package/src/components/NDatepicker.nexa +3 -3
  62. package/src/components/NInput.nexa +4 -3
  63. package/src/components/NInputNumber.nexa +2 -2
  64. package/src/components/NModal.nexa +4 -4
  65. package/src/components/NMultiSelect.nexa +3 -3
  66. package/src/components/NPaginator.nexa +4 -4
  67. package/src/components/NPassword.nexa +5 -4
  68. package/src/components/NProgressBar.nexa +4 -4
  69. package/src/components/NSelect.nexa +4 -4
  70. package/src/components/NTag.nexa +6 -6
  71. package/src/components/NToastContainer.nexa +6 -3
  72. package/src/components/NTreeMenu.nexa +1 -1
  73. package/src/styles/tokens.css +18 -0
@@ -46,6 +46,7 @@ const btnClass = computed(() => {
46
46
  :class="btnClass.value"
47
47
  :type="type"
48
48
  :disabled="disabled || loading"
49
+ :aria-busy="loading ? 'true' : undefined"
49
50
  @click="handleClick"
50
51
  >
51
52
  <span v-if="loading" class="n-btn-loader"></span>
@@ -123,11 +124,11 @@ const btnClass = computed(() => {
123
124
  .n-btn-success {
124
125
  background: linear-gradient(135deg, var(--n-color-success) 0%, var(--n-color-success-hover) 100%);
125
126
  color: white;
126
- box-shadow: 0 4px 12px -2px rgba(16, 185, 129, 0.3);
127
+ box-shadow: var(--n-shadow-glow-success);
127
128
  }
128
129
  .n-btn-success:hover:not(:disabled) {
129
130
  transform: translateY(-2px);
130
- box-shadow: 0 8px 20px -3px rgba(16, 185, 129, 0.4);
131
+ box-shadow: var(--n-shadow-glow-success);
131
132
  }
132
133
  .n-btn-success:active:not(:disabled) {
133
134
  transform: translateY(0) scale(0.97);
@@ -137,11 +138,11 @@ const btnClass = computed(() => {
137
138
  .n-btn-warning {
138
139
  background: linear-gradient(135deg, var(--n-color-warning) 0%, var(--n-color-warning-hover) 100%);
139
140
  color: white;
140
- box-shadow: 0 4px 12px -2px rgba(245, 158, 11, 0.3);
141
+ box-shadow: var(--n-shadow-glow-warning);
141
142
  }
142
143
  .n-btn-warning:hover:not(:disabled) {
143
144
  transform: translateY(-2px);
144
- box-shadow: 0 8px 20px -3px rgba(245, 158, 11, 0.4);
145
+ box-shadow: var(--n-shadow-glow-warning);
145
146
  }
146
147
  .n-btn-warning:active:not(:disabled) {
147
148
  transform: translateY(0) scale(0.97);
@@ -151,11 +152,11 @@ const btnClass = computed(() => {
151
152
  .n-btn-info {
152
153
  background: linear-gradient(135deg, var(--n-color-info) 0%, var(--n-color-info-hover) 100%);
153
154
  color: white;
154
- box-shadow: 0 4px 12px -2px rgba(6, 182, 212, 0.3);
155
+ box-shadow: var(--n-shadow-glow-info);
155
156
  }
156
157
  .n-btn-info:hover:not(:disabled) {
157
158
  transform: translateY(-2px);
158
- box-shadow: 0 8px 20px -3px rgba(6, 182, 212, 0.4);
159
+ box-shadow: var(--n-shadow-glow-info);
159
160
  }
160
161
  .n-btn-info:active:not(:disabled) {
161
162
  transform: translateY(0) scale(0.97);
@@ -165,11 +166,11 @@ const btnClass = computed(() => {
165
166
  .n-btn-danger {
166
167
  background: linear-gradient(135deg, var(--n-color-danger) 0%, var(--n-color-danger-hover) 100%);
167
168
  color: white;
168
- box-shadow: 0 4px 12px -2px rgba(239, 68, 68, 0.3);
169
+ box-shadow: var(--n-shadow-glow-danger);
169
170
  }
170
171
  .n-btn-danger:hover:not(:disabled) {
171
172
  transform: translateY(-2px);
172
- box-shadow: 0 8px 20px -3px rgba(220, 38, 38, 0.4);
173
+ box-shadow: var(--n-shadow-glow-danger);
173
174
  }
174
175
  .n-btn-danger:active:not(:disabled) {
175
176
  transform: translateY(0) scale(0.97);
@@ -258,7 +259,7 @@ const btnClass = computed(() => {
258
259
  .n-btn-ripple {
259
260
  position: absolute;
260
261
  border-radius: var(--n-radius-full);
261
- background: rgba(255, 255, 255, 0.35);
262
+ background: var(--n-color-glass);
262
263
  width: 20px;
263
264
  height: 20px;
264
265
  margin-left: -10px;
@@ -102,7 +102,7 @@ const props = defineProps({
102
102
 
103
103
  .n-card-footer {
104
104
  padding: var(--n-space-4) var(--n-space-6);
105
- background: rgba(0, 0, 0, 0.15);
105
+ background: var(--n-color-glass);
106
106
  border-top: 1px solid var(--n-color-border);
107
107
  margin-top: auto;
108
108
  }
@@ -18,7 +18,8 @@ const toggle = () => {
18
18
  <label class="n-checkbox" :class="{ 'is-checked': modelValue && !indeterminate, 'is-indeterminate': indeterminate, 'is-disabled': disabled }">
19
19
  <div class="n-checkbox-box" @click="toggle">
20
20
  <span class="n-checkbox-icon">
21
- {{ indeterminate ? '–' : '✓' }}
21
+ <svg v-if="indeterminate" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" focusable="false" aria-hidden="true"><line x1="4" y1="12" x2="20" y2="12"/></svg>
22
+ <svg v-else viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
22
23
  </span>
23
24
  </div>
24
25
  <span v-if="label" class="n-checkbox-label">{{ label }}</span>
@@ -47,7 +47,7 @@ const removeAt = (index) => {
47
47
  <div class="n-chips" :class="{ 'is-disabled': disabled }">
48
48
  <div v-for="(chip, i) in chips.value" :key="String(chip.value) + ':' + i" class="n-chip">
49
49
  <span class="n-chip-label">{{ chip.label }}</span>
50
- <button v-if="removable" type="button" class="n-chip-remove" :disabled="disabled" aria-label="Remove" @click="removeAt(i)">✕</button>
50
+ <button v-if="removable" type="button" class="n-chip-remove" :disabled="disabled" aria-label="Remove" @click="removeAt(i)"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M18 6L6 18"/><path d="M6 6l12 12"/></svg></button>
51
51
  </div>
52
52
  </div>
53
53
  </template>
@@ -1,7 +1,7 @@
1
1
  <script setup>
2
2
  import { signal, computed, effect } from 'nexa-framework'
3
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 } })
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 }, searchPlaceholder: { type: String, default: 'Search...' }, filterPlaceholder: { type: String, default: 'Filter' }, loading: { type: Boolean, default: false } })
5
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
6
  const internalFirst = signal(props.first || 0)
7
7
  const internalRows = signal(props.rows || 10)
@@ -163,29 +163,47 @@ const startResize = (e) => { if (!props.resizableColumns) return; const field =
163
163
  </script>
164
164
 
165
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>
166
+ <div class="n-dt" :class="[`n-dt-${size}`, showGridlines ? 'is-grid' : '', scrollable ? 'is-scroll' : '']" :aria-busy="loading ? 'true' : undefined">
167
+ <div class="n-dt-toolbar">
168
+ <div class="n-dt-global">
169
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
170
+ <input class="n-dt-global-input" :value="effectiveFilters.value.__global || ''" :placeholder="searchPlaceholder" @input="setGlobal" />
171
+ </div>
172
+ </div>
168
173
  <div class="n-dt-wrapper">
169
174
  <table class="n-dt-table" :class="{ 'is-striped': stripedRows, 'is-hover': hoverRows }">
170
175
  <thead class="n-dt-thead">
171
176
  <tr class="n-dt-head-row">
172
- <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>
177
+ <th v-if="selectionMode" class="n-dt-th is-selection">
178
+ <button v-if="selectionMode === 'multiple'" type="button" class="n-dt-selectbox" :class="{ 'is-checked': allVisibleSelected.value }" @click.stop="toggleAllVisible" :aria-label="allVisibleSelected.value ? 'Deselect all' : 'Select all'">
179
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
180
+ </button>
181
+ </th>
173
182
  <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">
174
183
  <div class="n-dt-th-content">
175
184
  <span class="n-dt-th-text">{{ $slots.value && $slots.value[`header-${col.field}`] ? $slots[`header-${col.field}`]({ column: col }) : getHeaderContent(col) }}</span>
176
185
  <span v-if="col.sortable" class="n-dt-sort" :class="{ 'is-active': internalSortField.value === col.field }">
177
- <span v-if="internalSortField.value !== col.field" class="n-dt-sort-icon">↕</span>
178
- <span v-else class="n-dt-sort-icon">{{ internalSortOrder.value === 1 ? '↑' : '↓' }}</span>
186
+ <svg class="n-dt-sort-arrow n-dt-sort-arrow-up" :class="{ 'is-on': internalSortField.value === col.field && internalSortOrder.value === 1 }" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>
187
+ <svg class="n-dt-sort-arrow n-dt-sort-arrow-down" :class="{ 'is-on': internalSortField.value === col.field && internalSortOrder.value === -1 }" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M12 5v14M19 12l-7 7-7-7"/></svg>
179
188
  </span>
180
189
  </div>
181
190
  <span v-if="resizableColumns" class="n-dt-resizer" :data-field="col.field" @mousedown="startResize"></span>
182
191
  </th>
183
192
  </tr>
184
- <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>
193
+ <tr v-if="filterDisplay === 'row'" class="n-dt-filter-row">
194
+ <th v-if="selectionMode" class="n-dt-th is-selection"></th>
195
+ <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 }">
196
+ <input v-if="col.filterable" class="n-dt-filter" :data-field="col.field" :value="(effectiveFilters.value[col.field]?.value) || ''" :placeholder="filterPlaceholder" @input="onColumnFilterInput" />
197
+ </th>
198
+ </tr>
185
199
  </thead>
186
200
  <tbody class="n-dt-tbody">
187
201
  <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)">
188
- <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>
202
+ <td v-if="selectionMode" class="n-dt-td is-selection">
203
+ <button v-if="selectionMode === 'multiple'" type="button" class="n-dt-selectbox" :class="{ 'is-checked': isRowSelected(row, i + internalFirst.value) }" @click.stop="toggleRowSelection(row, i + internalFirst.value)" :aria-label="isRowSelected(row, i + internalFirst.value) ? 'Deselect row' : 'Select row'">
204
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
205
+ </button>
206
+ </td>
189
207
  <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 }">
190
208
  {{ $slots.value && $slots.value[`body-${col.field}`] ? $slots[`body-${col.field}`]({ data: row, column: col, index: i + internalFirst.value }) : getCellContent(row, col, i + internalFirst.value) }}
191
209
  </td>
@@ -199,5 +217,295 @@ const startResize = (e) => { if (!props.resizableColumns) return; const field =
199
217
  </template>
200
218
 
201
219
  <style scoped>
202
- .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}
203
- </style>
220
+ .n-dt {
221
+ border: 1px solid var(--n-color-border);
222
+ border-radius: var(--n-radius-lg);
223
+ background: var(--n-color-surface);
224
+ overflow: hidden;
225
+ font-family: var(--n-font-sans);
226
+ }
227
+
228
+ .n-dt.is-grid {
229
+ border-color: var(--n-color-border);
230
+ }
231
+
232
+ .n-dt.is-grid .n-dt-th,
233
+ .n-dt.is-grid .n-dt-td {
234
+ border: 1px solid var(--n-color-border);
235
+ }
236
+
237
+ .n-dt-toolbar {
238
+ display: flex;
239
+ align-items: center;
240
+ justify-content: space-between;
241
+ padding: 0.75rem 1rem;
242
+ border-bottom: 1px solid var(--n-color-border);
243
+ }
244
+
245
+ .n-dt-global {
246
+ display: flex;
247
+ align-items: center;
248
+ gap: 0.5rem;
249
+ color: var(--n-color-text-muted);
250
+ }
251
+
252
+ .n-dt-global-input {
253
+ width: 260px;
254
+ max-width: 100%;
255
+ background: transparent;
256
+ border: 1px solid var(--n-color-border);
257
+ border-radius: var(--n-radius-md);
258
+ padding: 0.45rem 0.75rem;
259
+ color: var(--n-color-text);
260
+ font-size: var(--n-text-sm);
261
+ outline: none;
262
+ box-sizing: border-box;
263
+ transition: border-color 0.15s ease;
264
+ }
265
+
266
+ .n-dt-global-input:focus {
267
+ border-color: var(--n-color-primary);
268
+ }
269
+
270
+ .n-dt-wrapper {
271
+ width: 100%;
272
+ overflow: auto;
273
+ }
274
+
275
+ .n-dt-table {
276
+ width: 100%;
277
+ border-collapse: collapse;
278
+ table-layout: fixed;
279
+ }
280
+
281
+ .n-dt-thead {
282
+ background: var(--n-color-surface);
283
+ }
284
+
285
+ .n-dt-th {
286
+ position: relative;
287
+ padding: 0.65rem 0.85rem;
288
+ border-bottom: 2px solid var(--n-color-border);
289
+ color: var(--n-color-text-secondary);
290
+ font-size: var(--n-text-sm);
291
+ font-weight: var(--n-weight-semibold);
292
+ user-select: none;
293
+ vertical-align: middle;
294
+ text-align: left;
295
+ background: var(--n-color-surface);
296
+ transition: background 0.15s ease;
297
+ }
298
+
299
+ .n-dt-th.is-sortable {
300
+ cursor: pointer;
301
+ }
302
+
303
+ .n-dt-th.is-sortable:hover {
304
+ background: var(--n-color-glass);
305
+ }
306
+
307
+ .n-dt-th.is-right {
308
+ text-align: right;
309
+ }
310
+
311
+ .n-dt-th.is-center {
312
+ text-align: center;
313
+ }
314
+
315
+ .n-dt-th.is-selection {
316
+ width: 3rem;
317
+ text-align: center;
318
+ }
319
+
320
+ .n-dt-th-content {
321
+ display: flex;
322
+ align-items: center;
323
+ gap: 0.35rem;
324
+ }
325
+
326
+ .n-dt-th.is-right .n-dt-th-content {
327
+ justify-content: flex-end;
328
+ }
329
+
330
+ .n-dt-th.is-center .n-dt-th-content {
331
+ justify-content: center;
332
+ }
333
+
334
+ .n-dt-th-text {
335
+ overflow: hidden;
336
+ text-overflow: ellipsis;
337
+ white-space: nowrap;
338
+ }
339
+
340
+ .n-dt-sort {
341
+ display: inline-flex;
342
+ flex-direction: column;
343
+ align-items: center;
344
+ gap: 0;
345
+ line-height: 1;
346
+ margin-left: auto;
347
+ flex-shrink: 0;
348
+ }
349
+
350
+ .n-dt-sort-arrow {
351
+ display: block;
352
+ color: var(--n-color-border);
353
+ transition: color 0.15s ease;
354
+ margin: -3px 0;
355
+ }
356
+
357
+ .n-dt-sort-arrow-up.is-on {
358
+ color: var(--n-color-primary);
359
+ }
360
+
361
+ .n-dt-sort-arrow-down.is-on {
362
+ color: var(--n-color-primary);
363
+ }
364
+
365
+ .n-dt-th.is-sortable:hover .n-dt-sort-arrow {
366
+ color: var(--n-color-text-muted);
367
+ }
368
+
369
+ .n-dt-resizer {
370
+ position: absolute;
371
+ right: 0;
372
+ top: 0;
373
+ bottom: 0;
374
+ width: 6px;
375
+ cursor: col-resize;
376
+ background: transparent;
377
+ transition: background 0.15s ease;
378
+ }
379
+
380
+ .n-dt-resizer:hover {
381
+ background: var(--n-color-primary);
382
+ opacity: 0.4;
383
+ }
384
+
385
+ .n-dt-filter-row .n-dt-th {
386
+ border-bottom: 1px solid var(--n-color-border);
387
+ padding: 0.5rem 0.6rem;
388
+ background: var(--n-color-surface);
389
+ }
390
+
391
+ .n-dt-filter {
392
+ width: 100%;
393
+ max-width: 100%;
394
+ display: block;
395
+ background: var(--n-color-bg);
396
+ border: 1px solid var(--n-color-border);
397
+ border-radius: var(--n-radius-sm);
398
+ padding: 0.3rem 0.45rem;
399
+ color: var(--n-color-text);
400
+ font-size: var(--n-text-xs);
401
+ outline: none;
402
+ box-sizing: border-box;
403
+ transition: border-color 0.15s ease;
404
+ }
405
+
406
+ .n-dt-filter:focus {
407
+ border-color: var(--n-color-primary);
408
+ }
409
+
410
+ .n-dt-tbody .n-dt-row {
411
+ transition: background 0.15s ease;
412
+ }
413
+
414
+ .n-dt-tbody .n-dt-row.is-hover:hover {
415
+ background: var(--n-color-glass);
416
+ }
417
+
418
+ .n-dt-tbody .n-dt-row.is-selected {
419
+ background: var(--n-color-primary-light);
420
+ outline: none;
421
+ }
422
+
423
+ .n-dt-tbody .n-dt-row.is-hover.is-selected:hover {
424
+ background: var(--n-color-primary-light);
425
+ filter: brightness(0.96);
426
+ }
427
+
428
+ .n-dt-table.is-striped .n-dt-tbody .n-dt-row:nth-child(even) {
429
+ background: var(--n-color-glass);
430
+ }
431
+
432
+ .n-dt-table.is-striped .n-dt-tbody .n-dt-row:nth-child(even).is-selected {
433
+ background: var(--n-color-primary-light);
434
+ }
435
+
436
+ .n-dt-td {
437
+ padding: 0.6rem 0.85rem;
438
+ border-bottom: 1px solid var(--n-color-border);
439
+ color: var(--n-color-text);
440
+ font-size: var(--n-text-sm);
441
+ vertical-align: middle;
442
+ text-align: left;
443
+ }
444
+
445
+ .n-dt-td.is-right {
446
+ text-align: right;
447
+ }
448
+
449
+ .n-dt-td.is-center {
450
+ text-align: center;
451
+ }
452
+
453
+ .n-dt-td.is-selection {
454
+ width: 3rem;
455
+ text-align: center;
456
+ }
457
+
458
+ .n-dt-selectbox {
459
+ width: 20px;
460
+ height: 20px;
461
+ border: 2px solid var(--n-color-border);
462
+ border-radius: var(--n-radius-sm);
463
+ background: transparent;
464
+ cursor: pointer;
465
+ display: inline-flex;
466
+ align-items: center;
467
+ justify-content: center;
468
+ padding: 0;
469
+ transition: all 0.15s ease;
470
+ color: transparent;
471
+ }
472
+
473
+ .n-dt-selectbox.is-checked {
474
+ background: var(--n-color-primary);
475
+ border-color: var(--n-color-primary);
476
+ color: white;
477
+ }
478
+
479
+ .n-dt-selectbox:hover:not(.is-checked) {
480
+ border-color: var(--n-color-primary);
481
+ }
482
+
483
+ .n-dt-selectbox svg {
484
+ display: block;
485
+ opacity: 0;
486
+ transition: opacity 0.1s ease;
487
+ }
488
+
489
+ .n-dt-selectbox.is-checked svg {
490
+ opacity: 1;
491
+ }
492
+
493
+ .n-dt-empty-row .n-dt-empty {
494
+ padding: 2rem 1rem;
495
+ text-align: center;
496
+ color: var(--n-color-text-muted);
497
+ font-size: var(--n-text-sm);
498
+ }
499
+
500
+ .n-dt-sm .n-dt-th,
501
+ .n-dt-sm .n-dt-td {
502
+ padding: 0.4rem 0.6rem;
503
+ font-size: var(--n-text-xs);
504
+ }
505
+
506
+ .n-dt-lg .n-dt-th,
507
+ .n-dt-lg .n-dt-td {
508
+ padding: 0.85rem 1rem;
509
+ font-size: var(--n-text-base);
510
+ }
511
+ </style>
@@ -196,15 +196,15 @@ onBeforeUnmount(() => {
196
196
  :disabled="disabled"
197
197
  readonly
198
198
  />
199
- <span class="n-datepicker-icon">📅</span>
199
+ <span class="n-datepicker-icon"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg></span>
200
200
  </div>
201
201
 
202
202
  <Teleport to="body">
203
203
  <div v-if="isOpen.value" class="n-datepicker-dropdown" :class="{ 'is-top': resolvedPlacement.value === 'top' }" :data-datepicker-popup="instanceId" :style="popupStyle.value">
204
204
  <div class="n-datepicker-header">
205
- <button type="button" class="n-datepicker-nav" @click="prevMonth">‹</button>
205
+ <button type="button" class="n-datepicker-nav" @click="prevMonth" aria-label="Previous month"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
206
206
  <span class="n-datepicker-title">{{ monthNames[month.value] }} {{ year.value }}</span>
207
- <button type="button" class="n-datepicker-nav" @click="nextMonth">›</button>
207
+ <button type="button" class="n-datepicker-nav" @click="nextMonth" aria-label="Next month"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg></button>
208
208
  </div>
209
209
 
210
210
  <div class="n-datepicker-grid">
@@ -141,9 +141,10 @@ const togglePassword = () => {
141
141
  />
142
142
  <div class="n-input-focus-ring"></div>
143
143
  <div class="n-input-actions">
144
- <button v-if="clearable && draft.value" class="n-input-action" @click="clear" tabindex="-1" type="button">✕</button>
145
- <button v-if="type === 'password'" class="n-input-action" @click="togglePassword" tabindex="-1" type="button">
146
- {{ showPassword.value ? '◉' : '◎' }}
144
+ <button v-if="clearable && draft.value" class="n-input-action" @click="clear" tabindex="-1" type="button" aria-label="Clear"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M18 6L6 18"/><path d="M6 6l12 12"/></svg></button>
145
+ <button v-if="type === 'password'" class="n-input-action" @click="togglePassword" tabindex="-1" type="button" :aria-label="showPassword.value ? 'Hide password' : 'Show password'">
146
+ <svg v-if="!showPassword.value" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
147
+ <svg v-else viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/><path d="M23 1L1 23"/></svg>
147
148
  </button>
148
149
  <span v-if="suffixIcon" class="n-input-icon is-suffix">{{ suffixIcon }}</span>
149
150
  </div>
@@ -220,9 +220,9 @@ const onFocus = () => {
220
220
  <div class="n-inum">
221
221
  <label v-if="label" class="n-inum-label">{{ label }}</label>
222
222
  <div class="n-inum-wrap" :class="{ 'is-disabled': effectiveDisabled.value }">
223
- <button type="button" class="n-inum-btn n-inum-dec" :disabled="effectiveDisabled.value || readonly" aria-label="Decrement" @click="dec">−</button>
223
+ <button type="button" class="n-inum-btn n-inum-dec" :disabled="effectiveDisabled.value || readonly" aria-label="Decrement" @click="dec"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><line x1="5" y1="12" x2="19" y2="12"/></svg></button>
224
224
  <input class="n-inum-input" type="text" :value="displayText.value" :placeholder="placeholder" :disabled="effectiveDisabled.value" :readonly="readonly" inputmode="decimal" autocomplete="off" @beforeinput="onBeforeInput" @keydown="onKeydown" @paste="onPaste" @input="onInput" @focus="onFocus" @blur="onBlur" />
225
- <button type="button" class="n-inum-btn n-inum-inc" :disabled="effectiveDisabled.value || readonly" aria-label="Increment" @click="inc">+</button>
225
+ <button type="button" class="n-inum-btn n-inum-inc" :disabled="effectiveDisabled.value || readonly" aria-label="Increment" @click="inc"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button>
226
226
  </div>
227
227
  </div>
228
228
  </template>
@@ -100,11 +100,11 @@ onUnmounted(() => {
100
100
  @keydown="handleKeydown"
101
101
  >
102
102
  <div v-if="title || $slots.header" class="n-modal-header" :class="{ 'close-left': closeLeft }">
103
- <button v-if="closable && closeLeft" class="n-modal-close" @click="close" aria-label="Close">&times;</button>
103
+ <button v-if="closable && closeLeft" class="n-modal-close" @click="close" aria-label="Close"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M18 6L6 18"/><path d="M6 6l12 12"/></svg></button>
104
104
  <slot name="header">
105
105
  <h3>{{ title }}</h3>
106
106
  </slot>
107
- <button v-if="closable && !closeLeft" class="n-modal-close" @click="close" aria-label="Close">&times;</button>
107
+ <button v-if="closable && !closeLeft" class="n-modal-close" @click="close" aria-label="Close"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M18 6L6 18"/><path d="M6 6l12 12"/></svg></button>
108
108
  </div>
109
109
  <div class="n-modal-content">
110
110
  <slot />
@@ -155,7 +155,7 @@ onUnmounted(() => {
155
155
  box-shadow: var(--n-shadow-xl);
156
156
  transform: scale(0.9) translateY(20px);
157
157
  opacity: 0;
158
- transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
158
+ transition: all var(--n-transition-spring);
159
159
  overflow: hidden;
160
160
  outline: none;
161
161
  max-height: 85vh;
@@ -216,7 +216,7 @@ onUnmounted(() => {
216
216
 
217
217
  .n-modal-footer {
218
218
  padding: var(--n-space-5) var(--n-space-8);
219
- background: rgba(0, 0, 0, 0.15);
219
+ background: var(--n-color-glass);
220
220
  border-top: 1px solid var(--n-color-border);
221
221
  display: flex;
222
222
  justify-content: flex-end;
@@ -65,10 +65,10 @@ onBeforeUnmount(() => close())
65
65
  <div class="n-ms-trigger" role="combobox" tabindex="0" :aria-expanded="isOpen.value" @click="toggleOpen" @keydown="onKeydown">
66
66
  <div v-if="selectedLabels.value.length > 0" class="n-ms-chips"><div v-for="c in selectedLabels.value.slice(0, maxChips)" :key="String(c.value)" class="n-ms-chip"><span class="n-ms-chip-label">{{ c.label }}</span><button type="button" class="n-ms-chip-remove" aria-label="Remove" tabindex="-1" :disabled="effectiveDisabled.value" @click="removeChip(c.value, $event)">✕</button></div><span v-if="selectedLabels.value.length > maxChips" class="n-ms-more">+{{ selectedLabels.value.length - maxChips }}</span></div>
67
67
  <span v-else class="n-ms-placeholder">{{ placeholder }}</span>
68
- <div class="n-ms-actions"><button v-if="clearable && selectedLabels.value.length > 0" type="button" class="n-ms-clear" tabindex="-1" @click.stop="clear">✕</button><span class="n-ms-arrow">▾</span></div>
68
+ <div class="n-ms-actions"><button v-if="clearable && selectedLabels.value.length > 0" type="button" class="n-ms-clear" tabindex="-1" @click.stop="clear" aria-label="Clear"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M18 6L6 18"/><path d="M6 6l12 12"/></svg></button><span class="n-ms-arrow"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg></span></div>
69
69
  </div>
70
- <div v-if="isOpen.value && appendTo !== 'body'" class="n-ms-dropdown"><div v-if="searchable" class="n-ms-search"><input class="n-ms-search-input" :value="queryDraft.value" placeholder="Search..." @input="onSearchInput" @keydown.stop="onKeydown" /></div><div class="n-ms-options"><button v-for="(opt, i) in filteredOptions.value" :key="String(opt.value) + ':' + i" type="button" class="n-ms-option" :data-ms-opt="String(opt.value)" :class="{ 'is-selected': selectedSet.value.has(opt.value), 'is-focused': i === focusedIndex.value, 'is-disabled': opt.disabled }" :disabled="opt.disabled" @mouseenter="focusedIndex.value = i" @click="toggleValue(opt.value)"><span class="n-ms-check">{{ selectedSet.value.has(opt.value) ? '✓' : '' }}</span><span class="n-ms-option-label">{{ opt.label }}</span></button><div v-if="filteredOptions.value.length === 0" class="n-ms-empty">No options</div></div></div>
71
- <Teleport to="body"><div v-if="isOpen.value && appendTo === 'body'" class="n-ms-dropdown" :class="{ 'is-top': resolvedPlacement.value === 'top' }" :data-ms-popup="instanceId" :style="popupStyle.value"><div v-if="searchable" class="n-ms-search"><input class="n-ms-search-input" :value="queryDraft.value" placeholder="Search..." @input="onSearchInput" @keydown.stop="onKeydown" /></div><div class="n-ms-options"><button v-for="(opt, i) in filteredOptions.value" :key="String(opt.value) + ':' + i" type="button" class="n-ms-option" :data-ms-opt="String(opt.value)" :class="{ 'is-selected': selectedSet.value.has(opt.value), 'is-focused': i === focusedIndex.value, 'is-disabled': opt.disabled }" :disabled="opt.disabled" @mouseenter="focusedIndex.value = i" @click="toggleValue(opt.value)"><span class="n-ms-check">{{ selectedSet.value.has(opt.value) ? '✓' : '' }}</span><span class="n-ms-option-label">{{ opt.label }}</span></button><div v-if="filteredOptions.value.length === 0" class="n-ms-empty">No options</div></div></div></Teleport>
70
+ <div v-if="isOpen.value && appendTo !== 'body'" class="n-ms-dropdown"><div v-if="searchable" class="n-ms-search"><input class="n-ms-search-input" :value="queryDraft.value" placeholder="Search..." @input="onSearchInput" @keydown.stop="onKeydown" /></div><div class="n-ms-options"><button v-for="(opt, i) in filteredOptions.value" :key="String(opt.value) + ':' + i" type="button" class="n-ms-option" :data-ms-opt="String(opt.value)" :class="{ 'is-selected': selectedSet.value.has(opt.value), 'is-focused': i === focusedIndex.value, 'is-disabled': opt.disabled }" :disabled="opt.disabled" @mouseenter="focusedIndex.value = i" @click="toggleValue(opt.value)"><span class="n-ms-check"><svg v-if="selectedSet.value.has(opt.value)" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg></span><span class="n-ms-option-label">{{ opt.label }}</span></button><div v-if="filteredOptions.value.length === 0" class="n-ms-empty">No options</div></div></div>
71
+ <Teleport to="body"><div v-if="isOpen.value && appendTo === 'body'" class="n-ms-dropdown" :class="{ 'is-top': resolvedPlacement.value === 'top' }" :data-ms-popup="instanceId" :style="popupStyle.value"><div v-if="searchable" class="n-ms-search"><input class="n-ms-search-input" :value="queryDraft.value" placeholder="Search..." @input="onSearchInput" @keydown.stop="onKeydown" /></div><div class="n-ms-options"><button v-for="(opt, i) in filteredOptions.value" :key="String(opt.value) + ':' + i" type="button" class="n-ms-option" :data-ms-opt="String(opt.value)" :class="{ 'is-selected': selectedSet.value.has(opt.value), 'is-focused': i === focusedIndex.value, 'is-disabled': opt.disabled }" :disabled="opt.disabled" @mouseenter="focusedIndex.value = i" @click="toggleValue(opt.value)"><span class="n-ms-check"><svg v-if="selectedSet.value.has(opt.value)" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg></span><span class="n-ms-option-label">{{ opt.label }}</span></button><div v-if="filteredOptions.value.length === 0" class="n-ms-empty">No options</div></div></div></Teleport>
72
72
  </div>
73
73
  </template>
74
74
 
@@ -58,11 +58,11 @@ const end = computed(() => Math.min((safeFirst.value || 0) + (safeRows.value ||
58
58
  <span class="n-paginator-report">{{ start.value }}-{{ end.value }} of {{ totalRecords }}</span>
59
59
  </div>
60
60
  <div class="n-paginator-center">
61
- <button type="button" class="n-pg-btn" :disabled="!canPrev.value" @click="firstPage">«</button>
62
- <button type="button" class="n-pg-btn" :disabled="!canPrev.value" @click="prev">‹</button>
61
+ <button type="button" class="n-pg-btn" :disabled="!canPrev.value" @click="firstPage" aria-label="First page"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M11 17l-5-5 5-5M18 17l-5-5 5-5"/></svg></button>
62
+ <button type="button" class="n-pg-btn" :disabled="!canPrev.value" @click="prev" aria-label="Previous page"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
63
63
  <span class="n-pg-page">{{ page.value + 1 }} / {{ pageCount.value }}</span>
64
- <button type="button" class="n-pg-btn" :disabled="!canNext.value" @click="next">›</button>
65
- <button type="button" class="n-pg-btn" :disabled="!canNext.value" @click="lastPage">»</button>
64
+ <button type="button" class="n-pg-btn" :disabled="!canNext.value" @click="next" aria-label="Next page"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg></button>
65
+ <button type="button" class="n-pg-btn" :disabled="!canNext.value" @click="lastPage" aria-label="Last page"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M13 17l5-5-5-5M6 17l5-5-5-5"/></svg></button>
66
66
  </div>
67
67
  <div class="n-paginator-right">
68
68
  <select class="n-pg-select" :value="rows" @change="changeRows">