webcake-ui-kit 1.0.17 → 1.0.19

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.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "UI Kit for Vue 2 && 3 - Pure Options API",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -18,6 +18,8 @@
18
18
  "./WkAccordion": "./src/components/accordion/Accordion.vue",
19
19
  "./WkAccordionItem": "./src/components/accordion-item/AccordionItem.vue",
20
20
  "./WkAlertDialog": "./src/components/alert-dialog/AlertDialog.vue",
21
+ "./WkAvatar": "./src/components/avatar/Avatar.vue",
22
+ "./WkAvatarStack": "./src/components/avatar-stack/AvatarStack.vue",
21
23
  "./WkBadge": "./src/components/badge/Badge.vue",
22
24
  "./WkBreadcrumb": "./src/components/breadcrumb/Breadcrumb.vue",
23
25
  "./WkButton": "./src/components/button/Button.vue",
@@ -26,6 +28,9 @@
26
28
  "./WkCheckboxGroup": "./src/components/checkbox-group/CheckboxGroup.vue",
27
29
  "./WkDialog": "./src/components/dialog/Dialog.vue",
28
30
  "./WkDivider": "./src/components/divider/Divider.vue",
31
+ "./WkEmpty": "./src/components/empty/Empty.vue",
32
+ "./WkEmptyIcon": "./src/components/empty-icon/EmptyIcon.vue",
33
+ "./WkField": "./src/components/field/Field.vue",
29
34
  "./WkInput": "./src/components/input/Input.vue",
30
35
  "./WkPagination": "./src/components/pagination/Pagination.vue",
31
36
  "./WkRadio": "./src/components/radio/Radio.vue",
@@ -44,6 +49,7 @@
44
49
  "./WkTag": "./src/components/tag/Tag.vue",
45
50
  "./WkToggle": "./src/components/toggle/Toggle.vue",
46
51
  "./WkToggleGroup": "./src/components/toggle-group/ToggleGroup.vue",
52
+ "./WkTooltip": "./src/components/tooltip/Tooltip.vue",
47
53
  "./WkTypography": "./src/components/typography/Typography.vue",
48
54
  "./components/*": "./src/components/*",
49
55
  "./icons": "./src/icons/index.js",
@@ -0,0 +1,54 @@
1
+ <template>
2
+ <span :class="['ui-avatar', `ui-avatar--${size}`, `ui-avatar--${roundness}`]">
3
+ <img v-if="showImage" class="ui-avatar__image" :src="src" :alt="alt" @error="onImageError" />
4
+ <span v-else class="ui-avatar__fallback">
5
+ <slot>{{ name }}</slot>
6
+ </span>
7
+ <span v-if="online" class="ui-avatar__indicator" aria-hidden="true"></span>
8
+ </span>
9
+ </template>
10
+
11
+ <script>
12
+ export default {
13
+ name: 'Avatar',
14
+ props: {
15
+ size: {
16
+ type: String,
17
+ default: 'regular',
18
+ validator: v => ['regular', 'small', 'tiny', 'extra-tiny'].includes(v)
19
+ },
20
+ roundness: {
21
+ type: String,
22
+ default: 'round',
23
+ validator: v => ['round', 'roundrect'].includes(v)
24
+ },
25
+ src: { type: String, default: '' },
26
+ alt: { type: String, default: '' },
27
+ name: { type: String, default: '' },
28
+ online: { type: Boolean, default: false }
29
+ },
30
+ emits: [],
31
+ data() {
32
+ return {
33
+ imageError: false
34
+ }
35
+ },
36
+ computed: {
37
+ showImage() {
38
+ return !!this.src && !this.imageError
39
+ }
40
+ },
41
+ watch: {
42
+ src() {
43
+ this.imageError = false
44
+ }
45
+ },
46
+ methods: {
47
+ onImageError() {
48
+ this.imageError = true
49
+ }
50
+ }
51
+ }
52
+ </script>
53
+
54
+ <style src="./avatar.css" scoped></style>
@@ -0,0 +1,91 @@
1
+ .ui-avatar {
2
+ position: relative;
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ flex-shrink: 0;
7
+ background: var(--accent-bg);
8
+ color: var(--primary-fg);
9
+ font-family: var(--font-family-body);
10
+ font-weight: var(--paragraph-bold-font-weight);
11
+ letter-spacing: var(--paragraph-small-letter-spacing);
12
+ vertical-align: middle;
13
+ overflow: visible;
14
+ }
15
+
16
+ /* Sizes — match Figma frame widths exactly */
17
+ .ui-avatar--regular {
18
+ width: 40px;
19
+ height: 40px;
20
+ font-size: var(--paragraph-small-font-size);
21
+ line-height: var(--paragraph-small-line-height);
22
+ }
23
+ .ui-avatar--small {
24
+ width: 32px;
25
+ height: 32px;
26
+ font-size: var(--paragraph-mini-font-size);
27
+ line-height: var(--paragraph-mini-line-height);
28
+ }
29
+ .ui-avatar--tiny {
30
+ width: 24px;
31
+ height: 24px;
32
+ font-size: 10px;
33
+ line-height: 16px;
34
+ }
35
+ .ui-avatar--extra-tiny {
36
+ width: 20px;
37
+ height: 20px;
38
+ font-size: 9px;
39
+ line-height: 12px;
40
+ }
41
+
42
+ /* Roundness */
43
+ .ui-avatar--round {
44
+ border-radius: var(--rounded-full);
45
+ }
46
+ .ui-avatar--roundrect {
47
+ border-radius: var(--rounded-lg);
48
+ }
49
+
50
+ /* Image */
51
+ .ui-avatar__image {
52
+ display: block;
53
+ width: 100%;
54
+ height: 100%;
55
+ object-fit: cover;
56
+ border-radius: inherit;
57
+ }
58
+
59
+ /* Fallback (initials / custom slot) */
60
+ .ui-avatar__fallback {
61
+ display: inline-flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ width: 100%;
65
+ height: 100%;
66
+ text-align: center;
67
+ user-select: none;
68
+ }
69
+
70
+ /* Online indicator — auto-sized per avatar size, 2px white ring punches out of avatar */
71
+ .ui-avatar__indicator {
72
+ position: absolute;
73
+ right: 0;
74
+ bottom: 0;
75
+ border-radius: var(--rounded-full);
76
+ background: var(--positive-500);
77
+ box-shadow: 0 0 0 2px var(--primary-bg);
78
+ }
79
+ .ui-avatar--regular .ui-avatar__indicator {
80
+ width: 10px;
81
+ height: 10px;
82
+ }
83
+ .ui-avatar--small .ui-avatar__indicator {
84
+ width: 8px;
85
+ height: 8px;
86
+ }
87
+ .ui-avatar--tiny .ui-avatar__indicator,
88
+ .ui-avatar--extra-tiny .ui-avatar__indicator {
89
+ width: 6px;
90
+ height: 6px;
91
+ }
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <span :class="['ui-avatar-stack', `ui-avatar-stack--${size}`]">
3
+ <Avatar
4
+ v-for="(item, idx) in visibleItems"
5
+ :key="`avatar-${idx}`"
6
+ :size="size"
7
+ :src="item.src || ''"
8
+ :alt="item.alt || ''"
9
+ :name="item.name || ''"
10
+ :class="itemClasses"
11
+ />
12
+ <Avatar v-if="overflowCount > 0" :size="size" :class="[...itemClasses, 'ui-avatar-stack__overflow']">
13
+ <slot name="overflow" v-bind="{ count: overflowCount }">{{ formattedOverflow }}</slot>
14
+ </Avatar>
15
+ </span>
16
+ </template>
17
+
18
+ <script>
19
+ import Avatar from '../avatar/Avatar.vue'
20
+
21
+ export default {
22
+ name: 'AvatarStack',
23
+ components: { Avatar },
24
+ props: {
25
+ size: {
26
+ type: String,
27
+ default: 'regular',
28
+ validator: v => ['regular', 'small'].includes(v)
29
+ },
30
+ items: {
31
+ type: Array,
32
+ default: () => []
33
+ },
34
+ max: {
35
+ type: Number,
36
+ default: 0
37
+ },
38
+ overflowLabel: {
39
+ type: Function,
40
+ default: null
41
+ },
42
+ animation: {
43
+ type: String,
44
+ default: 'none',
45
+ validator: v => ['none', 'pulse', 'bounce', 'ring'].includes(v)
46
+ }
47
+ },
48
+ emits: [],
49
+ computed: {
50
+ visibleItems() {
51
+ if (this.max > 0 && this.items.length > this.max) {
52
+ return this.items.slice(0, this.max)
53
+ }
54
+ return this.items
55
+ },
56
+ overflowCount() {
57
+ if (this.max > 0 && this.items.length > this.max) {
58
+ return this.items.length - this.max
59
+ }
60
+ return 0
61
+ },
62
+ formattedOverflow() {
63
+ if (typeof this.overflowLabel === 'function') {
64
+ return this.overflowLabel(this.overflowCount)
65
+ }
66
+ return '+' + this.overflowCount
67
+ },
68
+ itemClasses() {
69
+ const classes = ['ui-avatar-stack__item']
70
+ if (this.animation !== 'none') {
71
+ classes.push(`ui-avatar-stack__item--animation-${this.animation}`)
72
+ }
73
+ return classes
74
+ }
75
+ }
76
+ }
77
+ </script>
78
+
79
+ <style src="./avatar-stack.css" scoped></style>
@@ -0,0 +1,110 @@
1
+ .ui-avatar-stack {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ vertical-align: middle;
5
+ }
6
+
7
+ /* Each child avatar overlaps the previous one and shows a white ring to
8
+ "punch out" of its neighbours. Box-shadow follows border-radius and does
9
+ not take up layout space, so the avatar dimensions stay intact. */
10
+ .ui-avatar-stack__item {
11
+ position: relative;
12
+ margin-left: -8px;
13
+ box-shadow: 0 0 0 2px var(--primary-bg);
14
+ }
15
+ .ui-avatar-stack__item:first-child {
16
+ margin-left: 0;
17
+ }
18
+
19
+ /* Stacking order: earlier siblings sit above later ones so the leftmost
20
+ avatar's ring is visible over its neighbour. */
21
+ .ui-avatar-stack__item {
22
+ z-index: 1;
23
+ }
24
+ .ui-avatar-stack__item:nth-child(1) {
25
+ z-index: 5;
26
+ }
27
+ .ui-avatar-stack__item:nth-child(2) {
28
+ z-index: 4;
29
+ }
30
+ .ui-avatar-stack__item:nth-child(3) {
31
+ z-index: 3;
32
+ }
33
+ .ui-avatar-stack__item:nth-child(4) {
34
+ z-index: 2;
35
+ }
36
+
37
+ /* Overflow chip — same shape as siblings, slightly darker background. */
38
+ .ui-avatar-stack__overflow {
39
+ background: var(--secondary-bg);
40
+ color: var(--secondary-fg);
41
+ }
42
+
43
+ /* ============================ Animations ============================
44
+ Three loop animations the consumer opts into via the `animation` prop
45
+ on AvatarStack. Each animation is applied to every visible avatar
46
+ child (including the "+N" overflow chip). All honour
47
+ `prefers-reduced-motion: reduce`. */
48
+
49
+ @keyframes ui-avatar-stack-pulse {
50
+ 0%,
51
+ 100% {
52
+ transform: scale(1);
53
+ opacity: 1;
54
+ }
55
+ 50% {
56
+ transform: scale(0.94);
57
+ opacity: 0.78;
58
+ }
59
+ }
60
+
61
+ @keyframes ui-avatar-stack-bounce {
62
+ 0%,
63
+ 100% {
64
+ transform: translateY(0);
65
+ }
66
+ 50% {
67
+ transform: translateY(-4px);
68
+ }
69
+ }
70
+
71
+ @keyframes ui-avatar-stack-ring {
72
+ 0% {
73
+ transform: scale(1);
74
+ opacity: 0.8;
75
+ }
76
+ 100% {
77
+ transform: scale(1.35);
78
+ opacity: 0;
79
+ }
80
+ }
81
+
82
+ .ui-avatar-stack__item--animation-pulse {
83
+ animation: ui-avatar-stack-pulse 1.6s ease-in-out infinite;
84
+ }
85
+
86
+ .ui-avatar-stack__item--animation-bounce {
87
+ animation: ui-avatar-stack-bounce 1.2s ease-in-out infinite;
88
+ }
89
+
90
+ .ui-avatar-stack__item--animation-ring::after {
91
+ content: '';
92
+ position: absolute;
93
+ inset: -3px;
94
+ border-radius: inherit;
95
+ border: 2px solid var(--positive-500);
96
+ pointer-events: none;
97
+ animation: ui-avatar-stack-ring 1.6s ease-out infinite;
98
+ }
99
+
100
+ @media (prefers-reduced-motion: reduce) {
101
+ .ui-avatar-stack__item--animation-pulse,
102
+ .ui-avatar-stack__item--animation-bounce {
103
+ animation: none;
104
+ }
105
+ .ui-avatar-stack__item--animation-ring::after {
106
+ animation: none;
107
+ opacity: 0.6;
108
+ transform: scale(1.05);
109
+ }
110
+ }
@@ -43,7 +43,7 @@
43
43
  letter-spacing: var(--paragraph-mini-letter-spacing);
44
44
  }
45
45
  .ui-btn--xs .ui-btn__icon,
46
- .ui-btn--xs :deep(svg) {
46
+ .ui-btn--xs ::v-deep svg {
47
47
  width: 16px;
48
48
  height: 16px;
49
49
  }
@@ -57,7 +57,7 @@
57
57
  letter-spacing: var(--paragraph-small-letter-spacing);
58
58
  }
59
59
  .ui-btn--sm .ui-btn__icon,
60
- .ui-btn--sm :deep(svg) {
60
+ .ui-btn--sm ::v-deep svg {
61
61
  width: 20px;
62
62
  height: 20px;
63
63
  }
@@ -71,7 +71,7 @@
71
71
  letter-spacing: var(--paragraph-small-letter-spacing);
72
72
  }
73
73
  .ui-btn--md .ui-btn__icon,
74
- .ui-btn--md :deep(svg) {
74
+ .ui-btn--md ::v-deep svg {
75
75
  width: 20px;
76
76
  height: 20px;
77
77
  }
@@ -85,7 +85,7 @@
85
85
  letter-spacing: var(--paragraph-small-letter-spacing);
86
86
  }
87
87
  .ui-btn--lg .ui-btn__icon,
88
- .ui-btn--lg :deep(svg) {
88
+ .ui-btn--lg ::v-deep svg {
89
89
  width: 20px;
90
90
  height: 20px;
91
91
  }
@@ -99,7 +99,7 @@
99
99
  letter-spacing: var(--paragraph-regular-letter-spacing);
100
100
  }
101
101
  .ui-btn--xl .ui-btn__icon,
102
- .ui-btn--xl :deep(svg) {
102
+ .ui-btn--xl ::v-deep svg {
103
103
  width: 20px;
104
104
  height: 20px;
105
105
  }
@@ -24,7 +24,6 @@
24
24
  .ui-divider--vertical {
25
25
  display: inline-block;
26
26
  width: 1px;
27
- align-self: stretch;
28
27
  }
29
28
 
30
29
  .ui-divider--vertical.ui-divider--spacing-regular {
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <div :class="['ui-empty', `ui-empty--${variant}`]">
3
+ <div class="ui-empty__content">
4
+ <div v-if="hasMedia" class="ui-empty__media">
5
+ <slot name="media"></slot>
6
+ </div>
7
+ <div v-if="hasTextBlock" class="ui-empty__text">
8
+ <p v-if="hasTitle" class="ui-empty__title">
9
+ <slot name="title">{{ title }}</slot>
10
+ </p>
11
+ <p v-if="hasDescription" class="ui-empty__description">
12
+ <slot name="description">{{ description }}</slot>
13
+ </p>
14
+ </div>
15
+ <div v-if="hasFooter" class="ui-empty__footer">
16
+ <slot></slot>
17
+ </div>
18
+ </div>
19
+ </div>
20
+ </template>
21
+
22
+ <script>
23
+ export default {
24
+ name: 'Empty',
25
+ props: {
26
+ variant: {
27
+ type: String,
28
+ default: 'default',
29
+ validator: v => ['default', 'outline', 'background', 'outline-dashed'].includes(v)
30
+ },
31
+ title: { type: String, default: '' },
32
+ description: { type: String, default: '' }
33
+ },
34
+ emits: [],
35
+ computed: {
36
+ hasMedia() {
37
+ return !!((this.$scopedSlots && this.$scopedSlots.media) || this.$slots.media)
38
+ },
39
+ hasTitle() {
40
+ return !!this.title || !!((this.$scopedSlots && this.$scopedSlots.title) || this.$slots.title)
41
+ },
42
+ hasDescription() {
43
+ return !!this.description || !!((this.$scopedSlots && this.$scopedSlots.description) || this.$slots.description)
44
+ },
45
+ hasTextBlock() {
46
+ return this.hasTitle || this.hasDescription
47
+ },
48
+ hasFooter() {
49
+ return !!((this.$scopedSlots && this.$scopedSlots.default) || this.$slots.default)
50
+ }
51
+ }
52
+ }
53
+ </script>
54
+
55
+ <style src="./empty.css" scoped></style>
@@ -0,0 +1,80 @@
1
+ .ui-empty {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: stretch;
5
+ padding: var(--spacing-2xl);
6
+ border-radius: var(--rounded-2xl);
7
+ width: 100%;
8
+ box-sizing: border-box;
9
+ }
10
+
11
+ /* Variants */
12
+ .ui-empty--default {
13
+ background: transparent;
14
+ border: 0;
15
+ }
16
+ .ui-empty--outline {
17
+ background: transparent;
18
+ border: 1px solid var(--border-primary);
19
+ }
20
+ .ui-empty--background {
21
+ background: var(--accent-bg);
22
+ border: 0;
23
+ }
24
+ .ui-empty--outline-dashed {
25
+ background: transparent;
26
+ border: 1px dashed var(--border-primary);
27
+ }
28
+
29
+ /* Inner content stack */
30
+ .ui-empty__content {
31
+ display: flex;
32
+ flex-direction: column;
33
+ align-items: center;
34
+ gap: var(--spacing-md);
35
+ width: 100%;
36
+ }
37
+
38
+ .ui-empty__media {
39
+ display: flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ }
43
+
44
+ .ui-empty__text {
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: var(--spacing-2xs);
48
+ text-align: center;
49
+ width: 100%;
50
+ }
51
+
52
+ .ui-empty__title {
53
+ margin: 0;
54
+ font-family: var(--font-family-body);
55
+ font-size: var(--paragraph-regular-font-size);
56
+ line-height: var(--paragraph-regular-line-height);
57
+ letter-spacing: var(--paragraph-regular-letter-spacing);
58
+ font-weight: var(--paragraph-medium-font-weight);
59
+ color: var(--primary-fg);
60
+ }
61
+
62
+ .ui-empty__description {
63
+ margin: 0;
64
+ font-family: var(--font-family-body);
65
+ font-size: var(--paragraph-small-font-size);
66
+ line-height: var(--paragraph-small-line-height);
67
+ letter-spacing: var(--paragraph-small-letter-spacing);
68
+ font-weight: var(--paragraph-font-weight);
69
+ color: var(--muted-fg);
70
+ }
71
+
72
+ /* Footer slot — stacks action group(s) below the text block.
73
+ Consumer composes button rows / link rows freely; gap matches the
74
+ outer content stack so a single child OR multiple children line up. */
75
+ .ui-empty__footer {
76
+ display: flex;
77
+ flex-direction: column;
78
+ align-items: center;
79
+ gap: var(--spacing-md);
80
+ }
@@ -0,0 +1,16 @@
1
+ <template>
2
+ <span class="ui-empty-icon">
3
+ <span class="ui-empty-icon__frame">
4
+ <slot></slot>
5
+ </span>
6
+ </span>
7
+ </template>
8
+
9
+ <script>
10
+ export default {
11
+ name: 'EmptyIcon',
12
+ emits: []
13
+ }
14
+ </script>
15
+
16
+ <style src="./empty-icon.css" scoped></style>
@@ -0,0 +1,30 @@
1
+ .ui-empty-icon {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ width: 40px;
6
+ height: 40px;
7
+ padding: var(--spacing-xs);
8
+ background: var(--secondary-bg);
9
+ border-radius: var(--rounded-lg);
10
+ box-sizing: border-box;
11
+ flex-shrink: 0;
12
+ }
13
+
14
+ /* Inner frame fixes the icon at the Figma-specified 24x24. Consumers
15
+ place their <svg> (or <img>) directly in the default slot; the frame
16
+ normalises sizing so different icons line up. */
17
+ .ui-empty-icon__frame {
18
+ display: inline-flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ width: 24px;
22
+ height: 24px;
23
+ color: var(--primary-fg);
24
+ }
25
+
26
+ .ui-empty-icon__frame ::v-deep svg {
27
+ width: 100%;
28
+ height: 100%;
29
+ display: block;
30
+ }
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <div
3
+ :class="[
4
+ 'ui-field',
5
+ `ui-field--${layout}`,
6
+ layout === 'horizontal' && `ui-field--align-${align}`,
7
+ required && 'ui-field--required',
8
+ isError && 'ui-field--error'
9
+ ]"
10
+ >
11
+ <div
12
+ v-if="hasLabel"
13
+ class="ui-field__label"
14
+ :style="layout === 'horizontal' ? { flexBasis: normalizedLabelWidth, width: normalizedLabelWidth } : null"
15
+ >
16
+ <slot name="label">{{ label }}</slot>
17
+ </div>
18
+ <div class="ui-field__body">
19
+ <div class="ui-field__control">
20
+ <slot></slot>
21
+ </div>
22
+ <div v-if="hasMessage" class="ui-field__message">
23
+ <span class="ui-field__message-icon" aria-hidden="true">
24
+ <slot name="message-icon">
25
+ <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
26
+ <circle cx="8" cy="8" r="6.25" stroke="currentColor" stroke-width="1.3" />
27
+ <path d="M8 7.25v3.75" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
28
+ <circle cx="8" cy="5.25" r="0.75" fill="currentColor" />
29
+ </svg>
30
+ </slot>
31
+ </span>
32
+ <span class="ui-field__message-text">
33
+ <slot name="message">{{ isError ? errorText : helpText }}</slot>
34
+ </span>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </template>
39
+
40
+ <script>
41
+ export default {
42
+ name: 'Field',
43
+ props: {
44
+ layout: {
45
+ type: String,
46
+ default: 'vertical',
47
+ validator: v => ['vertical', 'horizontal'].includes(v)
48
+ },
49
+ label: { type: String, default: '' },
50
+ required: { type: Boolean, default: false },
51
+ helpText: { type: String, default: '' },
52
+ errorText: { type: String, default: '' },
53
+ labelWidth: { type: [String, Number], default: '120px' },
54
+ align: {
55
+ type: String,
56
+ default: 'center',
57
+ validator: v => ['start', 'center'].includes(v)
58
+ }
59
+ },
60
+ emits: [],
61
+ computed: {
62
+ hasLabel() {
63
+ return !!this.label || !!((this.$scopedSlots && this.$scopedSlots.label) || this.$slots.label)
64
+ },
65
+ isError() {
66
+ return !!this.errorText
67
+ },
68
+ hasMessage() {
69
+ return (
70
+ !!this.errorText ||
71
+ !!this.helpText ||
72
+ !!((this.$scopedSlots && this.$scopedSlots.message) || this.$slots.message)
73
+ )
74
+ },
75
+ normalizedLabelWidth() {
76
+ return typeof this.labelWidth === 'number' ? `${this.labelWidth}px` : this.labelWidth
77
+ }
78
+ }
79
+ }
80
+ </script>
81
+
82
+ <style src="./field.css" scoped></style>