webcake-ui-kit 1.1.10 → 1.1.12

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-ui-kit",
3
- "version": "1.1.10",
3
+ "version": "1.1.12",
4
4
  "description": "UI Kit for Vue 2 && 3 - Pure Options API",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -33,6 +33,7 @@
33
33
  "./WkEmptyIcon": "./src/components/empty-icon/EmptyIcon.vue",
34
34
  "./WkField": "./src/components/field/Field.vue",
35
35
  "./WkInput": "./src/components/input/Input.vue",
36
+ "./WkInputNumber": "./src/components/input-number/InputNumber.vue",
36
37
  "./WkMenu": "./src/components/menu/Menu.vue",
37
38
  "./WkPagination": "./src/components/pagination/Pagination.vue",
38
39
  "./WkRadio": "./src/components/radio/Radio.vue",
@@ -0,0 +1,189 @@
1
+ <template>
2
+ <span
3
+ :class="[
4
+ 'ui-input-number',
5
+ `ui-input-number--size-${size}`,
6
+ `ui-input-number--round-${roundness}`,
7
+ error && 'ui-input-number--error',
8
+ disabled && 'ui-input-number--disabled',
9
+ $attrs.class
10
+ ]"
11
+ >
12
+ <input
13
+ ref="input"
14
+ class="ui-input-number__field"
15
+ type="text"
16
+ inputmode="decimal"
17
+ :value="displayValue"
18
+ :placeholder="placeholder"
19
+ :disabled="disabled"
20
+ :readonly="readonly"
21
+ :name="name"
22
+ v-bind="{ ...$attrs, class: undefined }"
23
+ @input="onInput"
24
+ @focus="onFocus"
25
+ @blur="onBlur"
26
+ @keydown="onKeydown"
27
+ />
28
+ </span>
29
+ </template>
30
+
31
+ <script>
32
+ export default {
33
+ name: 'InputNumber',
34
+ inheritAttrs: false,
35
+ props: {
36
+ value: { default: null },
37
+ modelValue: { default: undefined },
38
+ min: { type: Number, default: null },
39
+ max: { type: Number, default: null },
40
+ step: { type: Number, default: 1 },
41
+ precision: { type: Number, default: null },
42
+ placeholder: { type: String, default: '' },
43
+ name: { type: String, default: '' },
44
+ disabled: { type: Boolean, default: false },
45
+ readonly: { type: Boolean, default: false },
46
+ size: {
47
+ type: String,
48
+ default: 'md',
49
+ validator: v => ['tiny', 'xs', 'sm', 'md', 'lg'].includes(v)
50
+ },
51
+ roundness: {
52
+ type: String,
53
+ default: 'default',
54
+ validator: v => ['default', 'round'].includes(v)
55
+ },
56
+ error: { type: Boolean, default: false }
57
+ },
58
+ emits: ['input', 'update:modelValue', 'change', 'focus', 'blur', 'pressEnter'],
59
+ data() {
60
+ return {
61
+ focused: false,
62
+ inputText: ''
63
+ }
64
+ },
65
+ computed: {
66
+ currentValue() {
67
+ return this.modelValue !== undefined ? this.modelValue : this.value
68
+ },
69
+ effectivePrecision() {
70
+ if (this.precision !== null) return this.precision
71
+ const s = String(this.step)
72
+ const dot = s.indexOf('.')
73
+ return dot >= 0 ? s.length - dot - 1 : 0
74
+ },
75
+ displayValue() {
76
+ if (this.focused) return this.inputText
77
+ const v = this.currentValue
78
+ if (v === null || v === undefined) return ''
79
+ const n = Number(v)
80
+ if (isNaN(n)) return ''
81
+ return n.toFixed(this.effectivePrecision)
82
+ }
83
+ },
84
+ methods: {
85
+ focus() {
86
+ if (this.$refs.input) this.$refs.input.focus()
87
+ },
88
+ blur() {
89
+ if (this.$refs.input) this.$refs.input.blur()
90
+ },
91
+ toPrecision(num) {
92
+ return parseFloat(num.toFixed(this.effectivePrecision))
93
+ },
94
+ clamp(num) {
95
+ let v = num
96
+ if (this.max !== null && v > this.max) v = this.max
97
+ if (this.min !== null && v < this.min) v = this.min
98
+ return v
99
+ },
100
+ sanitize(raw) {
101
+ let s = raw.replace(/[^\d.-]/g, '')
102
+ const hasMinus = s.charAt(0) === '-'
103
+ s = s.replace(/-/g, '')
104
+ if (hasMinus) s = '-' + s
105
+ if (this.effectivePrecision === 0) {
106
+ s = s.replace(/\./g, '')
107
+ } else {
108
+ const firstDot = s.indexOf('.')
109
+ if (firstDot >= 0) {
110
+ s = s.slice(0, firstDot + 1) + s.slice(firstDot + 1).replace(/\./g, '')
111
+ }
112
+ }
113
+ return s
114
+ },
115
+ commitInput() {
116
+ const raw = this.inputText
117
+ if (raw === '' || raw === '-') {
118
+ this.$emit('input', null)
119
+ this.$emit('update:modelValue', null)
120
+ this.$emit('change', null)
121
+ return
122
+ }
123
+ const num = parseFloat(raw)
124
+ if (!isNaN(num)) {
125
+ const committed = this.clamp(this.toPrecision(num))
126
+ this.$emit('input', committed)
127
+ this.$emit('update:modelValue', committed)
128
+ this.$emit('change', committed)
129
+ this.inputText = committed.toFixed(this.effectivePrecision)
130
+ }
131
+ },
132
+ onInput(e) {
133
+ const sanitized = this.sanitize(e.target.value)
134
+ if (sanitized !== e.target.value) {
135
+ const pos = Math.max(0, e.target.selectionStart - (e.target.value.length - sanitized.length))
136
+ e.target.value = sanitized
137
+ try {
138
+ e.target.setSelectionRange(pos, pos)
139
+ } catch (err) {
140
+ console.error(err)
141
+ }
142
+ }
143
+ this.inputText = sanitized
144
+ if (sanitized === '' || sanitized === '-') return
145
+ const num = parseFloat(sanitized)
146
+ if (!isNaN(num)) {
147
+ this.$emit('input', this.toPrecision(num))
148
+ this.$emit('update:modelValue', this.toPrecision(num))
149
+ }
150
+ },
151
+ onFocus(e) {
152
+ this.focused = true
153
+ const v = this.currentValue
154
+ this.inputText =
155
+ v !== null && v !== undefined ? parseFloat(Number(v).toFixed(this.effectivePrecision)).toString() : ''
156
+ this.$emit('focus', this.currentValue, e)
157
+ },
158
+ onBlur(e) {
159
+ this.focused = false
160
+ this.commitInput()
161
+ this.$emit('blur', this.currentValue, e)
162
+ },
163
+ onKeydown(e) {
164
+ if (e.key === 'ArrowUp') {
165
+ e.preventDefault()
166
+ const base = this.currentValue !== null && this.currentValue !== undefined ? Number(this.currentValue) : 0
167
+ const next = this.clamp(this.toPrecision(base + this.step))
168
+ this.$emit('input', next)
169
+ this.$emit('update:modelValue', next)
170
+ this.$emit('change', next)
171
+ this.inputText = String(next)
172
+ } else if (e.key === 'ArrowDown') {
173
+ e.preventDefault()
174
+ const base = this.currentValue !== null && this.currentValue !== undefined ? Number(this.currentValue) : 0
175
+ const next = this.clamp(this.toPrecision(base - this.step))
176
+ this.$emit('input', next)
177
+ this.$emit('update:modelValue', next)
178
+ this.$emit('change', next)
179
+ this.inputText = String(next)
180
+ } else if (e.key === 'Enter') {
181
+ this.commitInput()
182
+ this.$emit('pressEnter', this.currentValue, e)
183
+ }
184
+ }
185
+ }
186
+ }
187
+ </script>
188
+
189
+ <style src="./input-number.css" scoped></style>
@@ -0,0 +1,130 @@
1
+ .ui-input-number {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ background: var(--input);
5
+ border: 1px solid var(--border-primary);
6
+ box-shadow: var(--shadow-xs);
7
+ font-family: var(--font-family-body);
8
+ color: var(--primary-fg);
9
+ overflow: hidden;
10
+ transition:
11
+ border-color 0.12s ease,
12
+ box-shadow 0.12s ease;
13
+ }
14
+
15
+ .ui-input-number--size-md {
16
+ height: 36px;
17
+ gap: var(--spacing-xs);
18
+ padding: var(--spacing-7p5) var(--spacing-sm);
19
+ font-size: var(--paragraph-small-font-size);
20
+ line-height: var(--paragraph-small-line-height);
21
+ letter-spacing: var(--paragraph-small-letter-spacing);
22
+ }
23
+
24
+ .ui-input-number--size-lg {
25
+ height: 40px;
26
+ gap: var(--spacing-sm);
27
+ padding: var(--spacing-9p5) var(--spacing-md);
28
+ font-size: var(--paragraph-small-font-size);
29
+ line-height: var(--paragraph-small-line-height);
30
+ letter-spacing: var(--paragraph-small-letter-spacing);
31
+ }
32
+
33
+ .ui-input-number--size-sm {
34
+ height: 32px;
35
+ gap: var(--spacing-6);
36
+ padding: var(--spacing-5p5) var(--spacing-xs);
37
+ font-size: var(--paragraph-small-font-size);
38
+ line-height: var(--paragraph-small-line-height);
39
+ letter-spacing: var(--paragraph-small-letter-spacing);
40
+ }
41
+
42
+ .ui-input-number--size-xs {
43
+ height: 28px;
44
+ gap: var(--spacing-2xs);
45
+ padding: var(--spacing-6);
46
+ font-size: var(--paragraph-mini-font-size);
47
+ line-height: var(--paragraph-mini-line-height);
48
+ letter-spacing: var(--paragraph-mini-letter-spacing);
49
+ }
50
+
51
+ .ui-input-number--size-tiny {
52
+ height: 24px;
53
+ gap: var(--spacing-2xs);
54
+ padding: var(--spacing-2xs) var(--spacing-6);
55
+ font-size: var(--paragraph-mini-font-size);
56
+ line-height: var(--paragraph-mini-line-height);
57
+ letter-spacing: var(--paragraph-mini-letter-spacing);
58
+ }
59
+
60
+ .ui-input-number--round-default {
61
+ border-radius: var(--rounded-xl);
62
+ }
63
+
64
+ .ui-input-number--size-xs.ui-input-number--round-default {
65
+ border-radius: var(--radius);
66
+ }
67
+
68
+ .ui-input-number--size-tiny.ui-input-number--round-default {
69
+ border-radius: var(--rounded-lg);
70
+ }
71
+
72
+ .ui-input-number--round-round {
73
+ border-radius: var(--rounded-full);
74
+ }
75
+
76
+ .ui-input-number:focus-within {
77
+ border-color: var(--border-focus);
78
+ box-shadow: 0 0 0 3px var(--focus-ring);
79
+ }
80
+
81
+ .ui-input-number--error {
82
+ border-color: var(--destructive-border);
83
+ box-shadow: none;
84
+ }
85
+
86
+ .ui-input-number--error:focus-within {
87
+ border-color: var(--destructive-border);
88
+ box-shadow: 0 0 0 3px var(--focus-ring-error);
89
+ }
90
+
91
+ .ui-input-number--disabled {
92
+ opacity: 0.5;
93
+ }
94
+
95
+ .ui-input-number--disabled:focus-within {
96
+ border-color: var(--border-primary);
97
+ box-shadow: var(--shadow-xs);
98
+ }
99
+
100
+ .ui-input-number__field {
101
+ flex: 1 1 0;
102
+ min-width: 0;
103
+ width: 100%;
104
+ margin: 0;
105
+ padding: 0;
106
+ background: transparent;
107
+ border: 0;
108
+ outline: 0;
109
+ color: inherit;
110
+ font: inherit;
111
+ letter-spacing: inherit;
112
+ -moz-appearance: textfield;
113
+ appearance: textfield;
114
+ }
115
+
116
+ .ui-input-number__field::-webkit-inner-spin-button,
117
+ .ui-input-number__field::-webkit-outer-spin-button {
118
+ -webkit-appearance: none;
119
+ appearance: none;
120
+ margin: 0;
121
+ }
122
+
123
+ .ui-input-number__field::placeholder {
124
+ color: var(--muted-fg);
125
+ opacity: 1;
126
+ }
127
+
128
+ .ui-input-number__field:disabled {
129
+ cursor: not-allowed;
130
+ }
@@ -184,6 +184,14 @@ export default {
184
184
  }
185
185
  },
186
186
 
187
+ mounted() {
188
+ this._syncSlotLabels()
189
+ },
190
+
191
+ updated() {
192
+ this._syncSlotLabels()
193
+ },
194
+
187
195
  computed: {
188
196
  effectiveValue() {
189
197
  return this.modelValue !== null ? this.modelValue : this.value
@@ -220,8 +228,9 @@ export default {
220
228
  if (this.isMultiMode) return ''
221
229
  if (!this.effectiveValue) return ''
222
230
  if (this.labelCache[this.effectiveValue]) return this.labelCache[this.effectiveValue]
223
- var opt = this.normalizedOptions.find(o => o.value === this.effectiveValue)
224
- return opt ? opt.label : this.effectiveValue
231
+ const opt = this.normalizedOptions.find(o => o.value === this.effectiveValue)
232
+ if (opt) return opt.label
233
+ return this._findLabelInSlot(this.effectiveValue) || this.effectiveValue
225
234
  },
226
235
  selectedItems() {
227
236
  if (!this.isMultiMode) return []
@@ -354,6 +363,42 @@ export default {
354
363
  },
355
364
  registerOption(value, label) {
356
365
  this.labelCache = Object.assign({}, this.labelCache, { [value]: label })
366
+ },
367
+ _slotNodes() {
368
+ // Vue 3: $slots.default is a function; Vue 2: it's an array
369
+ return typeof this.$slots.default === 'function' ? this.$slots.default() : this.$slots.default || []
370
+ },
371
+ _syncSlotLabels() {
372
+ const collect = nodes => {
373
+ if (!Array.isArray(nodes)) return
374
+ nodes.forEach(vnode => {
375
+ if (!vnode) return
376
+ const props = vnode.props || (vnode.componentOptions && vnode.componentOptions.propsData)
377
+ if (props && props.value != null && props.label) {
378
+ this.registerOption(String(props.value), String(props.label))
379
+ }
380
+ if (Array.isArray(vnode.children)) collect(vnode.children)
381
+ })
382
+ }
383
+ collect(this._slotNodes())
384
+ },
385
+ _findLabelInSlot(value) {
386
+ const scan = nodes => {
387
+ if (!Array.isArray(nodes)) return null
388
+ for (const vnode of nodes) {
389
+ if (!vnode) continue
390
+ const props = vnode.props || (vnode.componentOptions && vnode.componentOptions.propsData)
391
+ if (props && String(props.value) === String(value) && props.label) {
392
+ return String(props.label)
393
+ }
394
+ if (Array.isArray(vnode.children)) {
395
+ const found = scan(vnode.children)
396
+ if (found) return found
397
+ }
398
+ }
399
+ return null
400
+ }
401
+ return scan(this._slotNodes())
357
402
  }
358
403
  }
359
404
  }
@@ -39,7 +39,7 @@ export default {
39
39
  },
40
40
 
41
41
  props: {
42
- value: { type: [String, Number], default: null },
42
+ value: { default: null },
43
43
  modelValue: { default: null },
44
44
  tabs: {
45
45
  type: Array,
package/src/index.js CHANGED
@@ -16,6 +16,7 @@ export { default as WkEmpty } from './components/empty/Empty.vue'
16
16
  export { default as WkEmptyIcon } from './components/empty-icon/EmptyIcon.vue'
17
17
  export { default as WkField } from './components/field/Field.vue'
18
18
  export { default as WkInput } from './components/input/Input.vue'
19
+ export { default as WkInputNumber } from './components/input-number/InputNumber.vue'
19
20
  export { default as WkMenu } from './components/menu/Menu.vue'
20
21
  export { default as WkPagination } from './components/pagination/Pagination.vue'
21
22
  export { default as WkRadio } from './components/radio/Radio.vue'