nexa-ui-kit 0.7.11 → 0.8.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.
@@ -1,232 +1,232 @@
1
- <script setup>
2
- import { signal, computed, inject, effect, batch } from 'nexa-framework'
3
-
4
- const props = defineProps({
5
- modelValue: { type: Number, default: 0 },
6
- min: { type: Number, default: null },
7
- max: { type: Number, default: null },
8
- step: { type: Number, default: 1 },
9
- bindField: { type: Boolean, default: false },
10
- disabled: { type: Boolean, default: false },
11
- readonly: { type: Boolean, default: false },
12
- label: { type: String, default: '' },
13
- placeholder: { type: String, default: '' },
14
- currency: { type: String, default: undefined },
15
- locale: { type: String, default: undefined },
16
- })
17
-
18
- const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
19
-
20
- const field = inject('nexa-ui:form-field', undefined)
21
-
22
- const effectiveValue = computed(() => {
23
- if (props.bindField && field?.value) {
24
- const v = field.value.value
25
- return typeof v === 'number' ? v : Number(v)
26
- }
27
- return props.modelValue
28
- })
29
-
30
- const effectiveDisabled = computed(() => {
31
- if (props.disabled) return true
32
- if (props.bindField && field?.disabled) return !!field.disabled.value
33
- return false
34
- })
35
-
36
- const text = signal(String(effectiveValue.value ?? ''))
37
-
38
- effect(() => {
39
- const next = effectiveValue.value
40
- const nextText = next === null || next === undefined ? '' : String(next)
41
- if (text.value === nextText) return
42
- text.value = nextText
43
- })
44
-
45
- const canEdit = computed(() => !effectiveDisabled.value && !props.readonly)
46
-
47
- const isFocused = signal(false)
48
-
49
- const nfSignal = signal(null)
50
-
51
- effect(() => {
52
- if (props.currency) {
53
- nfSignal.value = new Intl.NumberFormat(props.locale || undefined, {
54
- style: 'currency',
55
- currency: props.currency,
56
- minimumFractionDigits: 2,
57
- maximumFractionDigits: 2,
58
- })
59
- } else {
60
- nfSignal.value = null
61
- }
62
- })
63
-
64
- const displayText = computed(() => {
65
- const raw = text.value
66
- const nf = nfSignal.value
67
- if (!nf || isFocused.value) return raw
68
- if (!raw) return raw
69
- const n = parseFloat(raw)
70
- if (Number.isNaN(n)) return raw
71
- return nf.format(n)
72
- })
73
-
74
- const sanitize = (raw) => {
75
- const input = String(raw ?? '')
76
- if (!input) return ''
77
- const allowMinus = typeof props.min === 'number' ? props.min < 0 : true
78
- let out = ''
79
- let hasDot = false
80
- let hasMinus = false
81
- for (let i = 0; i < input.length; i++) {
82
- const ch = input[i] === ',' ? '.' : input[i]
83
- const isDigit = ch >= '0' && ch <= '9'
84
- if (isDigit) {
85
- out += ch
86
- continue
87
- }
88
- if (ch === '.' && !hasDot) {
89
- hasDot = true
90
- out += ch
91
- continue
92
- }
93
- if (ch === '-' && allowMinus && !hasMinus && out.length === 0) {
94
- hasMinus = true
95
- out += ch
96
- }
97
- }
98
- return out
99
- }
100
-
101
- const clamp = (n) => {
102
- if (n == null || Number.isNaN(n)) return null
103
- let out = n
104
- if (typeof props.min === 'number' && !Number.isNaN(props.min)) out = Math.max(out, props.min)
105
- if (typeof props.max === 'number' && !Number.isNaN(props.max)) out = Math.min(out, props.max)
106
- return out
107
- }
108
-
109
- const parse = (raw) => {
110
- const s = sanitize(raw).trim()
111
- if (!s) return null
112
- const n = Number(s)
113
- if (Number.isNaN(n)) return null
114
- return n
115
- }
116
-
117
- const setValue = (next) => {
118
- if (!canEdit.value) return
119
- const clamped = clamp(next)
120
- if (clamped == null) return
121
- batch(() => {
122
- if (props.bindField && field?.setValue) field.setValue(clamped)
123
- else emit('update:modelValue', clamped)
124
- text.value = String(clamped)
125
- })
126
- }
127
-
128
- const inc = () => {
129
- if (!canEdit.value) return
130
- const base = typeof effectiveValue.value === 'number' && !Number.isNaN(effectiveValue.value) ? effectiveValue.value : 0
131
- setValue(base + (props.step || 1))
132
- }
133
-
134
- const dec = () => {
135
- if (!canEdit.value) return
136
- const base = typeof effectiveValue.value === 'number' && !Number.isNaN(effectiveValue.value) ? effectiveValue.value : 0
137
- setValue(base - (props.step || 1))
138
- }
139
-
140
- const onInput = (e) => {
141
- if (!canEdit.value) return
142
- const nextRaw = e.target.value
143
- const nextText = sanitize(nextRaw)
144
- if (e?.target && e.target.value !== nextText) {
145
- e.target.value = nextText
146
- }
147
- batch(() => {
148
- text.value = nextText
149
- const n = parse(nextText)
150
- const clamped = clamp(n)
151
- if (clamped == null) return
152
- if (props.bindField && field?.setValue) field.setValue(clamped)
153
- else emit('update:modelValue', clamped)
154
- })
155
- }
156
-
157
- const onBeforeInput = (e) => {
158
- if (!canEdit.value) return
159
- const inputType = e?.inputType
160
- if (typeof inputType === 'string' && inputType.includes('Composition')) return
161
- if (typeof inputType === 'string' && !inputType.startsWith('insert')) return
162
- const data = e?.data
163
- if (typeof data !== 'string' || !data) return
164
- for (let i = 0; i < data.length; i++) {
165
- const ch = data[i] === ',' ? '.' : data[i]
166
- const isDigit = ch >= '0' && ch <= '9'
167
- if (isDigit || ch === '.' || ch === '-') continue
168
- e.preventDefault()
169
- return
170
- }
171
- }
172
-
173
- const onPaste = (e) => {
174
- if (!canEdit.value) return
175
- const pasted = e?.clipboardData?.getData?.('text')
176
- if (typeof pasted !== 'string') return
177
- e.preventDefault()
178
- const nextText = sanitize(pasted)
179
- const target = e?.target
180
- if (!target) return
181
- target.value = nextText
182
- onInput({ target })
183
- }
184
-
185
- const onKeydown = (e) => {
186
- if (!canEdit.value) return
187
- if (e.ctrlKey || e.metaKey || e.altKey) return
188
- const key = e.key
189
- if (typeof key !== 'string' || key.length !== 1) return
190
- const ch = key === ',' ? '.' : key
191
- const isDigit = ch >= '0' && ch <= '9'
192
- if (isDigit || ch === '.' || ch === '-') return
193
- e.preventDefault()
194
- }
195
-
196
- const onBlur = () => {
197
- emit('blur')
198
- if (props.bindField && field?.onBlur) field.onBlur()
199
- isFocused.value = false
200
- const n = parse(text.value)
201
- const clamped = clamp(n)
202
- if (clamped == null) {
203
- text.value = String(effectiveValue.value ?? '')
204
- return
205
- }
206
- batch(() => {
207
- text.value = String(clamped)
208
- if (props.bindField && field?.setValue) field.setValue(clamped)
209
- else emit('update:modelValue', clamped)
210
- })
211
- }
212
-
213
- const onFocus = () => {
214
- isFocused.value = true
215
- emit('focus')
216
- }
217
- </script>
218
-
219
- <template>
220
- <div class="n-inum">
221
- <label v-if="label" class="n-inum-label">{{ label }}</label>
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>
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>
226
- </div>
227
- </div>
228
- </template>
229
-
230
- <style scoped>
231
- .n-inum{display:flex;flex-direction:column;gap:var(--n-space-2);width:100%;font-family:var(--n-font-sans)}.n-inum-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-inum-wrap{display:flex;align-items:stretch;min-height:44px;background:var(--n-color-surface);border:1px solid var(--n-color-border);border-radius:var(--n-radius-md);overflow:hidden;transition:all var(--n-transition-fast)}.n-inum-wrap:focus-within{border-color:var(--n-color-primary);box-shadow:0 0 0 3px var(--n-color-primary-light)}.n-inum-input{flex:1;background:transparent;border:none;outline:none;padding:0.75rem 0.75rem;color:var(--n-color-text);font-size:var(--n-text-base);font-family:inherit;text-align:center;line-height:1.2;box-sizing:border-box}.n-inum-btn{width:2.5rem;background:transparent;border:none;color:var(--n-color-text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:var(--n-text-base);transition:all var(--n-transition-fast)}.n-inum-btn:hover:not(:disabled){background:var(--n-color-glass);color:var(--n-color-text)}.n-inum-btn:disabled{opacity:0.5;cursor:not-allowed}.is-disabled{opacity:0.6;cursor:not-allowed;background:var(--n-color-surface-alt)}
232
- </style>
1
+ <script setup>
2
+ import { signal, computed, inject, effect, batch } from 'nexa-framework'
3
+
4
+ const props = defineProps({
5
+ modelValue: { type: Number, default: 0 },
6
+ min: { type: Number, default: null },
7
+ max: { type: Number, default: null },
8
+ step: { type: Number, default: 1 },
9
+ bindField: { type: Boolean, default: false },
10
+ disabled: { type: Boolean, default: false },
11
+ readonly: { type: Boolean, default: false },
12
+ label: { type: String, default: '' },
13
+ placeholder: { type: String, default: '' },
14
+ currency: { type: String, default: undefined },
15
+ locale: { type: String, default: undefined },
16
+ })
17
+
18
+ const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
19
+
20
+ const field = inject('nexa-ui:form-field', undefined)
21
+
22
+ const effectiveValue = computed(() => {
23
+ if (props.bindField && field?.value) {
24
+ const v = field.value.value
25
+ return typeof v === 'number' ? v : Number(v)
26
+ }
27
+ return props.modelValue
28
+ })
29
+
30
+ const effectiveDisabled = computed(() => {
31
+ if (props.disabled) return true
32
+ if (props.bindField && field?.disabled) return !!field.disabled.value
33
+ return false
34
+ })
35
+
36
+ const text = signal(String(effectiveValue.value ?? ''))
37
+
38
+ effect(() => {
39
+ const next = effectiveValue.value
40
+ const nextText = next === null || next === undefined ? '' : String(next)
41
+ if (text.value === nextText) return
42
+ text.value = nextText
43
+ })
44
+
45
+ const canEdit = computed(() => !effectiveDisabled.value && !props.readonly)
46
+
47
+ const isFocused = signal(false)
48
+
49
+ const nfSignal = signal(null)
50
+
51
+ effect(() => {
52
+ if (props.currency) {
53
+ nfSignal.value = new Intl.NumberFormat(props.locale || undefined, {
54
+ style: 'currency',
55
+ currency: props.currency,
56
+ minimumFractionDigits: 2,
57
+ maximumFractionDigits: 2,
58
+ })
59
+ } else {
60
+ nfSignal.value = null
61
+ }
62
+ })
63
+
64
+ const displayText = computed(() => {
65
+ const raw = text.value
66
+ const nf = nfSignal.value
67
+ if (!nf || isFocused.value) return raw
68
+ if (!raw) return raw
69
+ const n = parseFloat(raw)
70
+ if (Number.isNaN(n)) return raw
71
+ return nf.format(n)
72
+ })
73
+
74
+ const sanitize = (raw) => {
75
+ const input = String(raw ?? '')
76
+ if (!input) return ''
77
+ const allowMinus = typeof props.min === 'number' ? props.min < 0 : true
78
+ let out = ''
79
+ let hasDot = false
80
+ let hasMinus = false
81
+ for (let i = 0; i < input.length; i++) {
82
+ const ch = input[i] === ',' ? '.' : input[i]
83
+ const isDigit = ch >= '0' && ch <= '9'
84
+ if (isDigit) {
85
+ out += ch
86
+ continue
87
+ }
88
+ if (ch === '.' && !hasDot) {
89
+ hasDot = true
90
+ out += ch
91
+ continue
92
+ }
93
+ if (ch === '-' && allowMinus && !hasMinus && out.length === 0) {
94
+ hasMinus = true
95
+ out += ch
96
+ }
97
+ }
98
+ return out
99
+ }
100
+
101
+ const clamp = (n) => {
102
+ if (n == null || Number.isNaN(n)) return null
103
+ let out = n
104
+ if (typeof props.min === 'number' && !Number.isNaN(props.min)) out = Math.max(out, props.min)
105
+ if (typeof props.max === 'number' && !Number.isNaN(props.max)) out = Math.min(out, props.max)
106
+ return out
107
+ }
108
+
109
+ const parse = (raw) => {
110
+ const s = sanitize(raw).trim()
111
+ if (!s) return null
112
+ const n = Number(s)
113
+ if (Number.isNaN(n)) return null
114
+ return n
115
+ }
116
+
117
+ const setValue = (next) => {
118
+ if (!canEdit.value) return
119
+ const clamped = clamp(next)
120
+ if (clamped == null) return
121
+ batch(() => {
122
+ if (props.bindField && field?.setValue) field.setValue(clamped)
123
+ else emit('update:modelValue', clamped)
124
+ text.value = String(clamped)
125
+ })
126
+ }
127
+
128
+ const inc = () => {
129
+ if (!canEdit.value) return
130
+ const base = typeof effectiveValue.value === 'number' && !Number.isNaN(effectiveValue.value) ? effectiveValue.value : 0
131
+ setValue(base + (props.step || 1))
132
+ }
133
+
134
+ const dec = () => {
135
+ if (!canEdit.value) return
136
+ const base = typeof effectiveValue.value === 'number' && !Number.isNaN(effectiveValue.value) ? effectiveValue.value : 0
137
+ setValue(base - (props.step || 1))
138
+ }
139
+
140
+ const onInput = (e) => {
141
+ if (!canEdit.value) return
142
+ const nextRaw = e.target.value
143
+ const nextText = sanitize(nextRaw)
144
+ if (e?.target && e.target.value !== nextText) {
145
+ e.target.value = nextText
146
+ }
147
+ batch(() => {
148
+ text.value = nextText
149
+ const n = parse(nextText)
150
+ const clamped = clamp(n)
151
+ if (clamped == null) return
152
+ if (props.bindField && field?.setValue) field.setValue(clamped)
153
+ else emit('update:modelValue', clamped)
154
+ })
155
+ }
156
+
157
+ const onBeforeInput = (e) => {
158
+ if (!canEdit.value) return
159
+ const inputType = e?.inputType
160
+ if (typeof inputType === 'string' && inputType.includes('Composition')) return
161
+ if (typeof inputType === 'string' && !inputType.startsWith('insert')) return
162
+ const data = e?.data
163
+ if (typeof data !== 'string' || !data) return
164
+ for (let i = 0; i < data.length; i++) {
165
+ const ch = data[i] === ',' ? '.' : data[i]
166
+ const isDigit = ch >= '0' && ch <= '9'
167
+ if (isDigit || ch === '.' || ch === '-') continue
168
+ e.preventDefault()
169
+ return
170
+ }
171
+ }
172
+
173
+ const onPaste = (e) => {
174
+ if (!canEdit.value) return
175
+ const pasted = e?.clipboardData?.getData?.('text')
176
+ if (typeof pasted !== 'string') return
177
+ e.preventDefault()
178
+ const nextText = sanitize(pasted)
179
+ const target = e?.target
180
+ if (!target) return
181
+ target.value = nextText
182
+ onInput({ target })
183
+ }
184
+
185
+ const onKeydown = (e) => {
186
+ if (!canEdit.value) return
187
+ if (e.ctrlKey || e.metaKey || e.altKey) return
188
+ const key = e.key
189
+ if (typeof key !== 'string' || key.length !== 1) return
190
+ const ch = key === ',' ? '.' : key
191
+ const isDigit = ch >= '0' && ch <= '9'
192
+ if (isDigit || ch === '.' || ch === '-') return
193
+ e.preventDefault()
194
+ }
195
+
196
+ const onBlur = () => {
197
+ emit('blur')
198
+ if (props.bindField && field?.onBlur) field.onBlur()
199
+ isFocused.value = false
200
+ const n = parse(text.value)
201
+ const clamped = clamp(n)
202
+ if (clamped == null) {
203
+ text.value = String(effectiveValue.value ?? '')
204
+ return
205
+ }
206
+ batch(() => {
207
+ text.value = String(clamped)
208
+ if (props.bindField && field?.setValue) field.setValue(clamped)
209
+ else emit('update:modelValue', clamped)
210
+ })
211
+ }
212
+
213
+ const onFocus = () => {
214
+ isFocused.value = true
215
+ emit('focus')
216
+ }
217
+ </script>
218
+
219
+ <template>
220
+ <div class="n-inum">
221
+ <label v-if="label" class="n-inum-label">{{ label }}</label>
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>
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>
226
+ </div>
227
+ </div>
228
+ </template>
229
+
230
+ <style scoped>
231
+ .n-inum{display:flex;flex-direction:column;gap:var(--n-space-2);width:100%;font-family:var(--n-font-sans)}.n-inum-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-inum-wrap{display:flex;align-items:stretch;min-height:44px;background:var(--n-color-surface);border:1px solid var(--n-color-border);border-radius:var(--n-radius-md);overflow:hidden;transition:all var(--n-transition-fast)}.n-inum-wrap:focus-within{border-color:var(--n-color-primary);box-shadow:0 0 0 3px var(--n-color-primary-light)}.n-inum-input{flex:1;background:transparent;border:none;outline:none;padding:0.75rem 0.75rem;color:var(--n-color-text);font-size:var(--n-text-base);font-family:inherit;text-align:center;line-height:1.2;box-sizing:border-box}.n-inum-btn{width:2.5rem;background:transparent;border:none;color:var(--n-color-text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:var(--n-text-base);transition:all var(--n-transition-fast)}.n-inum-btn:hover:not(:disabled){background:var(--n-color-glass);color:var(--n-color-text)}.n-inum-btn:disabled{opacity:0.5;cursor:not-allowed}.is-disabled{opacity:0.6;cursor:not-allowed;background:var(--n-color-surface-alt)}
232
+ </style>