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,133 @@
1
+ <script setup>
2
+ import { signal, computed, provide } from 'nexa-framework'
3
+
4
+ const props = defineProps({
5
+ validateOn: { type: String, default: 'submit' },
6
+ disabled: { type: Boolean, default: false }
7
+ })
8
+
9
+ const emit = defineEmits(['submit', 'invalid'])
10
+
11
+ const fieldNames = signal([])
12
+ const fields = Object.create(null)
13
+
14
+ const registerField = (name, field) => {
15
+ if (!name) return () => {}
16
+ fields[name] = field
17
+ if (!fieldNames.value.includes(name)) fieldNames.value = [...fieldNames.value, name]
18
+ return () => {
19
+ delete fields[name]
20
+ if (fieldNames.value.includes(name)) {
21
+ fieldNames.value = fieldNames.value.filter(n => n !== name)
22
+ }
23
+ }
24
+ }
25
+
26
+ const getValues = () => {
27
+ const out = {}
28
+ for (const name of fieldNames.value) {
29
+ const field = fields[name]
30
+ if (!field) continue
31
+ out[name] = field.value.value
32
+ }
33
+ return out
34
+ }
35
+
36
+ const values = computed(() => {
37
+ const names = fieldNames.value
38
+ const out = {}
39
+ for (const name of names) {
40
+ const field = fields[name]
41
+ if (!field) continue
42
+ out[name] = field.value.value
43
+ }
44
+ return out
45
+ })
46
+
47
+ const errors = computed(() => {
48
+ const names = fieldNames.value
49
+ const out = {}
50
+ for (const name of names) {
51
+ const field = fields[name]
52
+ if (!field) continue
53
+ out[name] = field.errors.value
54
+ }
55
+ return out
56
+ })
57
+
58
+ const pending = computed(() => {
59
+ const names = fieldNames.value
60
+ for (const name of names) {
61
+ const field = fields[name]
62
+ if (!field) continue
63
+ if (field.pending.value) return true
64
+ }
65
+ return false
66
+ })
67
+
68
+ const valid = computed(() => {
69
+ const names = fieldNames.value
70
+ for (const name of names) {
71
+ const field = fields[name]
72
+ if (!field) continue
73
+ if (field.errors.value.length > 0) return false
74
+ }
75
+ return true
76
+ })
77
+
78
+ const validateAll = async () => {
79
+ const tasks = fieldNames.value.map(async (name) => {
80
+ const field = fields[name]
81
+ if (!field) return true
82
+ const errs = await field.validate('submit')
83
+ return errs.length === 0
84
+ })
85
+ const results = await Promise.all(tasks)
86
+ return results.every(Boolean)
87
+ }
88
+
89
+ const reset = () => {
90
+ for (const name of fieldNames.value) {
91
+ const field = fields[name]
92
+ if (!field) continue
93
+ field.reset()
94
+ }
95
+ }
96
+
97
+ const submit = async () => {
98
+ if (props.disabled) return
99
+ const ok = await validateAll()
100
+ if (!ok) {
101
+ emit('invalid', errors.value)
102
+ return
103
+ }
104
+ emit('submit', values.value)
105
+ }
106
+
107
+ const onSubmit = (e) => {
108
+ if (e && typeof e.preventDefault === 'function') e.preventDefault()
109
+ submit()
110
+ }
111
+
112
+ provide('nexa-ui:form', {
113
+ validateOn: props.validateOn,
114
+ disabled: props.disabled,
115
+ registerField,
116
+ getValues,
117
+ })
118
+ </script>
119
+
120
+ <template>
121
+ <form class="n-form" @submit="onSubmit">
122
+ <slot :values="values" :errors="errors" :valid="valid" :pending="pending" :submit="submit" :reset="reset" />
123
+ </form>
124
+ </template>
125
+
126
+ <style scoped>
127
+ .n-form {
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: var(--n-space-4);
131
+ width: 100%;
132
+ }
133
+ </style>
@@ -0,0 +1,171 @@
1
+ <script setup>
2
+ import { signal, computed, inject, provide, effect, onUnmounted } from 'nexa-framework'
3
+
4
+ let nextAutoId = 1
5
+
6
+ const props = defineProps({
7
+ name: { type: String, default: '' },
8
+ label: { type: String, default: '' },
9
+ help: { type: String, default: '' },
10
+ id: { type: String, default: '' },
11
+ modelValue: { type: null, default: '' },
12
+ rules: { type: Array, default: () => [] },
13
+ validateOn: { type: String, default: '' },
14
+ showError: { type: Boolean, default: true },
15
+ disabled: { type: Boolean, default: false }
16
+ })
17
+
18
+ const emit = defineEmits(['update:modelValue'])
19
+
20
+ const form = inject('nexa-ui:form', undefined)
21
+ const autoId = `n-field-${nextAutoId++}`
22
+
23
+ const inputId = computed(() => props.id || autoId)
24
+ const helpId = computed(() => `${inputId.value}-help`)
25
+ const errorId = computed(() => `${inputId.value}-error`)
26
+ const describedBy = computed(() => {
27
+ const parts = []
28
+ if (props.help) parts.push(helpId.value)
29
+ if (props.showError && error.value) parts.push(errorId.value)
30
+ return parts.join(' ') || undefined
31
+ })
32
+
33
+ const value = signal(props.modelValue)
34
+ const initialValue = signal(props.modelValue)
35
+ const touched = signal(false)
36
+ const pending = signal(false)
37
+ const errors = signal([])
38
+
39
+ const dirty = computed(() => value.value !== initialValue.value)
40
+ const error = computed(() => errors.value[0] || '')
41
+ const invalid = computed(() => errors.value.length > 0)
42
+
43
+ effect(() => {
44
+ if (props.modelValue === value.value) return
45
+ value.value = props.modelValue
46
+ })
47
+
48
+ const shouldValidateOn = (reason) => {
49
+ const mode = props.validateOn || form?.validateOn || 'submit'
50
+ if (mode === 'change' && (reason === 'change' || reason === 'input')) return true
51
+ if (mode === 'blur' && reason === 'blur') return true
52
+ if (mode === 'submit' && reason === 'submit') return true
53
+ return false
54
+ }
55
+
56
+ const validate = async (reason = 'submit') => {
57
+ if (!shouldValidateOn(reason) && reason !== 'submit') return errors.value
58
+ if (form?.disabled || props.disabled) return []
59
+ const rules = Array.isArray(props.rules) ? props.rules : []
60
+ if (rules.length === 0) {
61
+ errors.value = []
62
+ return []
63
+ }
64
+
65
+ pending.value = true
66
+ const nextErrors = []
67
+ const values = form?.getValues ? form.getValues() : { [props.name]: value.value }
68
+
69
+ for (const rule of rules) {
70
+ if (!rule) continue
71
+ const res = await rule(value.value, values)
72
+ if (!res) continue
73
+ nextErrors.push(String(res))
74
+ }
75
+
76
+ errors.value = nextErrors
77
+ pending.value = false
78
+ return nextErrors
79
+ }
80
+
81
+ const setValue = (next) => {
82
+ if (form?.disabled || props.disabled) return
83
+ value.value = next
84
+ emit('update:modelValue', next)
85
+ validate('change')
86
+ }
87
+
88
+ const onBlur = () => {
89
+ touched.value = true
90
+ validate('blur')
91
+ }
92
+
93
+ provide('nexa-ui:form-field', {
94
+ inputId,
95
+ describedBy,
96
+ invalid,
97
+ error,
98
+ errors,
99
+ touched,
100
+ dirty,
101
+ pending,
102
+ value,
103
+ setValue,
104
+ onBlur,
105
+ disabled: computed(() => !!(props.disabled || form?.disabled)),
106
+ })
107
+
108
+ const reset = () => {
109
+ touched.value = false
110
+ errors.value = []
111
+ pending.value = false
112
+ initialValue.value = props.modelValue
113
+ value.value = props.modelValue
114
+ }
115
+
116
+ let unregister = null
117
+ if (form?.registerField && props.name) {
118
+ unregister = form.registerField(props.name, { value, touched, dirty, errors, pending, validate, reset })
119
+ }
120
+
121
+ onUnmounted(() => {
122
+ if (unregister) unregister()
123
+ })
124
+ </script>
125
+
126
+ <template>
127
+ <div class="n-form-field" :class="{ 'is-invalid': invalid.value, 'is-disabled': disabled, 'is-pending': pending.value }">
128
+ <label v-if="label" class="n-form-field-label" :for="inputId.value">{{ label }}</label>
129
+ <div class="n-form-field-control">
130
+ <slot />
131
+ </div>
132
+ <div v-if="help" :id="helpId.value" class="n-form-field-help">{{ help }}</div>
133
+ <div v-if="showError && error.value" :id="errorId.value" class="n-form-field-error">{{ error.value }}</div>
134
+ </div>
135
+ </template>
136
+
137
+ <style scoped>
138
+ .n-form-field {
139
+ display: flex;
140
+ flex-direction: column;
141
+ gap: var(--n-space-2);
142
+ width: 100%;
143
+ }
144
+
145
+ .n-form-field-label {
146
+ font-size: var(--n-text-sm);
147
+ font-weight: var(--n-weight-semibold);
148
+ color: var(--n-color-text-secondary);
149
+ margin-left: var(--n-space-1);
150
+ }
151
+
152
+ .n-form-field-control {
153
+ width: 100%;
154
+ }
155
+
156
+ .n-form-field-help {
157
+ font-size: var(--n-text-xs);
158
+ color: var(--n-color-text-muted);
159
+ padding: 0 var(--n-space-1);
160
+ }
161
+
162
+ .n-form-field-error {
163
+ font-size: var(--n-text-xs);
164
+ color: var(--n-color-danger);
165
+ padding: 0 var(--n-space-1);
166
+ }
167
+
168
+ .is-disabled {
169
+ opacity: 0.6;
170
+ }
171
+ </style>
@@ -0,0 +1,311 @@
1
+ <script setup>
2
+ import { signal, computed, inject, effect } from 'nexa-framework'
3
+
4
+ const props = defineProps({
5
+ modelValue: { type: [String, Number], default: '' },
6
+ type: { type: String, default: 'text' },
7
+ placeholder: { type: String, default: '' },
8
+ label: { type: String, default: '' },
9
+ error: { type: String, default: '' },
10
+ id: { type: String, default: '' },
11
+ name: { type: String, default: '' },
12
+ autocomplete: { type: String, default: '' },
13
+ ariaDescribedby: { type: String, default: '' },
14
+ ariaInvalid: { type: [Boolean, String], default: false },
15
+ bindField: { type: Boolean, default: false },
16
+ disabled: { type: Boolean, default: false },
17
+ readonly: { type: Boolean, default: false },
18
+ clearable: { type: Boolean, default: false },
19
+ maxlength: { type: Number, default: 0 },
20
+ prefixIcon: { type: String, default: '' },
21
+ suffixIcon: { type: String, default: '' }
22
+ })
23
+
24
+ const emit = defineEmits(['update:modelValue', 'clear', 'focus', 'blur'])
25
+
26
+ const field = inject('nexa-ui:form-field', undefined)
27
+
28
+ const showPassword = signal(false)
29
+ const isFocused = signal(false)
30
+
31
+ const effectiveValue = computed(() => {
32
+ if (props.bindField && field?.value) return field.value.value
33
+ return props.modelValue
34
+ })
35
+
36
+ const draft = signal(String(effectiveValue.value ?? ''))
37
+
38
+ effect(() => {
39
+ const next = String(effectiveValue.value ?? '')
40
+ if (draft.value === next) return
41
+ draft.value = next
42
+ })
43
+
44
+ const effectiveId = computed(() => {
45
+ if (props.id) return props.id
46
+ if (props.bindField && field?.inputId) return field.inputId.value
47
+ return ''
48
+ })
49
+
50
+ const effectiveDescribedBy = computed(() => {
51
+ if (props.ariaDescribedby) return props.ariaDescribedby
52
+ if (props.bindField && field?.describedBy) return field.describedBy.value || ''
53
+ return ''
54
+ })
55
+
56
+ const effectiveInvalid = computed(() => {
57
+ if (props.bindField && field?.invalid) return field.invalid.value
58
+ return props.ariaInvalid
59
+ })
60
+
61
+ const effectiveError = computed(() => {
62
+ if (props.error) return props.error
63
+ if (props.bindField && field?.error) return field.error.value || ''
64
+ return ''
65
+ })
66
+
67
+ const effectiveDisabled = computed(() => {
68
+ if (props.disabled) return true
69
+ if (props.bindField && field?.disabled) return !!field.disabled.value
70
+ return false
71
+ })
72
+
73
+ const inputType = computed(() => {
74
+ if (props.type === 'password' && showPassword.value) return 'text'
75
+ return props.type
76
+ })
77
+
78
+ const currentLength = computed(() => draft.value.length)
79
+
80
+ const onInput = (e) => {
81
+ const next = e.target.value
82
+ if (draft.value !== next) draft.value = next
83
+ if (props.bindField && field?.setValue) {
84
+ field.setValue(next)
85
+ return
86
+ }
87
+ emit('update:modelValue', next)
88
+ }
89
+
90
+ const onFocus = () => {
91
+ isFocused.value = true
92
+ emit('focus')
93
+ }
94
+
95
+ const onBlur = () => {
96
+ isFocused.value = false
97
+ if (props.bindField && field?.onBlur) {
98
+ field.onBlur()
99
+ }
100
+ emit('blur')
101
+ }
102
+
103
+ const clear = () => {
104
+ if (draft.value) draft.value = ''
105
+ if (props.bindField && field?.setValue) {
106
+ field.setValue('')
107
+ emit('clear')
108
+ return
109
+ }
110
+ emit('update:modelValue', '')
111
+ emit('clear')
112
+ }
113
+
114
+ const togglePassword = () => {
115
+ showPassword.value = !showPassword.value
116
+ }
117
+ </script>
118
+
119
+ <template>
120
+ <div class="n-input-group">
121
+ <label v-if="label" class="n-input-label">{{ label }}</label>
122
+ <div class="n-input-wrapper" :class="{ 'is-focused': isFocused.value, 'has-error': effectiveError.value, 'is-disabled': effectiveDisabled.value }">
123
+ <span v-if="prefixIcon" class="n-input-icon is-prefix">{{ prefixIcon }}</span>
124
+ <input
125
+ :id="effectiveId.value || undefined"
126
+ :name="name || undefined"
127
+ :type="inputType.value"
128
+ :value="draft.value"
129
+ :placeholder="placeholder"
130
+ :disabled="effectiveDisabled.value"
131
+ :readonly="readonly"
132
+ :autocomplete="autocomplete || undefined"
133
+ :aria-describedby="effectiveDescribedBy.value || undefined"
134
+ :aria-invalid="effectiveInvalid.value || undefined"
135
+ :maxlength="maxlength || undefined"
136
+ class="n-input"
137
+ :class="{ 'has-prefix': !!prefixIcon, 'has-suffix': !!suffixIcon || clearable || type === 'password' }"
138
+ @input="onInput"
139
+ @focus="onFocus"
140
+ @blur="onBlur"
141
+ />
142
+ <div class="n-input-focus-ring"></div>
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 ? '◉' : '◎' }}
147
+ </button>
148
+ <span v-if="suffixIcon" class="n-input-icon is-suffix">{{ suffixIcon }}</span>
149
+ </div>
150
+ </div>
151
+ <div class="n-input-bottom">
152
+ <span v-if="effectiveError.value" class="n-input-error-msg">{{ effectiveError.value }}</span>
153
+ <span v-if="maxlength > 0" class="n-input-counter">{{ currentLength.value }}/{{ maxlength }}</span>
154
+ </div>
155
+ </div>
156
+ </template>
157
+
158
+ <style scoped>
159
+ .n-input-group {
160
+ display: flex;
161
+ flex-direction: column;
162
+ gap: var(--n-space-2);
163
+ width: 100%;
164
+ }
165
+
166
+ .n-input-label {
167
+ font-size: var(--n-text-sm);
168
+ font-weight: var(--n-weight-semibold);
169
+ color: var(--n-color-text-secondary);
170
+ margin-left: var(--n-space-1);
171
+ }
172
+
173
+ .n-input-wrapper {
174
+ position: relative;
175
+ display: flex;
176
+ align-items: center;
177
+ transition: all var(--n-transition-fast);
178
+ }
179
+
180
+ .n-input {
181
+ position: relative;
182
+ z-index: 1;
183
+ width: 100%;
184
+ min-height: 44px;
185
+ background: var(--n-color-bg);
186
+ border: 1px solid var(--n-color-border);
187
+ color: var(--n-color-text);
188
+ padding: 0.75rem 1rem;
189
+ border-radius: var(--n-radius-md);
190
+ font-family: inherit;
191
+ font-size: var(--n-text-base);
192
+ line-height: 1.2;
193
+ box-sizing: border-box;
194
+ transition: all var(--n-transition-normal);
195
+ outline: none;
196
+ }
197
+
198
+ .n-input.has-prefix {
199
+ padding-left: 2.5rem;
200
+ }
201
+
202
+ .n-input.has-suffix {
203
+ padding-right: 2.5rem;
204
+ }
205
+
206
+ .n-input::placeholder {
207
+ color: var(--n-color-text-muted);
208
+ }
209
+
210
+ .n-input-focus-ring {
211
+ position: absolute;
212
+ inset: 0;
213
+ border-radius: var(--n-radius-md);
214
+ background: linear-gradient(135deg, var(--n-color-primary), var(--n-color-info));
215
+ opacity: 0;
216
+ pointer-events: none;
217
+ transition: opacity var(--n-transition-normal);
218
+ z-index: 0;
219
+ }
220
+
221
+ .is-focused .n-input-focus-ring {
222
+ opacity: 0.35;
223
+ }
224
+
225
+ .is-focused .n-input {
226
+ border-color: var(--n-color-primary);
227
+ background: var(--n-color-surface);
228
+ }
229
+
230
+ .has-error .n-input {
231
+ border-color: var(--n-color-danger);
232
+ }
233
+
234
+ .has-error .n-input-focus-ring {
235
+ background: var(--n-color-danger);
236
+ opacity: 0.35;
237
+ }
238
+
239
+ .n-input-icon {
240
+ position: absolute;
241
+ top: 50%;
242
+ transform: translateY(-50%);
243
+ color: var(--n-color-text-muted);
244
+ font-size: var(--n-text-sm);
245
+ pointer-events: none;
246
+ display: flex;
247
+ align-items: center;
248
+ line-height: 1;
249
+ }
250
+
251
+ .n-input-icon.is-prefix {
252
+ left: 0.85rem;
253
+ }
254
+
255
+ .n-input-icon.is-suffix {
256
+ right: 0.85rem;
257
+ }
258
+
259
+ .n-input-actions {
260
+ position: absolute;
261
+ right: 0.5rem;
262
+ top: 50%;
263
+ transform: translateY(-50%);
264
+ display: flex;
265
+ align-items: center;
266
+ gap: 0.15rem;
267
+ }
268
+
269
+ .n-input-action {
270
+ background: transparent;
271
+ border: none;
272
+ color: var(--n-color-text-muted);
273
+ cursor: pointer;
274
+ padding: 0.25rem;
275
+ font-size: var(--n-text-sm);
276
+ border-radius: var(--n-radius-sm);
277
+ transition: all var(--n-transition-fast);
278
+ display: flex;
279
+ align-items: center;
280
+ line-height: 1;
281
+ }
282
+
283
+ .n-input-action:hover {
284
+ color: var(--n-color-text);
285
+ background: var(--n-color-glass);
286
+ }
287
+
288
+ .n-input-bottom {
289
+ display: flex;
290
+ justify-content: space-between;
291
+ align-items: center;
292
+ padding: 0 var(--n-space-1);
293
+ }
294
+
295
+ .n-input-error-msg {
296
+ font-size: var(--n-text-xs);
297
+ color: var(--n-color-danger);
298
+ }
299
+
300
+ .n-input-counter {
301
+ font-size: var(--n-text-xs);
302
+ color: var(--n-color-text-muted);
303
+ margin-left: auto;
304
+ }
305
+
306
+ .is-disabled .n-input {
307
+ opacity: 0.5;
308
+ cursor: not-allowed;
309
+ background: var(--n-color-surface-alt);
310
+ }
311
+ </style>