ux improvment

This commit is contained in:
2026-03-29 20:09:36 +03:00
parent 1cbe42ed75
commit cdaf91927b
4 changed files with 516 additions and 26 deletions

View File

@@ -0,0 +1,405 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useTemplateStore } from '../../stores/template'
import { useEditorStore } from '../../stores/editor'
import { isContainer } from '../../core/types'
import type { ContainerElement, TextStyle } from '../../core/types'
import type { ElementLayout } from '../../core/layout-types'
const props = defineProps<{
scale: number
layoutMap: Record<string, ElementLayout>
}>()
const templateStore = useTemplateStore()
const editorStore = useEditorStore()
const selected = computed(() => {
const id = editorStore.selectedElementId
if (!id || id === 'root') return null
return templateStore.getElementById(id) ?? null
})
const container = computed(() => {
const el = selected.value
return el && isContainer(el) ? el as ContainerElement : null
})
const isText = computed(() => {
const t = selected.value?.type
return t === 'static_text' || t === 'text'
})
const isLine = computed(() => selected.value?.type === 'line')
const toolbarStyle = computed(() => {
const el = selected.value
if (!el) return { display: 'none' }
const l = props.layoutMap[el.id]
if (!l) return { display: 'none' }
const s = props.scale
return {
position: 'absolute' as const,
left: `${l.x_mm * s}px`,
top: `${l.y_mm * s - 30}px`,
zIndex: 1100,
}
})
function update(updates: Record<string, unknown>) {
if (!selected.value) return
templateStore.updateElement(selected.value.id, updates as any)
}
function updateStyle(key: string, value: unknown) {
if (!selected.value) return
update({ style: { ...selected.value.style, [key]: value } })
}
// Container
function setDirection(dir: 'row' | 'column') { update({ direction: dir }) }
function setAlign(align: string) { update({ align }) }
function setJustify(justify: string) { update({ justify }) }
function setGap(e: Event) { update({ gap: parseFloat((e.target as HTMLInputElement).value) || 0 }) }
// Text
function setFontWeight(w: string) { updateStyle('fontWeight', w) }
function setTextAlign(a: string) { updateStyle('align', a) }
</script>
<template>
<div v-if="selected" class="et" :style="toolbarStyle" @pointerdown.stop>
<!-- ===== Container ===== -->
<template v-if="container">
<!-- Yön -->
<div class="et__group">
<button class="et__btn" :class="{ 'et__btn--active': container.direction === 'column' }" data-tip="Dikey" @click="setDirection('column')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<rect x="2" y="1" width="10" height="3" rx="0.5" fill="currentColor"/><rect x="2" y="5.5" width="10" height="3" rx="0.5" fill="currentColor"/><rect x="2" y="10" width="10" height="3" rx="0.5" fill="currentColor"/>
</svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': container.direction === 'row' }" data-tip="Yatay" @click="setDirection('row')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<rect x="1" y="2" width="3" height="10" rx="0.5" fill="currentColor"/><rect x="5.5" y="2" width="3" height="10" rx="0.5" fill="currentColor"/><rect x="10" y="2" width="3" height="10" rx="0.5" fill="currentColor"/>
</svg>
</button>
</div>
<div class="et__sep" />
<!-- Align -->
<div class="et__group">
<template v-if="container.direction === 'column'">
<button class="et__btn" :class="{ 'et__btn--active': container.align === 'start' }" data-tip="Sol" @click="setAlign('start')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1" y="2" width="1.5" height="10" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="3.5" y="3" width="8" height="2.5" rx="0.5" fill="currentColor"/><rect x="3.5" y="8" width="5" height="2.5" rx="0.5" fill="currentColor"/></svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': container.align === 'center' }" data-tip="Orta" @click="setAlign('center')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="6.25" y="1" width="1.5" height="12" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="3" y="3" width="8" height="2.5" rx="0.5" fill="currentColor"/><rect x="4.5" y="8" width="5" height="2.5" rx="0.5" fill="currentColor"/></svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': container.align === 'end' }" data-tip="Sag" @click="setAlign('end')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="11.5" y="2" width="1.5" height="10" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="2.5" y="3" width="8" height="2.5" rx="0.5" fill="currentColor"/><rect x="5.5" y="8" width="5" height="2.5" rx="0.5" fill="currentColor"/></svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': container.align === 'stretch' }" data-tip="Esnet" @click="setAlign('stretch')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1" y="2" width="1.5" height="10" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="11.5" y="2" width="1.5" height="10" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="3.5" y="3" width="7" height="2.5" rx="0.5" fill="currentColor"/><rect x="3.5" y="8" width="7" height="2.5" rx="0.5" fill="currentColor"/></svg>
</button>
</template>
<template v-else>
<button class="et__btn" :class="{ 'et__btn--active': container.align === 'start' }" data-tip="Ust" @click="setAlign('start')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="1" width="10" height="1.5" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="3" y="3.5" width="2.5" height="8" rx="0.5" fill="currentColor"/><rect x="8" y="3.5" width="2.5" height="5" rx="0.5" fill="currentColor"/></svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': container.align === 'center' }" data-tip="Orta" @click="setAlign('center')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1" y="6.25" width="12" height="1.5" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="3" y="2" width="2.5" height="10" rx="0.5" fill="currentColor"/><rect x="8" y="3.5" width="2.5" height="7" rx="0.5" fill="currentColor"/></svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': container.align === 'end' }" data-tip="Alt" @click="setAlign('end')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="11.5" width="10" height="1.5" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="3" y="2.5" width="2.5" height="8" rx="0.5" fill="currentColor"/><rect x="8" y="5.5" width="2.5" height="5" rx="0.5" fill="currentColor"/></svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': container.align === 'stretch' }" data-tip="Esnet" @click="setAlign('stretch')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="1" width="10" height="1.5" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="2" y="11.5" width="10" height="1.5" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="3" y="3.5" width="2.5" height="7" rx="0.5" fill="currentColor"/><rect x="8" y="3.5" width="2.5" height="7" rx="0.5" fill="currentColor"/></svg>
</button>
</template>
</div>
<div class="et__sep" />
<!-- Justify -->
<div class="et__group">
<template v-if="container.direction === 'column'">
<button class="et__btn" :class="{ 'et__btn--active': container.justify === 'start' }" data-tip="Ust" @click="setJustify('start')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="1" width="10" height="1.5" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="3" y="3.5" width="8" height="2" rx="0.5" fill="currentColor"/><rect x="3" y="6.5" width="8" height="2" rx="0.5" fill="currentColor"/></svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': container.justify === 'center' }" data-tip="Orta" @click="setJustify('center')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1" y="6.25" width="12" height="1.5" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="3" y="3" width="8" height="2" rx="0.5" fill="currentColor"/><rect x="3" y="9" width="8" height="2" rx="0.5" fill="currentColor"/></svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': container.justify === 'end' }" data-tip="Alt" @click="setJustify('end')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="11.5" width="10" height="1.5" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="3" y="5.5" width="8" height="2" rx="0.5" fill="currentColor"/><rect x="3" y="8.5" width="8" height="2" rx="0.5" fill="currentColor"/></svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': container.justify === 'space-between' }" data-tip="Esit Aralik" @click="setJustify('space-between')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="1" width="10" height="1.5" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="2" y="11.5" width="10" height="1.5" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="3" y="3.5" width="8" height="2" rx="0.5" fill="currentColor"/><rect x="3" y="8.5" width="8" height="2" rx="0.5" fill="currentColor"/></svg>
</button>
</template>
<template v-else>
<button class="et__btn" :class="{ 'et__btn--active': container.justify === 'start' }" data-tip="Sol" @click="setJustify('start')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1" y="2" width="1.5" height="10" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="3.5" y="3" width="3" height="8" rx="0.5" fill="currentColor"/><rect x="7.5" y="3" width="3" height="8" rx="0.5" fill="currentColor"/></svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': container.justify === 'center' }" data-tip="Orta" @click="setJustify('center')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="6.25" y="1" width="1.5" height="12" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="2" y="3" width="3" height="8" rx="0.5" fill="currentColor"/><rect x="9" y="3" width="3" height="8" rx="0.5" fill="currentColor"/></svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': container.justify === 'end' }" data-tip="Sag" @click="setJustify('end')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="11.5" y="2" width="1.5" height="10" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="3.5" y="3" width="3" height="8" rx="0.5" fill="currentColor"/><rect x="7.5" y="3" width="3" height="8" rx="0.5" fill="currentColor"/></svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': container.justify === 'space-between' }" data-tip="Esit Aralik" @click="setJustify('space-between')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1" y="2" width="1.5" height="10" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="11.5" y="2" width="1.5" height="10" rx="0.5" fill="currentColor" opacity="0.4"/><rect x="3.5" y="3" width="3" height="8" rx="0.5" fill="currentColor"/><rect x="7.5" y="3" width="3" height="8" rx="0.5" fill="currentColor"/></svg>
</button>
</template>
</div>
<div class="et__sep" />
<!-- Gap -->
<div class="et__group et__group--gap" data-tip="Bosluk (mm)">
<svg class="et__gap-icon" width="12" height="12" viewBox="0 0 12 12" fill="none">
<rect x="1" y="1" width="3.5" height="10" rx="0.5" stroke="currentColor" stroke-width="1" fill="none"/><rect x="7.5" y="1" width="3.5" height="10" rx="0.5" stroke="currentColor" stroke-width="1" fill="none"/><line x1="6" y1="3" x2="6" y2="9" stroke="currentColor" stroke-width="1" stroke-dasharray="1.5 1"/>
</svg>
<input type="number" class="et__num" step="1" min="0" :value="container.gap" @input="setGap" />
</div>
</template>
<!-- ===== Text / Static Text ===== -->
<template v-if="isText">
<!-- Bold -->
<div class="et__group">
<button class="et__btn" :class="{ 'et__btn--active': (selected!.style as TextStyle).fontWeight === 'bold' }" data-tip="Kalin" @click="setFontWeight((selected!.style as TextStyle).fontWeight === 'bold' ? 'normal' : 'bold')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M4 2.5h3.5a2.5 2.5 0 0 1 0 5H4V2.5z" stroke="currentColor" stroke-width="1.5" fill="none"/>
<path d="M4 7.5h4a2.5 2.5 0 0 1 0 5H4V7.5z" stroke="currentColor" stroke-width="1.5" fill="none"/>
</svg>
</button>
</div>
<div class="et__sep" />
<!-- Align -->
<div class="et__group">
<button class="et__btn" :class="{ 'et__btn--active': ((selected!.style as TextStyle).align ?? 'left') === 'left' }" data-tip="Sola Hizala" @click="setTextAlign('left')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<line x1="2" y1="3" x2="12" y2="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="2" y1="7" x2="9" y2="7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="2" y1="11" x2="11" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': (selected!.style as TextStyle).align === 'center' }" data-tip="Ortala" @click="setTextAlign('center')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<line x1="2" y1="3" x2="12" y2="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="3.5" y1="7" x2="10.5" y2="7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="2.5" y1="11" x2="11.5" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': (selected!.style as TextStyle).align === 'right' }" data-tip="Saga Hizala" @click="setTextAlign('right')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<line x1="2" y1="3" x2="12" y2="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="5" y1="7" x2="12" y2="7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="3" y1="11" x2="12" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
</div>
<div class="et__sep" />
<!-- Font size -->
<div class="et__group et__group--gap">
<svg class="et__gap-icon" width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2 10L6 2l4 8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<line x1="3.5" y1="7" x2="8.5" y2="7" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
</svg>
<input type="number" class="et__num" step="1" min="1" :value="(selected!.style as TextStyle).fontSize ?? 11" @input="(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 11)" data-tip="Yazi Boyutu (pt)" />
</div>
<div class="et__sep" />
<!-- Color -->
<div class="et__group">
<label class="et__color-wrap" data-tip="Renk">
<input type="color" class="et__color" :value="(selected!.style as TextStyle).color ?? '#000000'" @input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<rect x="2" y="11" width="10" height="2" rx="0.5" :fill="(selected!.style as TextStyle).color ?? '#000000'"/>
<path d="M5 9L7 3l2 6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<line x1="5.5" y1="7.5" x2="8.5" y2="7.5" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
</svg>
</label>
</div>
</template>
<!-- ===== Line ===== -->
<template v-if="isLine">
<!-- Stroke width -->
<div class="et__group et__group--gap">
<svg class="et__gap-icon" width="12" height="12" viewBox="0 0 12 12" fill="none">
<line x1="1" y1="6" x2="11" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<input type="number" class="et__num" step="0.1" min="0.1" :value="(selected!.style as any).strokeWidth ?? 0.5" @input="(e) => updateStyle('strokeWidth', parseFloat((e.target as HTMLInputElement).value) || 0.5)" data-tip="Kalinlik (mm)" />
</div>
<div class="et__sep" />
<!-- Color -->
<div class="et__group">
<label class="et__color-wrap" data-tip="Renk">
<input type="color" class="et__color" :value="(selected!.style as any).strokeColor ?? '#000000'" @input="(e) => updateStyle('strokeColor', (e.target as HTMLInputElement).value)" />
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<line x1="2" y1="7" x2="12" y2="7" :stroke="(selected!.style as any).strokeColor ?? '#000000'" stroke-width="2.5" stroke-linecap="round"/>
</svg>
</label>
</div>
</template>
</div>
</template>
<style scoped>
.et {
display: flex;
align-items: center;
gap: 2px;
background: #1e293b;
border-radius: 6px;
padding: 3px 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.06);
pointer-events: auto;
white-space: nowrap;
}
.et__group {
display: flex;
align-items: center;
gap: 1px;
}
.et__sep {
width: 1px;
height: 16px;
background: #334155;
margin: 0 2px;
flex-shrink: 0;
}
/* Tooltip */
[data-tip] {
position: relative;
}
[data-tip]::after {
content: attr(data-tip);
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
background: #0f172a;
color: #e2e8f0;
font-size: 10px;
padding: 3px 6px;
border-radius: 4px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
z-index: 10;
}
[data-tip]:hover::after,
[data-tip]:focus-within::after {
opacity: 1;
}
/* Button */
.et__btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border: none;
border-radius: 4px;
background: transparent;
color: #94a3b8;
cursor: pointer;
padding: 0;
transition: background 0.1s, color 0.1s;
}
.et__btn:hover {
background: #334155;
color: #e2e8f0;
}
.et__btn--active {
background: #3b82f6;
color: white;
}
.et__btn--active:hover {
background: #2563eb;
}
/* Number input */
.et__group--gap {
gap: 3px;
}
.et__gap-icon {
color: #64748b;
flex-shrink: 0;
}
.et__num {
width: 32px;
height: 22px;
border: 1px solid #334155;
border-radius: 4px;
background: #0f172a;
color: #e2e8f0;
text-align: center;
font-size: 11px;
font-family: inherit;
padding: 0;
outline: none;
-moz-appearance: textfield;
}
.et__num::-webkit-inner-spin-button,
.et__num::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.et__num:focus {
border-color: #3b82f6;
}
/* Color */
.et__color-wrap {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 4px;
cursor: pointer;
position: relative;
color: #94a3b8;
transition: background 0.1s;
}
.et__color-wrap:hover {
background: #334155;
color: #e2e8f0;
}
.et__color {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
width: 100%;
height: 100%;
}
</style>

View File

@@ -5,6 +5,7 @@ import { useEditorStore } from '../../stores/editor'
import type { ElementLayout } from '../../core/layout-types'
import type { TemplateElement, SizeValue, ContainerElement } from '../../core/types'
import { isContainer, sz } from '../../core/types'
import ElementToolbar from './ElementToolbar.vue'
const props = defineProps<{
scale: number
@@ -627,6 +628,13 @@ const isAnyDragActive = computed(() =>
<!-- Drop indicator (ortak hem eleman hem toolbox sürükleme) -->
<div v-if="isAnyDragActive" :style="dropIndicatorStyle" />
<!-- Element toolbar seçili elemanın üstünde -->
<ElementToolbar
v-if="!isDragging && !isResizing"
:scale="scale"
:layout-map="layoutMap"
/>
</div>
</template>

View File

@@ -4,6 +4,7 @@ import { useTemplateStore } from '../../stores/template'
import { useEditorStore } from '../../stores/editor'
import { useSchemaStore } from '../../stores/schema'
import { isContainer, sz } from '../../core/types'
import PaddingBox from '../properties/PaddingBox.vue'
import { schemaFormatToFormatType, defaultAlignForSchema } from '../../core/schema-parser'
import type {
TemplateElement,
@@ -580,32 +581,13 @@ function deleteElement() {
<!-- Padding -->
<div class="prop-section__subtitle">Padding (mm)</div>
<div class="prop-row-grid">
<div class="prop-row">
<label class="prop-label">Üst</label>
<input class="prop-input" type="number" step="1" min="0"
:value="(selectedElement as ContainerElement).padding.top"
@input="(e) => update({ padding: { ...(selectedElement as ContainerElement).padding, top: parseFloat((e.target as HTMLInputElement).value) || 0 } } as any)" />
</div>
<div class="prop-row">
<label class="prop-label">Sag</label>
<input class="prop-input" type="number" step="1" min="0"
:value="(selectedElement as ContainerElement).padding.right"
@input="(e) => update({ padding: { ...(selectedElement as ContainerElement).padding, right: parseFloat((e.target as HTMLInputElement).value) || 0 } } as any)" />
</div>
<div class="prop-row">
<label class="prop-label">Alt</label>
<input class="prop-input" type="number" step="1" min="0"
:value="(selectedElement as ContainerElement).padding.bottom"
@input="(e) => update({ padding: { ...(selectedElement as ContainerElement).padding, bottom: parseFloat((e.target as HTMLInputElement).value) || 0 } } as any)" />
</div>
<div class="prop-row">
<label class="prop-label">Sol</label>
<input class="prop-input" type="number" step="1" min="0"
:value="(selectedElement as ContainerElement).padding.left"
@input="(e) => update({ padding: { ...(selectedElement as ContainerElement).padding, left: parseFloat((e.target as HTMLInputElement).value) || 0 } } as any)" />
</div>
</div>
<PaddingBox
:top="(selectedElement as ContainerElement).padding.top"
:right="(selectedElement as ContainerElement).padding.right"
:bottom="(selectedElement as ContainerElement).padding.bottom"
:left="(selectedElement as ContainerElement).padding.left"
@update="(side, value) => update({ padding: { ...(selectedElement as ContainerElement).padding, [side]: value } } as any)"
/>
<!-- Container Style -->
<div class="prop-section__subtitle">Stil</div>

View File

@@ -0,0 +1,95 @@
<script setup lang="ts">
const props = defineProps<{
top: number
right: number
bottom: number
left: number
}>()
const emit = defineEmits<{
update: [side: 'top' | 'right' | 'bottom' | 'left', value: number]
}>()
function onInput(side: 'top' | 'right' | 'bottom' | 'left', e: Event) {
const val = parseFloat((e.target as HTMLInputElement).value) || 0
emit('update', side, val)
}
</script>
<template>
<div class="pb">
<span class="pb__label">Padding</span>
<div class="pb__box">
<input class="pb__in pb__in--t" type="number" step="1" min="0" :value="props.top" @input="(e) => onInput('top', e)" />
<input class="pb__in pb__in--r" type="number" step="1" min="0" :value="props.right" @input="(e) => onInput('right', e)" />
<input class="pb__in pb__in--b" type="number" step="1" min="0" :value="props.bottom" @input="(e) => onInput('bottom', e)" />
<input class="pb__in pb__in--l" type="number" step="1" min="0" :value="props.left" @input="(e) => onInput('left', e)" />
<div class="pb__center" />
</div>
</div>
</template>
<style scoped>
.pb {
display: flex;
align-items: center;
justify-content: space-between;
}
.pb__label {
font-size: 12px;
color: #475569;
flex-shrink: 0;
}
.pb__box {
position: relative;
width: 80px;
flex-shrink: 0;
height: 80px;
border: 1px solid #e2e8f0;
border-radius: 4px;
background: #f8fafc;
}
.pb__center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 1px dashed #cbd5e1;
border-radius: 2px;
}
.pb__in {
position: absolute;
width: 28px;
height: 16px;
border: none;
border-radius: 2px;
background: transparent;
text-align: center;
font-size: 10px;
color: #64748b;
padding: 0;
outline: none;
font-family: inherit;
-moz-appearance: textfield;
}
.pb__in::-webkit-inner-spin-button,
.pb__in::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.pb__in:hover { background: #f1f5f9; }
.pb__in:focus { background: white; box-shadow: 0 0 0 1px #93c5fd; }
.pb__in--t { top: 1px; left: 50%; transform: translateX(-50%); }
.pb__in--b { bottom: 1px; left: 50%; transform: translateX(-50%); }
.pb__in--l { left: 2px; top: 50%; transform: translateY(-50%); }
.pb__in--r { right: 2px; top: 50%; transform: translateY(-50%); }
</style>