nexa-ui-kit 0.10.0 → 0.11.1

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 +74 -35
  50. package/dist/components/NTreeMenu.nexa +52 -15
  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 +52 -15
  73. package/src/styles/tokens.css +18 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexa-ui-kit",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Premium component library for Nexa Framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,12 +22,12 @@
22
22
  "src"
23
23
  ],
24
24
  "dependencies": {
25
- "nexa-framework": "0.10.0",
26
- "nexa-mobile": "0.10.0"
25
+ "nexa-framework": "0.11.1",
26
+ "nexa-mobile": "0.11.1"
27
27
  },
28
28
  "devDependencies": {
29
29
  "cpx": "^1.5.0",
30
- "nexa-compiler": "0.10.0"
30
+ "nexa-compiler": "0.11.1"
31
31
  },
32
32
  "scripts": {
33
33
  "build": "tsc && node scripts/compile-nexa.js && node scripts/patch-imports.js && cpx \"src/**/*.nexa\" dist && cpx \"src/styles/*.css\" dist/styles",
@@ -22,12 +22,18 @@ const icons = { success: '✓', error: '✕', warning: '⚡', info: 'ℹ' }
22
22
 
23
23
  <template>
24
24
  <div v-if="visible.value" class="n-alert" :class="`is-${variant}`">
25
- <span class="n-alert-icon">{{ icon || icons[variant] || 'ℹ' }}</span>
25
+ <span class="n-alert-icon">
26
+ <template v-if="icon">{{ icon }}</template>
27
+ <svg v-else-if="variant === 'success'" viewBox="0 0 24 24" width="16" height="16" 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>
28
+ <svg v-else-if="variant === 'error'" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
29
+ <svg v-else-if="variant === 'warning'" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
30
+ <svg v-else viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
31
+ </span>
26
32
  <div class="n-alert-body">
27
33
  <span v-if="title" class="n-alert-title">{{ title }}</span>
28
34
  <span class="n-alert-text"><slot /></span>
29
35
  </div>
30
- <button v-if="closable" class="n-alert-close" @click="dismiss">&times;</button>
36
+ <button v-if="closable" class="n-alert-close" @click="dismiss" aria-label="Close"><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="M18 6L6 18"/><path d="M6 6l12 12"/></svg></button>
31
37
  </div>
32
38
  </template>
33
39
 
@@ -51,25 +57,25 @@ const icons = { success: '✓', error: '✕', warning: '⚡', info: 'ℹ' }
51
57
 
52
58
  .is-info {
53
59
  background: var(--n-color-primary-light);
54
- border-color: rgba(59, 130, 246, 0.2);
60
+ border-color: var(--n-color-primary-border);
55
61
  color: var(--n-color-primary);
56
62
  }
57
63
 
58
64
  .is-success {
59
65
  background: var(--n-color-success-light);
60
- border-color: rgba(16, 185, 129, 0.2);
66
+ border-color: var(--n-color-success-border);
61
67
  color: var(--n-color-success);
62
68
  }
63
69
 
64
70
  .is-warning {
65
71
  background: var(--n-color-warning-light);
66
- border-color: rgba(245, 158, 11, 0.2);
72
+ border-color: var(--n-color-warning-border);
67
73
  color: var(--n-color-warning);
68
74
  }
69
75
 
70
76
  .is-error {
71
77
  background: var(--n-color-danger-light);
72
- border-color: rgba(239, 68, 68, 0.2);
78
+ border-color: var(--n-color-danger-border);
73
79
  color: var(--n-color-danger);
74
80
  }
75
81
 
@@ -37,8 +37,8 @@ onBeforeUnmount(() => { clearTimeout(completeTimer); close() })
37
37
  <div class="n-ac-input-wrap" :class="{ 'is-disabled': disabled }">
38
38
  <input class="n-ac-input" :id="inputId" :value="modelValue" :placeholder="placeholder" :disabled="disabled" :readonly="readonly" role="combobox" aria-autocomplete="list" :aria-expanded="isOpen.value" :aria-controls="listboxId" :aria-activedescendant="activeId.value || undefined" @input="onInput" @focus="onFocus" @keydown="onKeydown" />
39
39
  <div class="n-ac-actions">
40
- <button v-if="clearable && modelValue" type="button" class="n-ac-action" :disabled="disabled || readonly" aria-label="Limpiar" @click="clearValue">✕</button>
41
- <button v-if="dropdown" type="button" class="n-ac-action" :disabled="disabled || readonly" aria-label="Abrir" @click="openAll">▾</button>
40
+ <button v-if="clearable && modelValue" type="button" class="n-ac-action" :disabled="disabled || readonly" aria-label="Clear" @click="clearValue"><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>
41
+ <button v-if="dropdown" type="button" class="n-ac-action" :disabled="disabled || readonly" aria-label="Open" @click="openAll"><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="M6 9l6 6 6-6"/></svg></button>
42
42
  </div>
43
43
  </div>
44
44
  <Teleport to="body">
@@ -54,5 +54,5 @@ onBeforeUnmount(() => { clearTimeout(completeTimer); close() })
54
54
  </template>
55
55
 
56
56
  <style scoped>
57
- .n-ac{display:flex;flex-direction:column;gap:var(--n-space-2);width:100%;font-family:var(--n-font-sans)}.n-ac-label{display:block;font-size:var(--n-text-sm);font-weight:var(--n-weight-medium);color:var(--n-color-text-secondary);margin-bottom:var(--n-space-2)}.n-ac-input-wrap{position:relative;display:flex;align-items:center;background:var(--n-color-surface);border:1px solid var(--n-color-border);border-radius:var(--n-radius-md);transition:all var(--n-transition-fast)}.n-ac-input-wrap:focus-within{border-color:var(--n-color-primary);box-shadow:0 0 0 3px var(--n-color-primary-light)}.n-ac-input{width:100%;background:transparent;border:none;outline:none;padding:0.75rem 2.75rem 0.75rem 1rem;color:var(--n-color-text);font-size:var(--n-text-base);font-family:inherit}.n-ac-input::placeholder{color:var(--n-color-text-muted)}.n-ac-actions{position:absolute;right:0.5rem;display:flex;align-items:center;gap:0.15rem}.n-ac-action{background:transparent;border:none;color:var(--n-color-text-muted);cursor:pointer;padding:0.25rem;border-radius:var(--n-radius-sm);transition:all var(--n-transition-fast);line-height:1;display:flex;align-items:center}.n-ac-action:hover:not(:disabled){color:var(--n-color-text);background:var(--n-color-glass)}.n-ac-action:disabled{opacity:0.5;cursor:not-allowed}.n-ac-input-wrap.is-disabled{opacity:0.5;cursor:not-allowed;background:var(--n-color-surface-alt)}.n-ac-popup{background:var(--n-color-surface);border:1px solid var(--n-color-border);border-radius:var(--n-radius-md);box-shadow:var(--n-shadow-lg);overflow:hidden;animation:n-ac-in .2s cubic-bezier(0,1,0,1)}.n-ac-popup.is-top{animation:n-ac-in-top .2s cubic-bezier(0,1,0,1)}@keyframes n-ac-in{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes n-ac-in-top{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.n-ac-loading{padding:var(--n-space-3) var(--n-space-4);color:var(--n-color-text-muted);font-size:var(--n-text-sm)}.n-ac-list{max-height:260px;overflow:auto;display:flex;flex-direction:column}.n-ac-option{text-align:left;padding:0.7rem 1rem;color:var(--n-color-text-secondary);background:transparent;border:none;cursor:pointer;transition:all var(--n-transition-fast)}.n-ac-option:hover,.n-ac-option.is-focused{background:var(--n-color-glass);color:var(--n-color-text)}.n-ac-empty{padding:var(--n-space-4);color:var(--n-color-text-muted);text-align:center;font-size:var(--n-text-sm)}
57
+ .n-ac{display:flex;flex-direction:column;gap:var(--n-space-2);width:100%;font-family:var(--n-font-sans)}.n-ac-label{display:block;font-size:var(--n-text-sm);font-weight:var(--n-weight-medium);color:var(--n-color-text-secondary);margin-bottom:var(--n-space-2)}.n-ac-input-wrap{position:relative;display:flex;align-items:center;background:var(--n-color-surface);border:1px solid var(--n-color-border);border-radius:var(--n-radius-md);transition:all var(--n-transition-fast)}.n-ac-input-wrap:focus-within{border-color:var(--n-color-primary);box-shadow:0 0 0 3px var(--n-color-primary-light)}.n-ac-input{width:100%;background:transparent;border:none;outline:none;padding:0.75rem 2.75rem 0.75rem 1rem;color:var(--n-color-text);font-size:var(--n-text-base);font-family:inherit}.n-ac-input::placeholder{color:var(--n-color-text-muted)}.n-ac-actions{position:absolute;right:0.5rem;display:flex;align-items:center;gap:0.15rem}.n-ac-action{background:transparent;border:none;color:var(--n-color-text-muted);cursor:pointer;padding:0.25rem;border-radius:var(--n-radius-sm);transition:all var(--n-transition-fast);line-height:1;display:flex;align-items:center}.n-ac-action:hover:not(:disabled){color:var(--n-color-text);background:var(--n-color-glass)}.n-ac-action:disabled{opacity:0.5;cursor:not-allowed}.n-ac-input-wrap.is-disabled{opacity:0.5;cursor:not-allowed;background:var(--n-color-surface-alt)}.n-ac-popup{background:var(--n-color-surface);border:1px solid var(--n-color-border);border-radius:var(--n-radius-md);box-shadow:var(--n-shadow-lg);overflow:hidden;animation:n-ac-in .2s ease-out}.n-ac-popup.is-top{animation:n-ac-in-top .2s ease-out}@keyframes n-ac-in{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes n-ac-in-top{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.n-ac-loading{padding:var(--n-space-3) var(--n-space-4);color:var(--n-color-text-muted);font-size:var(--n-text-sm)}.n-ac-list{max-height:260px;overflow:auto;display:flex;flex-direction:column}.n-ac-option{text-align:left;padding:0.7rem 1rem;color:var(--n-color-text-secondary);background:transparent;border:none;cursor:pointer;transition:all var(--n-transition-fast)}.n-ac-option:hover,.n-ac-option.is-focused{background:var(--n-color-glass);color:var(--n-color-text)}.n-ac-empty{padding:var(--n-space-4);color:var(--n-color-text-muted);text-align:center;font-size:var(--n-text-sm)}
58
58
  </style>
@@ -74,7 +74,7 @@ const overlayOpacity = computed(() => {
74
74
  class="n-bottom-sheet-container"
75
75
  :style="{
76
76
  transform: `translateY(${translateY.value})`,
77
- transition: isDragging.value ? 'none' : 'transform 0.4s cubic-bezier(0.4, 0, 0.2, 1)'
77
+ transition: isDragging.value ? 'none' : 'transform var(--n-transition-slow)'
78
78
  }"
79
79
  >
80
80
  <div class="n-bottom-sheet-handle"></div>
@@ -121,7 +121,7 @@ const overlayOpacity = computed(() => {
121
121
  background: var(--n-color-surface);
122
122
  border-top: 1px solid var(--n-color-border);
123
123
  border-radius: var(--n-radius-2xl) var(--n-radius-2xl) 0 0;
124
- box-shadow: 0 -10px 25px -5px rgba(0, 0, 0, 0.3);
124
+ box-shadow: var(--n-shadow-top-lg);
125
125
  padding-bottom: env(safe-area-inset-bottom, 20px);
126
126
  touch-action: none;
127
127
  display: flex;
@@ -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>