change repo structure

This commit is contained in:
lana-k
2026-01-15 21:53:12 +01:00
parent 85b5a200e2
commit 7edc196a02
64 changed files with 74 additions and 74 deletions
+115
View File
@@ -0,0 +1,115 @@
<template>
<div
:class="[
'checkbox-container',
{ checked: checked },
{ disabled: disabled }
]"
@click.stop="onClick"
>
<div v-show="!checked" class="unchecked" />
<img
v-show="checked && !disabled && theme === 'light'"
class="checked-light"
src="~@/assets/images/checkbox_checked_light.svg"
/>
<img
v-show="checked && !disabled && theme !== 'light'"
class="checked"
src="~@/assets/images/checkbox_checked.svg"
/>
<img
v-show="checked && disabled"
class="checked-disabled"
src="~@/assets/images/checkbox_checked_disabled.svg"
/>
<span v-if="label" class="label">{{ label }}</span>
</div>
</template>
<script>
export default {
name: 'CheckBox',
props: {
theme: {
type: String,
required: false,
default: 'accent',
validator: value => {
return ['accent', 'light'].includes(value)
}
},
init: {
type: Boolean,
required: false,
default: false
},
label: {
type: String,
required: false,
default: ''
},
disabled: {
type: Boolean,
required: false,
default: false
}
},
emits: ['click'],
data() {
return {
checked: this.init
}
},
methods: {
onClick() {
if (!this.disabled) {
this.checked = !this.checked
this.$emit('click', this.checked)
}
}
}
}
</script>
<style scoped>
.checkbox-container {
display: inline-flex;
cursor: pointer;
}
.unchecked {
width: 18px;
height: 18px;
background-color: white;
border: 1px solid var(--color-border);
border-radius: var(--border-radius-medium);
box-sizing: border-box;
}
.unchecked:hover {
background-color: var(--color-bg-light);
}
img {
display: block;
}
.label {
margin-left: 6px;
color: var(--color-text-base);
}
.checked .label {
color: var(--color-text-active);
}
.disabled.checkbox-container {
cursor: default;
}
.disabled .label {
color: var(--color-text-light-2);
}
.disabled .unchecked,
.disabled .unchecked:hover {
background-color: var(--color-bg-light-2);
}
</style>
+124
View File
@@ -0,0 +1,124 @@
<template>
<button
:class="['icon-btn', { active }]"
:disabled="disabled"
@click="onClick"
@mouseenter="showTooltip($event, tooltipPosition)"
@mouseleave="hideTooltip"
>
<div class="icon"><slot /></div>
<div v-show="loading" class="icon-in-progress">
<loading-indicator />
</div>
<span
v-if="tooltip"
ref="tooltip"
class="icon-tooltip"
:style="tooltipStyle"
>
{{ tooltip }}
</span>
</button>
</template>
<script>
import tooltipMixin from '@/tooltipMixin'
import LoadingIndicator from '@/components/Common/LoadingIndicator'
export default {
name: 'SideBarButton',
components: { LoadingIndicator },
mixins: [tooltipMixin],
props: {
active: Boolean,
disabled: Boolean,
tooltip: String,
tooltipPosition: String,
loading: Boolean
},
emits: ['click'],
methods: {
onClick() {
this.hideTooltip()
this.$emit('click')
}
}
}
</script>
<style scoped>
.icon-btn {
box-sizing: border-box;
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
background-color: transparent;
border: none;
}
.icon-btn:hover {
border: 1px solid var(--color-border);
border-radius: var(--border-radius-medium-2);
}
.icon-btn:hover .icon :deep(path),
.icon-btn.active .icon :deep(path),
.icon-btn:hover .icon :deep(circle),
.icon-btn.active .icon :deep(circle) {
fill: var(--color-accent);
}
.icon-btn:disabled .icon :deep(path),
.icon-btn:disabled .icon :deep(circle) {
fill: var(--color-border);
}
.icon-btn:disabled {
cursor: default;
pointer-events: none;
}
.disabled.icon-btn:hover .icon :deep(path) {
fill: var(--color-border);
}
.icon-in-progress {
position: absolute;
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-bg-light);
will-change: opacity;
/*
We need to show loader in 1 sec after starting query execution. We can't do that with
setTimeout because the main thread can be busy by getting a result set from the web worker.
But we can use CSS animation for opacity. Opacity triggers changes only in the Composite Layer
stage in rendering waterfall. Hence it can be processed only with Compositor Thread while
the Main Thread processes a result set.
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
*/
animation: show-loader 1s linear 0s 1;
}
@keyframes show-loader {
0% {
opacity: 0;
}
99% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.icon {
display: flex;
align-items: center;
justify-content: center;
}
</style>
+125
View File
@@ -0,0 +1,125 @@
<template>
<modal
v-model="show"
class="dialog"
:clickToClose="false"
:contentTransition="{ name: 'loading-dialog' }"
:overlayTransition="{ name: 'loading-dialog' }"
@update:model-value="$emit('update:modelValue', $event)"
>
<div class="dialog-header">
{{ title }}
<close-icon :disabled="loading" @click="cancel" />
</div>
<div class="dialog-body">
<div v-if="loading" class="loading-dialog-body">
<loading-indicator :size="30" class="state-icon" />
{{ loadingMsg }}
</div>
<div v-else class="loading-dialog-body">
<img
src="~@/assets/images/success.svg"
class="success-icon state-icon"
/>
{{ successMsg }}
</div>
</div>
<div class="dialog-buttons-container">
<button
class="secondary"
type="button"
:disabled="loading"
@click="cancel"
>
Cancel
</button>
<button
class="primary"
type="button"
:disabled="loading"
@click="$emit('action')"
>
{{ actionBtnName }}
</button>
</div>
</modal>
</template>
<script>
import LoadingIndicator from '@/components/Common/LoadingIndicator'
import CloseIcon from '@/components/svg/close'
export default {
name: 'LoadingDialog',
components: { LoadingIndicator, CloseIcon },
props: {
modelValue: Boolean,
loadingMsg: String,
successMsg: String,
actionBtnName: String,
title: String,
loading: Boolean
},
emits: ['cancel', 'action', 'update:modelValue'],
data() {
return {
show: this.modelValue
}
},
watch: {
modelValue() {
this.show = this.modelValue
},
loading() {
if (this.loading) {
this.$emit('update:modelValue', true)
}
}
},
methods: {
cancel() {
this.$emit('cancel')
this.$emit('update:modelValue', false)
}
}
}
</script>
<style>
.loading-dialog-enter-active {
animation: show-modal 1s linear 0s 1;
}
.loading-dialog-leave-active {
opacity: 0;
}
@keyframes show-modal {
0% {
opacity: 0;
}
99% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.loading-modal {
width: 400px;
}
</style>
<style scoped>
.loading-dialog-body {
display: flex;
align-items: center;
}
.success-icon {
width: 30px;
}
.state-icon {
margin-right: 8px;
}
</style>
+129
View File
@@ -0,0 +1,129 @@
<template>
<svg
:class="animationClass"
:height="size"
:width="size"
:viewBox="`0 0 ${size} ${size}`"
>
<circle
class="loader-svg bg"
:style="{ strokeWidth }"
:cx="size / 2"
:cy="size / 2"
:r="radius"
/>
<circle
class="loader-svg front"
:style="{
strokeDasharray: circleProgress,
strokeDashoffset: offset,
strokeWidth
}"
:cx="size / 2"
:cy="size / 2"
:r="radius"
/>
</svg>
</template>
<script>
export default {
name: 'LoadingIndicator',
props: {
progress: {
type: Number,
required: false
},
size: {
type: Number,
required: false,
default: 20
}
},
emits: [],
computed: {
circleProgress() {
const circle = this.radius * 3.14 * 2
const dash = this.progress
? (circle * this.progress) / 100
: (circle * 1) / 3
const space = circle - dash
return `${dash}px, ${space}px`
},
animationClass() {
return this.progress === undefined ? 'loading' : 'progress'
},
radius() {
return this.size / 2 - this.strokeWidth
},
offset() {
return (this.radius * 3.14) / 2
},
strokeWidth() {
return this.size / 10
}
}
}
</script>
<style scoped>
.loader-svg {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
fill: none;
stroke-linecap: round;
stroke: var(--color-accent);
}
.loader-svg.bg {
stroke: var(--color-gray-light-3);
}
.loading .loader-svg.front {
will-change: transform;
animation: fill-animation-loading 1s cubic-bezier(1, 1, 1, 1) 0s infinite;
transform-origin: center;
}
/*
We can't change anything in loading animation except transform, opacity and filter. Because in
our case the Main Thread can be busy and animation will be frozen (e. g. getting a result set
from the web-worker after query execution).
But transform, opacity and filter trigger changes only in the Composite Layer stage in rendering
waterfall. Hence they can be processed only with Compositor Thread while the Main Thread
processes something else.
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
*/
@keyframes fill-animation-loading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.progress .loader-svg.front {
transition: stroke-dasharray 0.2s;
}
.progress .loader-svg.bg {
animation: bg-animation 1.5s cubic-bezier(1, 1, 1, 1) 0s infinite;
}
@keyframes bg-animation {
0% {
r: 8;
}
50% {
stroke: var(--color-gray-light-2);
r: 9;
}
100% {
r: 8;
}
}
</style>
+94
View File
@@ -0,0 +1,94 @@
<template>
<div ref="logsContainer" class="logs-container">
<div v-for="(msg, index) in messages" :key="index" class="msg">
<img v-if="msg.type === 'error'" src="~@/assets/images/error.svg" />
<img
v-if="msg.type === 'info'"
src="~@/assets/images/info.svg"
width="20px"
/>
<img v-if="msg.type === 'success'" src="~@/assets/images/success.svg" />
<loading-indicator
v-if="msg.type === 'loading'"
:progress="msg.progress"
/>
<span class="msg-text">{{ serializeMessage(msg) }}</span>
</div>
</div>
</template>
<script>
import LoadingIndicator from '@/components/Common/LoadingIndicator'
export default {
name: 'Logs',
components: { LoadingIndicator },
props: { messages: Array },
emits: [],
watch: {
'messages.length': 'scrollToBottom'
},
mounted() {
this.scrollToBottom()
},
methods: {
async scrollToBottom() {
const container = this.$refs.logsContainer
if (container) {
await this.$nextTick()
container.scrollTop = container.scrollHeight
}
},
serializeMessage(msg) {
let result = ''
if (msg.row !== null && msg.row !== undefined) {
if (msg.type === 'error') {
result += `Error in row ${msg.row}. `
} else {
result += `Information about row ${msg.row}. `
}
}
result += msg.message
if (!/(\.|!|\?)$/.test(result)) {
result += '.'
}
if (msg.hint) {
result += ` ${msg.hint}`
}
return result
}
}
}
</script>
<style scoped>
.logs-container {
background-color: var(--color-white);
padding: 0 5px;
border-radius: 5px;
border: 1px solid var(--color-border-light);
box-sizing: border-box;
overflow-y: auto;
color: var(--color-text-base);
}
.msg {
padding: 12px 7px;
border-bottom: 1px solid var(--color-border-light);
display: flex;
align-items: flex-start;
font-family: monospace;
font-size: 13px;
}
.msg:last-child {
border-bottom: none;
}
.msg-text {
margin-left: 6px;
margin-top: 2px;
}
</style>
+109
View File
@@ -0,0 +1,109 @@
<template>
<paginate
v-model="page"
:pageCount="pageCount"
:pageRange="5"
:marginPages="1"
:prevText="chevron"
:nextText="chevron"
:noLiSurround="true"
containerClass="paginator-continer"
pageLinkClass="paginator-page-link"
activeClass="paginator-active-page"
breakViewLinkClass="paginator-break"
nextLinkClass="paginator-next"
prevLinkClass="paginator-prev"
disabledClass="paginator-disabled"
/>
</template>
<script>
import Paginate from 'vuejs-paginate-next'
export default {
name: 'Pager',
components: { Paginate },
props: {
pageCount: Number,
modelValue: Number
},
emits: ['update:modelValue'],
data() {
return {
page: this.modelValue,
chevron: `
<svg width="9" height="9" viewBox="0 0 8 12" fill="none">
<path
d="M0.721924 9.93097L4.85292 5.79997L0.721924 1.66897L1.99992 0.399973L7.39992
5.79997L1.99992 11.2L0.721924 9.93097Z" fill="#506784"
/>
</svg>
`
}
},
watch: {
page() {
this.$emit('update:modelValue', this.page)
},
modelValue() {
this.page = this.modelValue
}
}
}
</script>
<style scoped>
.paginator-continer {
display: flex;
align-items: center;
line-height: 10px;
}
:deep(a) {
cursor: pointer;
}
:deep(.paginator-page-link) {
padding: 2px 3px;
margin: 0 5px;
display: block;
color: var(--color-text-base);
font-size: 11px;
}
:deep(.paginator-page-link:hover) {
color: var(--color-text-active);
}
:deep(.paginator-page-link:active),
:deep(.paginator-page-link:visited),
:deep(.paginator-page-link:focus),
:deep(.paginator-next:active),
:deep(.paginator-next:visited),
:deep(.paginator-next:focus),
:deep(.paginator-prev:active),
:deep(.paginator-prev:visited),
:deep(.paginator-prev:focus) {
outline: none;
}
:deep(.paginator-active-page),
:deep(.paginator-active-page:hover) {
color: var(--color-accent);
}
:deep(.paginator-break:hover),
:deep(.paginator-disabled:hover) {
cursor: default;
}
:deep(.paginator-prev svg) {
transform: rotate(180deg);
}
:deep(.paginator-next:hover path),
:deep(.paginator-prev:hover path) {
fill: var(--color-text-active);
}
:deep(.paginator-disabled path),
:deep(.paginator-disabled:hover path) {
fill: var(--color-text-light-2);
}
</style>
+394
View File
@@ -0,0 +1,394 @@
<template>
<div
ref="container"
:class="[
'splitpanes',
`splitpanes-${horizontal ? 'horizontal' : 'vertical'}`,
{ 'splitpanes-dragging': dragging }
]"
>
<div
ref="movableSplitter"
class="movable-splitter"
:style="movableSplitterStyle"
/>
<div
ref="left"
class="splitpanes-pane"
:size="paneBefore.size"
max-size="30"
:style="styles.before"
>
<slot name="left-pane" />
</div>
<!-- Splitter start-->
<div
class="splitpanes-splitter"
@mousedown="bindEvents"
@touchstart="bindEvents"
>
<div
:class="[
'toggle-btns',
{
both:
after.max === 100 &&
before.max === 100 &&
paneAfter.size > 0 &&
paneBefore.size > 0
}
]"
>
<div
v-if="after.max === 100 && paneAfter.size > 0"
class="toggle-btn"
@click="togglePane(paneBefore)"
>
<img
class="direction-icon"
src="~@/assets/images/chevron.svg"
:style="directionBeforeIconStyle"
/>
</div>
<div
v-if="before.max === 100 && paneBefore.size > 0"
class="toggle-btn"
@click="togglePane(paneAfter)"
>
<img
class="direction-icon"
src="~@/assets/images/chevron.svg"
:style="directionAfterIconStyle"
/>
</div>
</div>
</div>
<!-- splitter end -->
<div ref="right" class="splitpanes-pane" :style="styles.after">
<slot name="right-pane" />
</div>
</div>
</template>
<script>
import splitter from './splitter'
export default {
name: 'Splitpanes',
props: {
horizontal: { type: Boolean, default: false },
before: { type: Object },
after: { type: Object },
default: {
type: Object,
default: () => {
return {
before: 50,
after: 50
}
}
}
},
emits: [],
data() {
return {
container: null,
paneBefore: this.before,
paneAfter: this.after,
beforeMinimising:
!this.after.size || !this.before.size
? this.default
: {
before: this.before.size,
after: this.after.size
},
dragging: false,
movableSplitter: {
top: 0,
left: 0,
visibility: 'hidden'
}
}
},
computed: {
styles() {
return {
before: {
[this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%`
},
after: {
[this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%`
}
}
},
movableSplitterStyle() {
const style = { ...this.movableSplitter }
style.top += '%'
style.left += '%'
return style
},
directionBeforeIconStyle() {
const expanded = this.paneBefore.size !== 0
const translation = 'translate(-50%, -50%) '
let rotation = ''
if (this.horizontal) {
rotation = expanded ? 'rotate(90deg)' : 'rotate(-90deg)'
} else {
rotation = expanded ? 'rotate(0deg)' : 'rotate(180deg)'
}
return {
transform: translation + rotation
}
},
directionAfterIconStyle() {
const expanded = this.paneAfter.size !== 0
const translation = 'translate(-50%, -50%)'
let rotation = ''
if (this.horizontal) {
rotation = expanded ? 'rotate(-90deg)' : 'rotate(90deg)'
} else {
rotation = expanded ? 'rotate(180deg)' : 'rotate(0deg)'
}
return {
transform: translation + rotation
}
}
},
mounted() {
this.container = this.$refs.container
},
methods: {
bindEvents() {
// Passive: false to prevent scrolling while touch dragging.
document.addEventListener('mousemove', this.onMouseMove, {
passive: false
})
document.addEventListener('mouseup', this.onMouseUp)
if ('ontouchstart' in window) {
document.addEventListener('touchmove', this.onMouseMove, {
passive: false
})
document.addEventListener('touchend', this.onMouseUp)
}
},
unbindEvents() {
document.removeEventListener('mousemove', this.onMouseMove, {
passive: false
})
document.removeEventListener('mouseup', this.onMouseUp)
if ('ontouchstart' in window) {
document.removeEventListener('touchmove', this.onMouseMove, {
passive: false
})
document.removeEventListener('touchend', this.onMouseUp)
}
},
onMouseMove(event) {
event.preventDefault()
this.dragging = true
this.movableSplitter.visibility = 'visible'
this.moveSplitter(event)
},
onMouseUp() {
if (this.dragging) {
const dragPercentage = this.horizontal
? this.movableSplitter.top
: this.movableSplitter.left
this.paneBefore.size = dragPercentage
this.paneAfter.size = 100 - dragPercentage
this.movableSplitter = {
top: 0,
left: 0,
visibility: 'hidden'
}
this.dragging = false
}
this.unbindEvents()
},
moveSplitter(event) {
const splitterInfo = {
container: this.container,
paneBeforeMax: this.paneBefore.max,
paneAfterMax: this.paneAfter.max,
isHorisontal: this.horizontal
}
const offset = splitter.calculateOffset(event, splitterInfo)
const dir = this.horizontal ? 'top' : 'left'
this.movableSplitter[dir] = offset
},
togglePane(pane) {
if (pane.size > 0) {
this.beforeMinimising.before = this.paneBefore.size
this.beforeMinimising.after = this.paneAfter.size
pane.size = 0
const otherPane =
pane === this.paneBefore ? this.paneAfter : this.paneBefore
otherPane.size = 100 - pane.size
} else {
this.paneBefore.size = this.beforeMinimising.before
this.paneAfter.size = this.beforeMinimising.after
}
}
}
}
</script>
<style>
.splitpanes {
display: flex;
height: 100%;
position: relative;
}
.splitpanes-vertical {
flex-direction: row;
}
.splitpanes-horizontal {
flex-direction: column;
}
.splitpanes-dragging * {
user-select: none;
}
.splitpanes-pane {
width: 100%;
height: 100%;
overflow: auto;
}
/* Splitter */
.splitpanes-vertical > .splitpanes-splitter,
.splitpanes-vertical.splitpanes-dragging {
cursor: col-resize;
}
.splitpanes-horizontal > .splitpanes-splitter,
.splitpanes-horizontal.splitpanes-dragging {
cursor: row-resize;
}
.splitpanes-splitter {
touch-action: none;
background-color: var(--color-bg-light);
box-sizing: border-box;
position: relative;
flex-shrink: 0;
z-index: 1;
}
.splitpanes-horizontal > .splitpanes-splitter {
border-top: 1px solid var(--color-border-light);
border-bottom: 1px solid var(--color-border-light);
}
.splitpanes-vertical > .splitpanes-splitter {
border-left: 1px solid var(--color-border-light);
border-right: 1px solid var(--color-border-light);
}
.movable-splitter {
position: absolute;
background-color: rgba(162, 177, 198, 0.5);
}
.splitpanes-vertical > .splitpanes-splitter,
.splitpanes-vertical > .movable-splitter {
width: 8px;
z-index: 5;
height: 100%;
}
.splitpanes-horizontal > .splitpanes-splitter,
.splitpanes-horizontal > .movable-splitter {
height: 8px;
width: 100%;
z-index: 5;
}
/* Toggle buttons */
.toggle-btns {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
}
.splitpanes-vertical > .splitpanes-splitter .toggle-btns {
flex-direction: column;
}
.splitpanes-horizontal > .splitpanes-splitter .toggle-btns {
flex-direction: row;
}
.toggle-btn {
background-color: var(--color-border-light);
border-radius: var(--border-radius-small);
border: 1px solid var(--color-border);
box-sizing: border-box;
position: relative;
cursor: pointer;
}
.splitpanes-vertical > .splitpanes-splitter .toggle-btn {
height: 49px;
width: 8px;
}
.splitpanes-horizontal > .splitpanes-splitter .toggle-btn {
width: 49px;
height: 8px;
}
.toggle-btn .direction-icon {
position: absolute;
top: 50%;
left: 50%;
}
.splitpanes-horizontal
> .splitpanes-splitter
.toggle-btns.both
.toggle-btn:first-child {
border-radius: var(--border-radius-small) 0 0 var(--border-radius-small);
}
.splitpanes-horizontal
> .splitpanes-splitter
.toggle-btns.both
.toggle-btn:last-child {
border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0;
margin-left: -1px;
}
.splitpanes-vertical
> .splitpanes-splitter
.toggle-btns.both
.toggle-btn:first-child {
border-radius: var(--border-radius-small) var(--border-radius-small) 0 0;
}
.splitpanes-vertical
> .splitpanes-splitter
.toggle-btns.both
.toggle-btn:last-child {
border-radius: 0 0 var(--border-radius-small) var(--border-radius-small);
margin-top: -1px;
}
</style>
@@ -0,0 +1,47 @@
export default {
// Get the cursor position relative to the splitpane container.
getCurrentMouseDrag(event, container) {
const rect = container.getBoundingClientRect()
const { clientX, clientY } =
'ontouchstart' in window && event.touches ? event.touches[0] : event
return {
x: clientX - rect.left,
y: clientY - rect.top
}
},
// Returns the drag percentage of the splitter relative to the 2 panes it's inbetween.
getCurrentDragPercentage(event, container, isHorisontal) {
let drag = this.getCurrentMouseDrag(event, container)
drag = drag[isHorisontal ? 'y' : 'x']
const containerSize =
container[isHorisontal ? 'clientHeight' : 'clientWidth']
return (drag * 100) / containerSize
},
// Returns the new position in percents.
calculateOffset(
event,
{ container, isHorisontal, paneBeforeMax, paneAfterMax }
) {
const dragPercentage = this.getCurrentDragPercentage(
event,
container,
isHorisontal
)
const paneBeforeMaxReached =
paneBeforeMax < 100 && dragPercentage >= paneBeforeMax
const paneAfterMaxReached =
paneAfterMax < 100 && dragPercentage <= 100 - paneAfterMax
// Prevent dragging beyond pane max.
if (paneBeforeMaxReached || paneAfterMaxReached) {
return paneBeforeMaxReached
? paneBeforeMax
: Math.max(100 - paneAfterMax, 0)
} else {
return Math.min(Math.max(dragPercentage, 0), paneBeforeMax)
}
}
}
+111
View File
@@ -0,0 +1,111 @@
<template>
<div>
<div
v-if="label"
:class="['text-field-label', { error: errorMsg }, { disabled: disabled }]"
>
{{ label }}
<hint-icon
v-if="hint"
class="hint"
:hint="hint"
:maxWidth="maxHintWidth || '149px'"
/>
</div>
<input
type="text"
:placeholder="placeholder"
:class="{ error: errorMsg }"
:style="{ width: width }"
:value="modelValue"
:disabled="disabled"
@input="$emit('update:modelValue', $event.target.value)"
/>
<div v-show="errorMsg" class="text-field-error">{{ errorMsg }}</div>
</div>
</template>
<script>
import HintIcon from '@/components/svg/hint'
export default {
name: 'TextField',
components: { HintIcon },
props: {
placeholder: String,
label: String,
errorMsg: String,
modelValue: String,
width: String,
hint: String,
maxHintWidth: String,
disabled: Boolean
},
emits: ['update:modelValue']
}
</script>
<style scoped>
input {
background: var(--color-white);
border: 1px solid var(--color-border);
color: var(--color-text-base);
border-radius: var(--border-radius-medium-2);
height: 36px;
padding: 0 8px;
font-size: 13px;
box-sizing: border-box;
display: block;
}
input::placeholder {
color: var(--color-text-light-2);
}
input:focus {
outline: none;
}
input:disabled {
background: var(--color-bg-light);
color: var(--color-text-light-2);
cursor: default;
}
input.error {
border-color: var(--color-text-error);
}
.text-field-label {
font-size: 12px;
color: var(--color-text-base);
padding-left: 8px;
margin-bottom: 2px;
display: inline-block;
position: relative;
}
.text-field-label .hint {
position: absolute;
top: -2px;
right: -22px;
}
.text-field-label.error {
color: var(--color-text-error);
}
.text-field-label.disabled {
color: var(--color-text-light-2);
}
.text-field-error {
color: var(--color-text-error);
font-size: 12px;
padding-left: 8px;
margin-top: 2px;
position: absolute;
}
.text-field-error:first-letter {
text-transform: uppercase;
}
</style>