mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-01 18:39:16 +00:00
refactor
This commit is contained in:
File diff suppressed because it is too large
Load Diff
53
frontend/src/components/editor/toolbars/ChartToolbar.vue
Normal file
53
frontend/src/components/editor/toolbars/ChartToolbar.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChartElement, ChartType } from '../../../core/types'
|
||||
|
||||
defineProps<{ chart: ChartElement }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [updates: Record<string, unknown>]
|
||||
updateStyle: [key: string, value: unknown]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Chart type -->
|
||||
<div class="et__group">
|
||||
<button class="et__btn" :class="{ 'et__btn--active': chart.chartType === 'bar' }" data-tip="Cubuk" @click="emit('update', { chartType: 'bar' as ChartType })">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="6" width="3" height="6" rx="0.5" fill="currentColor" /><rect x="5.5" y="3" width="3" height="9" rx="0.5" fill="currentColor" /><rect x="9" y="5" width="3" height="7" rx="0.5" fill="currentColor" /></svg>
|
||||
</button>
|
||||
<button class="et__btn" :class="{ 'et__btn--active': chart.chartType === 'line' }" data-tip="Cizgi" @click="emit('update', { chartType: 'line' as ChartType })">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><polyline points="2,10 5,5 8,7 12,3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none" /><circle cx="2" cy="10" r="1.2" fill="currentColor" /><circle cx="5" cy="5" r="1.2" fill="currentColor" /><circle cx="8" cy="7" r="1.2" fill="currentColor" /><circle cx="12" cy="3" r="1.2" fill="currentColor" /></svg>
|
||||
</button>
|
||||
<button class="et__btn" :class="{ 'et__btn--active': chart.chartType === 'pie' }" data-tip="Pasta" @click="emit('update', { chartType: 'pie' as ChartType })">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 2a5 5 0 1 1-3.54 1.46" stroke="currentColor" stroke-width="1.3" fill="none" /><path d="M7 7V2A5 5 0 0 0 3.46 3.46Z" fill="currentColor" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="et__sep" />
|
||||
|
||||
<!-- Show labels -->
|
||||
<div class="et__group">
|
||||
<button class="et__btn" :class="{ 'et__btn--active': chart.labels?.show !== false }" data-tip="Etiketler" @click="emit('update', { labels: { ...chart.labels, show: chart.labels?.show === false ? true : false } })">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="8" width="3" height="4" rx="0.5" fill="currentColor" opacity="0.4" /><rect x="5.5" y="5" width="3" height="7" rx="0.5" fill="currentColor" opacity="0.4" /><rect x="9" y="6" width="3" height="6" rx="0.5" fill="currentColor" opacity="0.4" /><text x="3.5" y="7" font-size="4" fill="currentColor" text-anchor="middle" font-weight="bold">3</text><text x="7" y="4" font-size="4" fill="currentColor" text-anchor="middle" font-weight="bold">7</text><text x="10.5" y="5" font-size="4" fill="currentColor" text-anchor="middle" font-weight="bold">5</text></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="et__sep" />
|
||||
|
||||
<!-- Show grid -->
|
||||
<div class="et__group">
|
||||
<button class="et__btn" :class="{ 'et__btn--active': chart.axis?.showGrid !== false }" data-tip="Izgara" @click="emit('update', { axis: { ...chart.axis, showGrid: chart.axis?.showGrid === false ? true : false } })">
|
||||
<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="0.8" stroke-dasharray="2 1.5" /><line x1="2" y1="7" x2="12" y2="7" stroke="currentColor" stroke-width="0.8" stroke-dasharray="2 1.5" /><line x1="2" y1="11" x2="12" y2="11" stroke="currentColor" stroke-width="0.8" stroke-dasharray="2 1.5" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="et__sep" />
|
||||
|
||||
<!-- Background color -->
|
||||
<div class="et__group">
|
||||
<label class="et__color-wrap" data-tip="Arka Plan">
|
||||
<input type="color" class="et__color" :value="chart.style.backgroundColor ?? '#ffffff'" @input="(e) => emit('updateStyle', 'backgroundColor', (e.target as HTMLInputElement).value)" />
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="2" width="10" height="10" rx="1.5" :fill="chart.style.backgroundColor ?? '#ffffff'" stroke="#94a3b8" stroke-width="0.8" /></svg>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
97
frontend/src/components/editor/toolbars/ContainerToolbar.vue
Normal file
97
frontend/src/components/editor/toolbars/ContainerToolbar.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContainerElement } from '../../../core/types'
|
||||
|
||||
const props = defineProps<{ container: ContainerElement }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [updates: Record<string, unknown>]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Direction -->
|
||||
<div class="et__group">
|
||||
<button class="et__btn" :class="{ 'et__btn--active': container.direction === 'column' }" data-tip="Dikey" @click="emit('update', { direction: '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="emit('update', { direction: '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="emit('update', { align: '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="emit('update', { align: '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="emit('update', { align: '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="emit('update', { align: '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="emit('update', { align: '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="emit('update', { align: '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="emit('update', { align: '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="emit('update', { align: '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="emit('update', { justify: '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="emit('update', { justify: '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="emit('update', { justify: '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="emit('update', { justify: '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="emit('update', { justify: '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="emit('update', { justify: '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="emit('update', { justify: '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="emit('update', { justify: '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="(e) => emit('update', { gap: parseFloat((e.target as HTMLInputElement).value) || 0 })" />
|
||||
</div>
|
||||
</template>
|
||||
51
frontend/src/components/editor/toolbars/TableToolbar.vue
Normal file
51
frontend/src/components/editor/toolbars/TableToolbar.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import type { TableStyle } from '../../../core/types'
|
||||
|
||||
defineProps<{ tableStyle: TableStyle }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
updateStyle: [key: string, value: unknown]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Font size -->
|
||||
<div class="et__group et__group--gap" data-tip="Yazi Boyutu">
|
||||
<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="6" :value="tableStyle.fontSize ?? 10" @input="(e) => emit('updateStyle', 'fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)" />
|
||||
</div>
|
||||
|
||||
<div class="et__sep" />
|
||||
|
||||
<!-- Header bg color -->
|
||||
<div class="et__group">
|
||||
<label class="et__color-wrap" data-tip="Header Rengi">
|
||||
<input type="color" class="et__color" :value="tableStyle.headerBg ?? '#f0f0f0'" @input="(e) => emit('updateStyle', 'headerBg', (e.target as HTMLInputElement).value)" />
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="2" width="10" height="4" rx="1" :fill="tableStyle.headerBg ?? '#f0f0f0'" stroke="#94a3b8" stroke-width="0.5" /><rect x="2" y="7" width="10" height="2" rx="0.5" fill="none" stroke="#94a3b8" stroke-width="0.5" /><rect x="2" y="10" width="10" height="2" rx="0.5" fill="none" stroke="#94a3b8" stroke-width="0.5" /></svg>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Zebra color -->
|
||||
<div class="et__group">
|
||||
<label class="et__color-wrap" data-tip="Zebra Rengi">
|
||||
<input type="color" class="et__color" :value="tableStyle.zebraOdd ?? '#fafafa'" @input="(e) => emit('updateStyle', 'zebraOdd', (e.target as HTMLInputElement).value)" />
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="2" width="10" height="2.5" rx="0.5" fill="none" stroke="#94a3b8" stroke-width="0.5" /><rect x="2" y="5.5" width="10" height="2.5" rx="0.5" :fill="tableStyle.zebraOdd ?? '#fafafa'" stroke="#94a3b8" stroke-width="0.5" /><rect x="2" y="9" width="10" height="2.5" rx="0.5" fill="none" stroke="#94a3b8" stroke-width="0.5" /></svg>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="et__sep" />
|
||||
|
||||
<!-- Border color -->
|
||||
<div class="et__group">
|
||||
<label class="et__color-wrap" data-tip="Kenarlik Rengi">
|
||||
<input type="color" class="et__color" :value="tableStyle.borderColor ?? '#cccccc'" @input="(e) => emit('updateStyle', 'borderColor', (e.target as HTMLInputElement).value)" />
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="2" width="10" height="10" rx="1" fill="none" :stroke="tableStyle.borderColor ?? '#cccccc'" stroke-width="1.5" /><line x1="2" y1="6" x2="12" y2="6" :stroke="tableStyle.borderColor ?? '#cccccc'" stroke-width="0.8" /><line x1="7" y1="2" x2="7" y2="12" :stroke="tableStyle.borderColor ?? '#cccccc'" stroke-width="0.8" /></svg>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Border width -->
|
||||
<div class="et__group et__group--gap" data-tip="Kenarlik (mm)">
|
||||
<svg class="et__gap-icon" width="12" height="12" viewBox="0 0 12 12" fill="none"><rect x="1" y="1" width="10" height="10" rx="1" fill="none" stroke="currentColor" stroke-width="1.5" /></svg>
|
||||
<input type="number" class="et__num" step="0.1" min="0" :value="tableStyle.borderWidth ?? 0.5" @input="(e) => emit('updateStyle', 'borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
</div>
|
||||
</template>
|
||||
52
frontend/src/components/editor/toolbars/TextToolbar.vue
Normal file
52
frontend/src/components/editor/toolbars/TextToolbar.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import type { TextStyle, TemplateElement } from '../../../core/types'
|
||||
|
||||
const props = defineProps<{ element: TemplateElement }>()
|
||||
const style = () => props.element.style as TextStyle
|
||||
|
||||
const emit = defineEmits<{
|
||||
updateStyle: [key: string, value: unknown]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Bold -->
|
||||
<div class="et__group">
|
||||
<button class="et__btn" :class="{ 'et__btn--active': style().fontWeight === 'bold' }" data-tip="Kalin" @click="emit('updateStyle', 'fontWeight', style().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': (style().align ?? 'left') === 'left' }" data-tip="Sola Hizala" @click="emit('updateStyle', 'align', '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': style().align === 'center' }" data-tip="Ortala" @click="emit('updateStyle', 'align', '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': style().align === 'right' }" data-tip="Saga Hizala" @click="emit('updateStyle', 'align', '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="style().fontSize ?? 11" @input="(e) => emit('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="style().color ?? '#000000'" @input="(e) => emit('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="style().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>
|
||||
@@ -1,25 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||
import { useSchemaStore } from '../../stores/schema'
|
||||
import type { BarcodeElement, BarcodeFormat, TemplateElement } from '../../core/types'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropSelect from './shared/PropSelect.vue'
|
||||
import PropColorInput from './shared/PropColorInput.vue'
|
||||
import PropCheckbox from './shared/PropCheckbox.vue'
|
||||
import PropFieldSelect from './shared/PropFieldSelect.vue'
|
||||
import type { BarcodeElement, BarcodeFormat } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: BarcodeElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||
const schemaStore = useSchemaStore()
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
update({ style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
|
||||
}
|
||||
const formatOptions = [
|
||||
{ value: 'qr', label: 'QR Kod' },
|
||||
{ value: 'ean13', label: 'EAN-13' },
|
||||
{ value: 'ean8', label: 'EAN-8' },
|
||||
{ value: 'code128', label: 'Code 128' },
|
||||
{ value: 'code39', label: 'Code 39' },
|
||||
]
|
||||
|
||||
const barcodeDefaults: Record<BarcodeFormat, string> = {
|
||||
qr: 'https://example.com',
|
||||
@@ -73,7 +74,6 @@ watch(
|
||||
function onBarcodeValueInput(e: Event) {
|
||||
const val = (e.target as HTMLInputElement).value
|
||||
barcodeInputValue.value = val
|
||||
|
||||
if (validateBarcode(props.element.format, val)) {
|
||||
barcodeInputInvalid.value = false
|
||||
update({ value: val } as any)
|
||||
@@ -82,38 +82,29 @@ function onBarcodeValueInput(e: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
function onBarcodeFormatChange(newFormat: BarcodeFormat) {
|
||||
function onBarcodeFormatChange(newFormat: string) {
|
||||
const fmt = newFormat as BarcodeFormat
|
||||
const currentValue = props.element.value ?? ''
|
||||
if (validateBarcode(newFormat, currentValue)) {
|
||||
update({ format: newFormat } as any)
|
||||
if (validateBarcode(fmt, currentValue)) {
|
||||
update({ format: fmt } as any)
|
||||
} else {
|
||||
const defaultVal = barcodeDefaults[newFormat]
|
||||
const defaultVal = barcodeDefaults[fmt]
|
||||
barcodeInputValue.value = defaultVal
|
||||
barcodeInputInvalid.value = false
|
||||
update({ format: newFormat, value: defaultVal } as any)
|
||||
update({ format: fmt, value: defaultVal } as any)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Barkod Ayarlari</div>
|
||||
<div class="prop-row" data-tip="Barkod formati">
|
||||
<label class="prop-label">Format</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.format"
|
||||
@change="
|
||||
(e) => onBarcodeFormatChange((e.target as HTMLSelectElement).value as BarcodeFormat)
|
||||
"
|
||||
>
|
||||
<option value="qr">QR Kod</option>
|
||||
<option value="ean13">EAN-13</option>
|
||||
<option value="ean8">EAN-8</option>
|
||||
<option value="code128">Code 128</option>
|
||||
<option value="code39">Code 39</option>
|
||||
</select>
|
||||
</div>
|
||||
<PropSection title="Barkod Ayarlari">
|
||||
<PropSelect
|
||||
label="Format"
|
||||
:model-value="element.format"
|
||||
:options="formatOptions"
|
||||
data-tip="Barkod formati"
|
||||
@update:model-value="onBarcodeFormatChange"
|
||||
/>
|
||||
<div class="prop-row" data-tip="Barkod icerigi — formata uygun olmali">
|
||||
<label class="prop-label">Deger</label>
|
||||
<input
|
||||
@@ -124,63 +115,36 @@ function onBarcodeFormatChange(newFormat: BarcodeFormat) {
|
||||
@input="onBarcodeValueInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Barkod cizgi/modül rengi">
|
||||
<label class="prop-label">Renk</label>
|
||||
<div class="prop-row-inline">
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.color ?? '#000000'"
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button
|
||||
v-if="element.style.color"
|
||||
class="prop-clear"
|
||||
@click="updateStyle('color', undefined)"
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
<PropColorInput
|
||||
label="Renk"
|
||||
:model-value="element.style.color ?? '#000000'"
|
||||
:clearable="true"
|
||||
data-tip="Barkod cizgi/modul rengi"
|
||||
@update:model-value="(v) => updateStyle('color', v)"
|
||||
/>
|
||||
<PropCheckbox
|
||||
v-if="element.format !== 'qr'"
|
||||
class="prop-row"
|
||||
label="Metin Goster"
|
||||
:model-value="
|
||||
element.style.includeText ?? (element.format === 'ean13' || element.format === 'ean8')
|
||||
"
|
||||
data-tip="Barkod altinda degeri metin olarak goster"
|
||||
>
|
||||
<label class="prop-label">Metin Goster</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="
|
||||
element.style.includeText ?? (element.format === 'ean13' || element.format === 'ean8')
|
||||
"
|
||||
@change="(e) => updateStyle('includeText', (e.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@update:model-value="(v) => updateStyle('includeText', v)"
|
||||
/>
|
||||
<PropFieldSelect
|
||||
v-if="schemaStore.scalarFields.length > 0"
|
||||
class="prop-row"
|
||||
label="Veri Baglama"
|
||||
:model-value="element.binding?.path ?? ''"
|
||||
:fields="schemaStore.scalarFields"
|
||||
:allow-empty="true"
|
||||
empty-label="Yok (statik deger)"
|
||||
data-tip="Schema'dan dinamik veri baglama"
|
||||
>
|
||||
<label class="prop-label">Veri Baglama</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.binding?.path ?? ''"
|
||||
@change="
|
||||
(e) => {
|
||||
const val = (e.target as HTMLSelectElement).value
|
||||
if (val) {
|
||||
update({ binding: { type: 'scalar', path: val } } as any)
|
||||
} else {
|
||||
update({ binding: undefined } as any)
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<option value="">Yok (statik deger)</option>
|
||||
<option v-for="field in schemaStore.scalarFields" :key="field.path" :value="field.path">
|
||||
{{ field.title }} ({{ field.path }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
if (v) update({ binding: { type: 'scalar', path: v } } as any)
|
||||
else update({ binding: undefined } as any)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
@@ -1,32 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import type { CalculatedTextElement, TextStyle, TemplateElement } from '../../core/types'
|
||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropSelect from './shared/PropSelect.vue'
|
||||
import PropTextStyleGroup from './shared/PropTextStyleGroup.vue'
|
||||
import DexprEditor from '../common/DexprEditor.vue'
|
||||
import type { CalculatedTextElement, TextStyle } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: CalculatedTextElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||
const style = () => props.element.style as TextStyle
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
update({ style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
|
||||
}
|
||||
|
||||
function onExpressionChange(value: string) {
|
||||
update({ expression: value } as any)
|
||||
}
|
||||
const formatOptions = [
|
||||
{ value: '', label: 'Yok' },
|
||||
{ value: 'currency', label: 'Para Birimi' },
|
||||
{ value: 'number', label: 'Sayi' },
|
||||
{ value: 'percentage', label: 'Yuzde' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Hesaplanan Metin</div>
|
||||
<PropSection title="Hesaplanan Metin">
|
||||
<div
|
||||
class="prop-row-stack"
|
||||
data-tip="Hesaplama ifadesi (orn: toplamlar.kdv + toplamlar.araToplam)"
|
||||
@@ -34,69 +28,26 @@ function onExpressionChange(value: string) {
|
||||
<label class="prop-label">Ifade</label>
|
||||
<DexprEditor
|
||||
:model-value="element.expression"
|
||||
@update:model-value="onExpressionChange"
|
||||
@update:model-value="(v) => update({ expression: v } as any)"
|
||||
placeholder="toplamlar.kdv + toplamlar.araToplam"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Sonucun gosterim formati">
|
||||
<label class="prop-label">Format</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.format ?? ''"
|
||||
@change="
|
||||
(e) => update({ format: (e.target as HTMLSelectElement).value || undefined } as any)
|
||||
"
|
||||
>
|
||||
<option value="">Yok</option>
|
||||
<option value="currency">Para Birimi</option>
|
||||
<option value="number">Sayi</option>
|
||||
<option value="percentage">Yuzde</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Yazi tipi boyutu (point)">
|
||||
<label class="prop-label">Boyut (pt)</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="(element.style as TextStyle).fontSize ?? 11"
|
||||
@input="
|
||||
(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 11)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metin rengi">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="(element.style as TextStyle).color ?? '#000000'"
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Yazi tipi kalinligi">
|
||||
<label class="prop-label">Kalinlik</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="(element.style as TextStyle).fontWeight ?? 'normal'"
|
||||
@change="(e) => updateStyle('fontWeight', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="normal">Normal</option>
|
||||
<option value="bold">Kalin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metnin yatay hizalamasi">
|
||||
<label class="prop-label">Hizalama</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="(element.style as TextStyle).align ?? 'left'"
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="left">Sol</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="right">Sag</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<PropSelect
|
||||
label="Format"
|
||||
:model-value="element.format ?? ''"
|
||||
:options="formatOptions"
|
||||
data-tip="Sonucun gosterim formati"
|
||||
@update:model-value="(v) => update({ format: v || undefined } as any)"
|
||||
/>
|
||||
<PropTextStyleGroup
|
||||
:font-size="style().fontSize ?? 11"
|
||||
:font-weight="style().fontWeight ?? 'normal'"
|
||||
:color="style().color ?? '#000000'"
|
||||
:align="style().align ?? 'left'"
|
||||
@update:font-size="(v) => updateStyle('fontSize', v)"
|
||||
@update:font-weight="(v) => updateStyle('fontWeight', v)"
|
||||
@update:color="(v) => updateStyle('color', v)"
|
||||
@update:align="(v) => updateStyle('align', v)"
|
||||
/>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
@@ -1,32 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||
import { useSchemaStore } from '../../stores/schema'
|
||||
import type { ChartElement, ChartType, GroupMode, TemplateElement } from '../../core/types'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropSelect from './shared/PropSelect.vue'
|
||||
import PropNumberInput from './shared/PropNumberInput.vue'
|
||||
import PropColorInput from './shared/PropColorInput.vue'
|
||||
import PropCheckbox from './shared/PropCheckbox.vue'
|
||||
import PropFieldSelect from './shared/PropFieldSelect.vue'
|
||||
import type { ChartElement, ChartType, GroupMode } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: ChartElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
const { update, updateStyle, updateNested } = usePropertyUpdate(() => props.element)
|
||||
const schemaStore = useSchemaStore()
|
||||
|
||||
function update(updates: Partial<ChartElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates as Partial<TemplateElement>)
|
||||
}
|
||||
const chartTypeOptions = [
|
||||
{ value: 'bar', label: 'Bar' },
|
||||
{ value: 'line', label: 'Line' },
|
||||
{ value: 'pie', label: 'Pie' },
|
||||
]
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
const newStyle = { ...props.element.style, [key]: value }
|
||||
if (value === undefined || value === '') delete (newStyle as Record<string, unknown>)[key]
|
||||
update({ style: newStyle })
|
||||
}
|
||||
const groupModeOptions = [
|
||||
{ value: 'grouped', label: 'Yan Yana' },
|
||||
{ value: 'stacked', label: 'Yigin' },
|
||||
]
|
||||
|
||||
// Schema'daki array alanlari
|
||||
const arrayFields = computed(() => schemaStore.arrayFields)
|
||||
const alignOptions = [
|
||||
{ value: 'left', label: 'Sol' },
|
||||
{ value: 'center', label: 'Orta' },
|
||||
{ value: 'right', label: 'Sag' },
|
||||
]
|
||||
|
||||
const legendPositionOptions = [
|
||||
{ value: 'top', label: 'Ust' },
|
||||
{ value: 'bottom', label: 'Alt' },
|
||||
{ value: 'right', label: 'Sag' },
|
||||
]
|
||||
|
||||
// Secili array'in item alanlari
|
||||
const itemFields = computed(() => {
|
||||
const path = props.element.dataSource?.path
|
||||
if (!path) return []
|
||||
@@ -38,6 +49,15 @@ const numberFields = computed(() =>
|
||||
itemFields.value.filter((f) => f.type === 'number' || f.type === 'integer'),
|
||||
)
|
||||
|
||||
const isPie = computed(() => props.element.chartType === 'pie')
|
||||
const hasGroup = computed(() => !!props.element.groupField)
|
||||
|
||||
const colorList = computed(() => {
|
||||
return (
|
||||
props.element.style.colors ?? ['#4F46E5', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899']
|
||||
)
|
||||
})
|
||||
|
||||
function updateDataSource(path: string) {
|
||||
const fields = schemaStore.getArrayItemFields(path)
|
||||
const strField = fields.find((f) => f.type === 'string')
|
||||
@@ -47,39 +67,9 @@ function updateDataSource(path: string) {
|
||||
categoryField: strField?.key ?? fields[0]?.key ?? '',
|
||||
valueField: numField?.key ?? fields[1]?.key ?? '',
|
||||
groupField: undefined,
|
||||
})
|
||||
} as any)
|
||||
}
|
||||
|
||||
function updateTitle(key: string, value: unknown) {
|
||||
const current = props.element.title ?? { text: '' }
|
||||
update({ title: { ...current, [key]: value } })
|
||||
}
|
||||
|
||||
function updateLegend(key: string, value: unknown) {
|
||||
const current = props.element.legend ?? { show: false }
|
||||
update({ legend: { ...current, [key]: value } })
|
||||
}
|
||||
|
||||
function updateLabels(key: string, value: unknown) {
|
||||
const current = props.element.labels ?? { show: false }
|
||||
update({ labels: { ...current, [key]: value } })
|
||||
}
|
||||
|
||||
function updateAxis(key: string, value: unknown) {
|
||||
const current = props.element.axis ?? {}
|
||||
update({ axis: { ...current, [key]: value } })
|
||||
}
|
||||
|
||||
const isPie = computed(() => props.element.chartType === 'pie')
|
||||
const hasGroup = computed(() => !!props.element.groupField)
|
||||
|
||||
// Renk paleti (default 6 renk)
|
||||
const colorList = computed(() => {
|
||||
return (
|
||||
props.element.style.colors ?? ['#4F46E5', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899']
|
||||
)
|
||||
})
|
||||
|
||||
function updateColor(index: number, value: string) {
|
||||
const colors = [...colorList.value]
|
||||
colors[index] = value
|
||||
@@ -87,8 +77,7 @@ function updateColor(index: number, value: string) {
|
||||
}
|
||||
|
||||
function addColor() {
|
||||
const colors = [...colorList.value, '#6B7280']
|
||||
updateStyle('colors', colors)
|
||||
updateStyle('colors', [...colorList.value, '#6B7280'])
|
||||
}
|
||||
|
||||
function removeColor(index: number) {
|
||||
@@ -100,218 +89,140 @@ function removeColor(index: number) {
|
||||
<template>
|
||||
<div class="chart-properties">
|
||||
<!-- Grafik Tipi -->
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Grafik Tipi</div>
|
||||
<div class="prop-row">
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.chartType"
|
||||
@change="update({ chartType: ($event.target as HTMLSelectElement).value as ChartType })"
|
||||
>
|
||||
<option value="bar">Bar</option>
|
||||
<option value="line">Line</option>
|
||||
<option value="pie">Pie</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<PropSection title="Grafik Tipi">
|
||||
<PropSelect
|
||||
label=""
|
||||
:model-value="element.chartType"
|
||||
:options="chartTypeOptions"
|
||||
@update:model-value="(v) => update({ chartType: v as ChartType } as any)"
|
||||
/>
|
||||
</PropSection>
|
||||
|
||||
<!-- Veri Kaynagi -->
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Veri Kaynagi</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Array</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.dataSource?.path ?? ''"
|
||||
@change="updateDataSource(($event.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="" disabled>Sec...</option>
|
||||
<option v-for="arr in arrayFields" :key="arr.path" :value="arr.path">
|
||||
{{ arr.title || arr.path }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Kategori</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.categoryField"
|
||||
@change="update({ categoryField: ($event.target as HTMLSelectElement).value })"
|
||||
>
|
||||
<option v-for="f in itemFields" :key="f.key" :value="f.key">
|
||||
{{ f.title || f.key }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Deger</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.valueField"
|
||||
@change="update({ valueField: ($event.target as HTMLSelectElement).value })"
|
||||
>
|
||||
<option v-for="f in numberFields" :key="f.key" :value="f.key">
|
||||
{{ f.title || f.key }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Gruplama</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.groupField ?? ''"
|
||||
@change="update({ groupField: ($event.target as HTMLSelectElement).value || undefined })"
|
||||
>
|
||||
<option value="">Yok</option>
|
||||
<option v-for="f in stringFields" :key="f.key" :value="f.key">
|
||||
{{ f.title || f.key }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="hasGroup && !isPie" class="prop-row">
|
||||
<label class="prop-label">Grup Modu</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.groupMode ?? 'grouped'"
|
||||
@change="update({ groupMode: ($event.target as HTMLSelectElement).value as GroupMode })"
|
||||
>
|
||||
<option value="grouped">Yan Yana</option>
|
||||
<option value="stacked">Yigin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<PropSection title="Veri Kaynagi">
|
||||
<PropFieldSelect
|
||||
label="Array"
|
||||
:model-value="element.dataSource?.path ?? ''"
|
||||
:fields="schemaStore.arrayFields"
|
||||
placeholder="Sec..."
|
||||
@update:model-value="updateDataSource"
|
||||
/>
|
||||
<PropFieldSelect
|
||||
label="Kategori"
|
||||
:model-value="element.categoryField"
|
||||
:fields="itemFields"
|
||||
@update:model-value="(v) => update({ categoryField: v } as any)"
|
||||
/>
|
||||
<PropFieldSelect
|
||||
label="Deger"
|
||||
:model-value="element.valueField"
|
||||
:fields="numberFields"
|
||||
@update:model-value="(v) => update({ valueField: v } as any)"
|
||||
/>
|
||||
<PropFieldSelect
|
||||
label="Gruplama"
|
||||
:model-value="element.groupField ?? ''"
|
||||
:fields="stringFields"
|
||||
:allow-empty="true"
|
||||
empty-label="Yok"
|
||||
@update:model-value="(v) => update({ groupField: v || undefined } as any)"
|
||||
/>
|
||||
<PropSelect
|
||||
v-if="hasGroup && !isPie"
|
||||
label="Grup Modu"
|
||||
:model-value="element.groupMode ?? 'grouped'"
|
||||
:options="groupModeOptions"
|
||||
@update:model-value="(v) => update({ groupMode: v as GroupMode } as any)"
|
||||
/>
|
||||
</PropSection>
|
||||
|
||||
<!-- Baslik -->
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Baslik</div>
|
||||
<PropSection title="Baslik">
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Metin</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="text"
|
||||
:value="element.title?.text ?? ''"
|
||||
@change="updateTitle('text', ($event.target as HTMLInputElement).value)"
|
||||
@change="(e) => updateNested('title', 'text', (e.target as HTMLInputElement).value, { text: '' })"
|
||||
placeholder="Grafik basligi"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" v-if="element.title?.text">
|
||||
<label class="prop-label">Boyut</label>
|
||||
<input
|
||||
class="prop-input prop-input--sm"
|
||||
type="number"
|
||||
:value="element.title?.fontSize ?? 4"
|
||||
step="0.5"
|
||||
@change="updateTitle('fontSize', parseFloat(($event.target as HTMLInputElement).value))"
|
||||
<template v-if="element.title?.text">
|
||||
<PropNumberInput
|
||||
label="Boyut"
|
||||
:model-value="element.title?.fontSize ?? 4"
|
||||
:step="0.5"
|
||||
@update:model-value="(v) => updateNested('title', 'fontSize', v, { text: '' })"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" v-if="element.title?.text">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input
|
||||
class="prop-color"
|
||||
type="color"
|
||||
:value="element.title?.color ?? '#333333'"
|
||||
@input="updateTitle('color', ($event.target as HTMLInputElement).value)"
|
||||
<PropColorInput
|
||||
label="Renk"
|
||||
:model-value="element.title?.color ?? '#333333'"
|
||||
@update:model-value="(v) => updateNested('title', 'color', v, { text: '' })"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" v-if="element.title?.text">
|
||||
<label class="prop-label">Hiza</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.title?.align ?? 'center'"
|
||||
@change="updateTitle('align', ($event.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="left">Sol</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="right">Sag</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<PropSelect
|
||||
label="Hiza"
|
||||
:model-value="element.title?.align ?? 'center'"
|
||||
:options="alignOptions"
|
||||
@update:model-value="(v) => updateNested('title', 'align', v, { text: '' })"
|
||||
/>
|
||||
</template>
|
||||
</PropSection>
|
||||
|
||||
<!-- Gosterge (Legend) -->
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Gosterge</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Goster</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="element.legend?.show ?? false"
|
||||
@change="updateLegend('show', ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
</div>
|
||||
<PropSection title="Gosterge">
|
||||
<PropCheckbox
|
||||
label="Goster"
|
||||
:model-value="element.legend?.show ?? false"
|
||||
@update:model-value="(v) => updateNested('legend', 'show', v, { show: false })"
|
||||
/>
|
||||
<template v-if="element.legend?.show">
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Konum</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.legend?.position ?? 'bottom'"
|
||||
@change="updateLegend('position', ($event.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="top">Ust</option>
|
||||
<option value="bottom">Alt</option>
|
||||
<option value="right">Sag</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Boyut</label>
|
||||
<input
|
||||
class="prop-input prop-input--sm"
|
||||
type="number"
|
||||
:value="element.legend?.fontSize ?? 2.8"
|
||||
step="0.2"
|
||||
@change="
|
||||
updateLegend('fontSize', parseFloat(($event.target as HTMLInputElement).value))
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<PropSelect
|
||||
label="Konum"
|
||||
:model-value="element.legend?.position ?? 'bottom'"
|
||||
:options="legendPositionOptions"
|
||||
@update:model-value="(v) => updateNested('legend', 'position', v)"
|
||||
/>
|
||||
<PropNumberInput
|
||||
label="Boyut"
|
||||
:model-value="element.legend?.fontSize ?? 2.8"
|
||||
:step="0.2"
|
||||
@update:model-value="(v) => updateNested('legend', 'fontSize', v)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</PropSection>
|
||||
|
||||
<!-- Etiketler -->
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Etiketler</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Goster</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="element.labels?.show ?? false"
|
||||
@change="updateLabels('show', ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
</div>
|
||||
<PropSection title="Etiketler">
|
||||
<PropCheckbox
|
||||
label="Goster"
|
||||
:model-value="element.labels?.show ?? false"
|
||||
@update:model-value="(v) => updateNested('labels', 'show', v, { show: false })"
|
||||
/>
|
||||
<template v-if="element.labels?.show">
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Boyut</label>
|
||||
<input
|
||||
class="prop-input prop-input--sm"
|
||||
type="number"
|
||||
:value="element.labels?.fontSize ?? 2.2"
|
||||
step="0.2"
|
||||
@change="
|
||||
updateLabels('fontSize', parseFloat(($event.target as HTMLInputElement).value))
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input
|
||||
class="prop-color"
|
||||
type="color"
|
||||
:value="element.labels?.color ?? '#333333'"
|
||||
@input="updateLabels('color', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<PropNumberInput
|
||||
label="Boyut"
|
||||
:model-value="element.labels?.fontSize ?? 2.2"
|
||||
:step="0.2"
|
||||
@update:model-value="(v) => updateNested('labels', 'fontSize', v)"
|
||||
/>
|
||||
<PropColorInput
|
||||
label="Renk"
|
||||
:model-value="element.labels?.color ?? '#333333'"
|
||||
@update:model-value="(v) => updateNested('labels', 'color', v)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</PropSection>
|
||||
|
||||
<!-- Eksenler (pie haric) -->
|
||||
<div class="prop-section" v-if="!isPie">
|
||||
<div class="prop-section__title">Eksenler</div>
|
||||
<PropSection v-if="!isPie" title="Eksenler">
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">X Etiketi</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="text"
|
||||
:value="element.axis?.xLabel ?? ''"
|
||||
@change="updateAxis('xLabel', ($event.target as HTMLInputElement).value || undefined)"
|
||||
@change="(e) => updateNested('axis', 'xLabel', (e.target as HTMLInputElement).value || undefined, {})"
|
||||
placeholder="X ekseni"
|
||||
/>
|
||||
</div>
|
||||
@@ -321,116 +232,84 @@ function removeColor(index: number) {
|
||||
class="prop-input"
|
||||
type="text"
|
||||
:value="element.axis?.yLabel ?? ''"
|
||||
@change="updateAxis('yLabel', ($event.target as HTMLInputElement).value || undefined)"
|
||||
@change="(e) => updateNested('axis', 'yLabel', (e.target as HTMLInputElement).value || undefined, {})"
|
||||
placeholder="Y ekseni"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Izgara</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="element.axis?.showGrid ?? true"
|
||||
@change="updateAxis('showGrid', ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" v-if="element.axis?.showGrid !== false">
|
||||
<label class="prop-label">Izgara Renk</label>
|
||||
<input
|
||||
class="prop-color"
|
||||
type="color"
|
||||
:value="element.axis?.gridColor ?? '#E5E7EB'"
|
||||
@input="updateAxis('gridColor', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PropCheckbox
|
||||
label="Izgara"
|
||||
:model-value="element.axis?.showGrid ?? true"
|
||||
@update:model-value="(v) => updateNested('axis', 'showGrid', v, {})"
|
||||
/>
|
||||
<PropColorInput
|
||||
v-if="element.axis?.showGrid !== false"
|
||||
label="Izgara Renk"
|
||||
:model-value="element.axis?.gridColor ?? '#E5E7EB'"
|
||||
@update:model-value="(v) => updateNested('axis', 'gridColor', v, {})"
|
||||
/>
|
||||
</PropSection>
|
||||
|
||||
<!-- Stil -->
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Stil</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Arka Plan</label>
|
||||
<input
|
||||
class="prop-color"
|
||||
type="color"
|
||||
:value="element.style.backgroundColor ?? '#FFFFFF'"
|
||||
@input="updateStyle('backgroundColor', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<PropSection title="Stil">
|
||||
<PropColorInput
|
||||
label="Arka Plan"
|
||||
:model-value="element.style.backgroundColor ?? '#FFFFFF'"
|
||||
@update:model-value="(v) => updateStyle('backgroundColor', v)"
|
||||
/>
|
||||
|
||||
<!-- Renk Paleti -->
|
||||
<div class="prop-section__subtitle">Renk Paleti</div>
|
||||
<div v-for="(color, i) in colorList" :key="i" class="prop-row">
|
||||
<input
|
||||
class="prop-color"
|
||||
type="color"
|
||||
:value="color"
|
||||
@input="updateColor(i, ($event.target as HTMLInputElement).value)"
|
||||
@input="(e) => updateColor(i, (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button class="prop-btn-sm prop-btn-sm--danger" @click="removeColor(i)" title="Kaldir">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<button class="prop-btn-sm" @click="addColor">+ Renk Ekle</button>
|
||||
</div>
|
||||
</PropSection>
|
||||
|
||||
<!-- Tipe Ozel -->
|
||||
<div class="prop-section" v-if="element.chartType === 'bar'">
|
||||
<div class="prop-section__title">Bar Ayarlari</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Bar Boslugu</label>
|
||||
<input
|
||||
class="prop-input prop-input--sm"
|
||||
type="number"
|
||||
:value="element.style.barGap ?? 0.2"
|
||||
step="0.05"
|
||||
min="0"
|
||||
max="0.8"
|
||||
@change="updateStyle('barGap', parseFloat(($event.target as HTMLInputElement).value))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PropSection v-if="element.chartType === 'bar'" title="Bar Ayarlari">
|
||||
<PropNumberInput
|
||||
label="Bar Boslugu"
|
||||
:model-value="element.style.barGap ?? 0.2"
|
||||
:step="0.05"
|
||||
:min="0"
|
||||
:max="0.8"
|
||||
@update:model-value="(v) => updateStyle('barGap', v)"
|
||||
/>
|
||||
</PropSection>
|
||||
|
||||
<div class="prop-section" v-if="element.chartType === 'line'">
|
||||
<div class="prop-section__title">Line Ayarlari</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Cizgi Kalinligi</label>
|
||||
<input
|
||||
class="prop-input prop-input--sm"
|
||||
type="number"
|
||||
:value="element.style.lineWidth ?? 0.5"
|
||||
step="0.1"
|
||||
min="0.1"
|
||||
@change="updateStyle('lineWidth', parseFloat(($event.target as HTMLInputElement).value))"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Noktalar</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="element.style.showPoints ?? true"
|
||||
@change="updateStyle('showPoints', ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PropSection v-if="element.chartType === 'line'" title="Line Ayarlari">
|
||||
<PropNumberInput
|
||||
label="Cizgi Kalinligi"
|
||||
:model-value="element.style.lineWidth ?? 0.5"
|
||||
:step="0.1"
|
||||
:min="0.1"
|
||||
@update:model-value="(v) => updateStyle('lineWidth', v)"
|
||||
/>
|
||||
<PropCheckbox
|
||||
label="Noktalar"
|
||||
:model-value="element.style.showPoints ?? true"
|
||||
@update:model-value="(v) => updateStyle('showPoints', v)"
|
||||
/>
|
||||
</PropSection>
|
||||
|
||||
<div class="prop-section" v-if="element.chartType === 'pie'">
|
||||
<div class="prop-section__title">Pie Ayarlari</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Ic Yaricap</label>
|
||||
<input
|
||||
class="prop-input prop-input--sm"
|
||||
type="number"
|
||||
:value="element.style.innerRadius ?? 0"
|
||||
step="0.05"
|
||||
min="0"
|
||||
max="0.9"
|
||||
@change="
|
||||
updateStyle('innerRadius', parseFloat(($event.target as HTMLInputElement).value))
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<PropSection v-if="element.chartType === 'pie'" title="Pie Ayarlari">
|
||||
<PropNumberInput
|
||||
label="Ic Yaricap"
|
||||
:model-value="element.style.innerRadius ?? 0"
|
||||
:step="0.05"
|
||||
:min="0"
|
||||
:max="0.9"
|
||||
@update:model-value="(v) => updateStyle('innerRadius', v)"
|
||||
/>
|
||||
<div class="prop-row" style="font-size: 11px; color: #94a3b8">0 = Pie, >0 = Donut</div>
|
||||
</div>
|
||||
</PropSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,63 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import type { CheckboxElement, TemplateElement } from '../../core/types'
|
||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropNumberInput from './shared/PropNumberInput.vue'
|
||||
import PropColorInput from './shared/PropColorInput.vue'
|
||||
import PropCheckbox from './shared/PropCheckbox.vue'
|
||||
import type { CheckboxElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: CheckboxElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
update({ style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
|
||||
}
|
||||
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Onay Kutusu</div>
|
||||
<div v-if="!element.binding" class="prop-row" data-tip="Onay kutusunun varsayilan durumu">
|
||||
<label class="prop-label">Isaretli</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="element.checked ?? false"
|
||||
@change="(e) => update({ checked: (e.target as HTMLInputElement).checked } as any)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Onay kutusu boyutu (mm)">
|
||||
<label class="prop-label">Boyut (mm)</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="1"
|
||||
:value="element.style.size ?? 4"
|
||||
@input="(e) => updateStyle('size', parseFloat((e.target as HTMLInputElement).value) || 4)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Isaret (tik) rengi">
|
||||
<label class="prop-label">Isaret Rengi</label>
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.checkColor ?? '#000000'"
|
||||
@input="(e) => updateStyle('checkColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Kutu kenarlik rengi">
|
||||
<label class="prop-label">Kenar Rengi</label>
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.borderColor ?? '#333333'"
|
||||
@input="(e) => updateStyle('borderColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PropSection title="Onay Kutusu">
|
||||
<PropCheckbox
|
||||
v-if="!element.binding"
|
||||
label="Isaretli"
|
||||
:model-value="element.checked ?? false"
|
||||
data-tip="Onay kutusunun varsayilan durumu"
|
||||
@update:model-value="(v) => update({ checked: v } as any)"
|
||||
/>
|
||||
<PropNumberInput
|
||||
label="Boyut (mm)"
|
||||
:model-value="element.style.size ?? 4"
|
||||
:step="0.5"
|
||||
:min="1"
|
||||
data-tip="Onay kutusu boyutu (mm)"
|
||||
@update:model-value="(v) => updateStyle('size', v)"
|
||||
/>
|
||||
<PropColorInput
|
||||
label="Isaret Rengi"
|
||||
:model-value="element.style.checkColor ?? '#000000'"
|
||||
data-tip="Isaret (tik) rengi"
|
||||
@update:model-value="(v) => updateStyle('checkColor', v)"
|
||||
/>
|
||||
<PropColorInput
|
||||
label="Kenar Rengi"
|
||||
:model-value="element.style.borderColor ?? '#333333'"
|
||||
data-tip="Kutu kenarlik rengi"
|
||||
@update:model-value="(v) => updateStyle('borderColor', v)"
|
||||
/>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
@@ -1,52 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropSelect from './shared/PropSelect.vue'
|
||||
import PropNumberInput from './shared/PropNumberInput.vue'
|
||||
import PropColorInput from './shared/PropColorInput.vue'
|
||||
import PaddingBox from './PaddingBox.vue'
|
||||
import type { ContainerElement, TemplateElement } from '../../core/types'
|
||||
import type { ContainerElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: ContainerElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
const directionOptions = [
|
||||
{ value: 'column', label: 'Dikey' },
|
||||
{ value: 'row', label: 'Yatay' },
|
||||
]
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
update({ style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
|
||||
}
|
||||
const breakOptions = [
|
||||
{ value: 'auto', label: 'Izin Ver' },
|
||||
{ value: 'avoid', label: 'Bolme' },
|
||||
]
|
||||
|
||||
const borderStyleOptions = [
|
||||
{ value: 'solid', label: 'Duz' },
|
||||
{ value: 'dashed', label: 'Kesikli' },
|
||||
{ value: 'dotted', label: 'Noktali' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Container Ayarlari</div>
|
||||
<div class="prop-row" data-tip="Cocuk elemanlarin dizilim yonu">
|
||||
<label class="prop-label">Yon</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.direction"
|
||||
@change="(e) => update({ direction: (e.target as HTMLSelectElement).value } as any)"
|
||||
>
|
||||
<option value="column">Dikey</option>
|
||||
<option value="row">Yatay</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Cocuk elemanlar arasi bosluk (mm)">
|
||||
<label class="prop-label">Bosluk (mm)</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
:value="element.gap"
|
||||
@input="
|
||||
(e) => update({ gap: parseFloat((e.target as HTMLInputElement).value) || 0 } as any)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<PropSection title="Container Ayarlari">
|
||||
<PropSelect
|
||||
label="Yon"
|
||||
:model-value="element.direction"
|
||||
:options="directionOptions"
|
||||
data-tip="Cocuk elemanlarin dizilim yonu"
|
||||
@update:model-value="(v) => update({ direction: v } as any)"
|
||||
/>
|
||||
<PropNumberInput
|
||||
label="Bosluk (mm)"
|
||||
:model-value="element.gap"
|
||||
:step="1"
|
||||
:min="0"
|
||||
data-tip="Cocuk elemanlar arasi bosluk (mm)"
|
||||
@update:model-value="(v) => update({ gap: v } as any)"
|
||||
/>
|
||||
|
||||
<div class="prop-row" data-tip="Cocuklarin cross-axis hizalamasi">
|
||||
<label class="prop-label">{{
|
||||
element.direction === 'column' ? 'Yatay Hizalama' : 'Dikey Hizalama'
|
||||
@@ -87,92 +86,53 @@ function updateStyle(key: string, value: unknown) {
|
||||
@update="(side, value) => update({ padding: { ...element.padding, [side]: value } } as any)"
|
||||
/>
|
||||
|
||||
<div class="prop-row" data-tip="Sayfa sonunda bolunmeyi kontrol eder">
|
||||
<label class="prop-label">Sayfa Bolme</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.breakInside ?? 'auto'"
|
||||
@change="(e) => update({ breakInside: (e.target as HTMLSelectElement).value } as any)"
|
||||
>
|
||||
<option value="auto">Izin Ver</option>
|
||||
<option value="avoid">Bolme</option>
|
||||
</select>
|
||||
</div>
|
||||
<PropSelect
|
||||
label="Sayfa Bolme"
|
||||
:model-value="element.breakInside ?? 'auto'"
|
||||
:options="breakOptions"
|
||||
data-tip="Sayfa sonunda bolunmeyi kontrol eder"
|
||||
@update:model-value="(v) => update({ breakInside: v } as any)"
|
||||
/>
|
||||
</PropSection>
|
||||
|
||||
<div class="prop-section__subtitle">Stil</div>
|
||||
<div class="prop-row" data-tip="Container arka plan rengi">
|
||||
<label class="prop-label">Arka plan</label>
|
||||
<div class="prop-row-inline">
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.backgroundColor ?? '#ffffff'"
|
||||
@input="(e) => updateStyle('backgroundColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button
|
||||
v-if="element.style.backgroundColor"
|
||||
class="prop-clear"
|
||||
@click="updateStyle('backgroundColor', undefined)"
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Kenarlik kalinligi (mm)">
|
||||
<label class="prop-label">Kenarlik (mm)</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
:value="element.style.borderWidth ?? 0"
|
||||
@input="
|
||||
(e) => updateStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Kenarlik cizgisi rengi">
|
||||
<label class="prop-label">Kenarlik rengi</label>
|
||||
<div class="prop-row-inline">
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.borderColor ?? '#000000'"
|
||||
@input="(e) => updateStyle('borderColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button
|
||||
v-if="element.style.borderColor"
|
||||
class="prop-clear"
|
||||
@click="updateStyle('borderColor', undefined)"
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Kenarlik cizgi stili">
|
||||
<label class="prop-label">Kenarlik stili</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.style.borderStyle ?? 'solid'"
|
||||
@change="(e) => updateStyle('borderStyle', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="solid">Duz</option>
|
||||
<option value="dashed">Kesikli</option>
|
||||
<option value="dotted">Noktali</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Kose yuvarlakligi (mm)">
|
||||
<label class="prop-label">Radius (mm)</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
:value="element.style.borderRadius ?? 0"
|
||||
@input="
|
||||
(e) => updateStyle('borderRadius', parseFloat((e.target as HTMLInputElement).value) || 0)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PropSection title="Stil">
|
||||
<PropColorInput
|
||||
label="Arka plan"
|
||||
:model-value="element.style.backgroundColor"
|
||||
default-color="#ffffff"
|
||||
:clearable="true"
|
||||
data-tip="Container arka plan rengi"
|
||||
@update:model-value="(v) => updateStyle('backgroundColor', v)"
|
||||
/>
|
||||
<PropNumberInput
|
||||
label="Kenarlik (mm)"
|
||||
:model-value="element.style.borderWidth ?? 0"
|
||||
:step="0.1"
|
||||
:min="0"
|
||||
data-tip="Kenarlik kalinligi (mm)"
|
||||
@update:model-value="(v) => updateStyle('borderWidth', v)"
|
||||
/>
|
||||
<PropColorInput
|
||||
label="Kenarlik rengi"
|
||||
:model-value="element.style.borderColor"
|
||||
:clearable="true"
|
||||
data-tip="Kenarlik cizgisi rengi"
|
||||
@update:model-value="(v) => updateStyle('borderColor', v)"
|
||||
/>
|
||||
<PropSelect
|
||||
label="Kenarlik stili"
|
||||
:model-value="element.style.borderStyle ?? 'solid'"
|
||||
:options="borderStyleOptions"
|
||||
data-tip="Kenarlik cizgi stili"
|
||||
@update:model-value="(v) => updateStyle('borderStyle', v)"
|
||||
/>
|
||||
<PropNumberInput
|
||||
label="Radius (mm)"
|
||||
:model-value="element.style.borderRadius ?? 0"
|
||||
:step="0.5"
|
||||
:min="0"
|
||||
data-tip="Kose yuvarlakligi (mm)"
|
||||
@update:model-value="(v) => updateStyle('borderRadius', v)"
|
||||
/>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
@@ -1,73 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import type { CurrentDateElement, TextStyle, TemplateElement } from '../../core/types'
|
||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropSelect from './shared/PropSelect.vue'
|
||||
import PropTextStyleGroup from './shared/PropTextStyleGroup.vue'
|
||||
import type { CurrentDateElement, TextStyle } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: CurrentDateElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||
const style = () => props.element.style as TextStyle
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
update({ style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
|
||||
}
|
||||
const formatOptions = [
|
||||
{ value: 'DD.MM.YYYY', label: '30.03.2026' },
|
||||
{ value: 'DD/MM/YYYY', label: '30/03/2026' },
|
||||
{ value: 'YYYY-MM-DD', label: '2026-03-30' },
|
||||
{ value: 'DD.MM.YYYY HH:mm', label: '30.03.2026 14:30' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Tarih</div>
|
||||
<div class="prop-row" data-tip="Tarih gosterim formati">
|
||||
<label class="prop-label">Format</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.format ?? 'DD.MM.YYYY'"
|
||||
@change="(e) => update({ format: (e.target as HTMLSelectElement).value } as any)"
|
||||
>
|
||||
<option value="DD.MM.YYYY">30.03.2026</option>
|
||||
<option value="DD/MM/YYYY">30/03/2026</option>
|
||||
<option value="YYYY-MM-DD">2026-03-30</option>
|
||||
<option value="DD.MM.YYYY HH:mm">30.03.2026 14:30</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Yazi tipi boyutu (point)">
|
||||
<label class="prop-label">Boyut (pt)</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="(element.style as TextStyle).fontSize ?? 10"
|
||||
@input="
|
||||
(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metin rengi">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="(element.style as TextStyle).color ?? '#666666'"
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metnin yatay hizalamasi">
|
||||
<label class="prop-label">Hizalama</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="(element.style as TextStyle).align ?? 'left'"
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="left">Sol</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="right">Sag</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<PropSection title="Tarih">
|
||||
<PropSelect
|
||||
label="Format"
|
||||
:model-value="element.format ?? 'DD.MM.YYYY'"
|
||||
:options="formatOptions"
|
||||
data-tip="Tarih gosterim formati"
|
||||
@update:model-value="(v) => update({ format: v } as any)"
|
||||
/>
|
||||
<PropTextStyleGroup
|
||||
:font-size="style().fontSize ?? 10"
|
||||
:color="style().color ?? '#666666'"
|
||||
:align="style().align ?? 'left'"
|
||||
:show-weight="false"
|
||||
@update:font-size="(v) => updateStyle('fontSize', v)"
|
||||
@update:color="(v) => updateStyle('color', v)"
|
||||
@update:align="(v) => updateStyle('align', v)"
|
||||
/>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||
import { useSchemaStore } from '../../stores/schema'
|
||||
import type { ImageElement, TemplateElement } from '../../core/types'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropSelect from './shared/PropSelect.vue'
|
||||
import PropFieldSelect from './shared/PropFieldSelect.vue'
|
||||
import type { ImageElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: ImageElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||
const schemaStore = useSchemaStore()
|
||||
|
||||
/** Statik mi dinamik mi? */
|
||||
const isDynamic = computed(() => !!props.element.binding)
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
const imageScalarFields = computed(() =>
|
||||
schemaStore.scalarFields.filter((f) => f.format === 'image' || f.type === 'string'),
|
||||
)
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
update({ style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
|
||||
}
|
||||
const fitOptions = [
|
||||
{ value: 'contain', label: 'Sigdir' },
|
||||
{ value: 'cover', label: 'Kap' },
|
||||
{ value: 'stretch', label: 'Esnet' },
|
||||
]
|
||||
|
||||
function onImageFileSelect(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
@@ -30,40 +30,24 @@ function onImageFileSelect(e: Event) {
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
update({ src: reader.result as string, binding: undefined } as Partial<TemplateElement>)
|
||||
update({ src: reader.result as string, binding: undefined } as any)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
function setMode(mode: 'static' | 'dynamic') {
|
||||
if (mode === 'static') {
|
||||
update({ binding: undefined } as Partial<TemplateElement>)
|
||||
update({ binding: undefined } as any)
|
||||
} else {
|
||||
// Dinamik moda geç — ilk uygun alanı seç veya boş bırak
|
||||
const imageFields = schemaStore.scalarFields.filter(
|
||||
(f) => f.format === 'image' || f.type === 'string',
|
||||
)
|
||||
const path = imageFields.length > 0 ? imageFields[0].path : ''
|
||||
update({ src: undefined, binding: { type: 'scalar', path } } as Partial<TemplateElement>)
|
||||
const path = imageScalarFields.value.length > 0 ? imageScalarFields.value[0].path : ''
|
||||
update({ src: undefined, binding: { type: 'scalar', path } } as any)
|
||||
}
|
||||
}
|
||||
|
||||
function setBindingPath(path: string) {
|
||||
update({ binding: { type: 'scalar', path } } as Partial<TemplateElement>)
|
||||
}
|
||||
|
||||
/** Schema'dan görsel olabilecek alanlar (format: image veya string) */
|
||||
const imageScalarFields = computed(() => {
|
||||
return schemaStore.scalarFields.filter((f) => f.format === 'image' || f.type === 'string')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Gorsel</div>
|
||||
|
||||
<!-- Statik / Dinamik toggle -->
|
||||
<div class="prop-row" data-tip="Gorsel kaynagi: dosya veya veri alanından">
|
||||
<PropSection title="Gorsel">
|
||||
<div class="prop-row" data-tip="Gorsel kaynagi: dosya veya veri alanindan">
|
||||
<label class="prop-label">Mod</label>
|
||||
<div class="prop-toggle-group">
|
||||
<button
|
||||
@@ -83,7 +67,6 @@ const imageScalarFields = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statik: dosya seçimi -->
|
||||
<template v-if="!isDynamic">
|
||||
<div class="prop-row" data-tip="Gorsel dosyasi secin (PNG, JPG, SVG)">
|
||||
<label class="prop-label">Kaynak</label>
|
||||
@@ -104,41 +87,28 @@ const imageScalarFields = computed(() => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Dinamik: schema alan seçimi -->
|
||||
<template v-else>
|
||||
<div class="prop-row" data-tip="Gorsel URL'sinin gelecegi veri alani">
|
||||
<label class="prop-label">Veri Alani</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.binding?.path ?? ''"
|
||||
@change="(e) => setBindingPath((e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="" disabled>Secin...</option>
|
||||
<option v-for="field in imageScalarFields" :key="field.path" :value="field.path">
|
||||
{{ field.title }} ({{ field.path }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<PropFieldSelect
|
||||
label="Veri Alani"
|
||||
:model-value="element.binding?.path ?? ''"
|
||||
:fields="imageScalarFields"
|
||||
data-tip="Gorsel URL'sinin gelecegi veri alani"
|
||||
@update:model-value="(v) => update({ binding: { type: 'scalar', path: v } } as any)"
|
||||
/>
|
||||
<div v-if="element.binding?.path" class="prop-row">
|
||||
<label class="prop-label">Path</label>
|
||||
<span class="prop-info">{{ element.binding.path }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Sığdırma modu (ortak) -->
|
||||
<div class="prop-row" data-tip="Gorselin alana sigdirma modu">
|
||||
<label class="prop-label">Sigdirma</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.style.objectFit ?? 'contain'"
|
||||
@change="(e) => updateStyle('objectFit', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="contain">Sigdir</option>
|
||||
<option value="cover">Kap</option>
|
||||
<option value="stretch">Esnet</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<PropSelect
|
||||
label="Sigdirma"
|
||||
:model-value="element.style.objectFit ?? 'contain'"
|
||||
:options="fitOptions"
|
||||
data-tip="Gorselin alana sigdirma modu"
|
||||
@update:model-value="(v) => updateStyle('objectFit', v)"
|
||||
/>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,46 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import type { LineElement, TemplateElement } from '../../core/types'
|
||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropNumberInput from './shared/PropNumberInput.vue'
|
||||
import PropColorInput from './shared/PropColorInput.vue'
|
||||
import type { LineElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: LineElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, {
|
||||
style: { ...props.element.style, [key]: value },
|
||||
} as Partial<TemplateElement>)
|
||||
}
|
||||
const { updateStyle } = usePropertyUpdate(() => props.element)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Cizgi Stili</div>
|
||||
<div class="prop-row" data-tip="Cizgi kalinligi (mm)">
|
||||
<label class="prop-label">Kalinlik (mm)</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0.1"
|
||||
:value="element.style.strokeWidth ?? 0.5"
|
||||
@input="
|
||||
(e) => updateStyle('strokeWidth', parseFloat((e.target as HTMLInputElement).value) || 0.5)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Cizgi rengi">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.strokeColor ?? '#000000'"
|
||||
@input="(e) => updateStyle('strokeColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PropSection title="Cizgi Stili">
|
||||
<PropNumberInput
|
||||
label="Kalinlik (mm)"
|
||||
:model-value="element.style.strokeWidth ?? 0.5"
|
||||
:step="0.1"
|
||||
:min="0.1"
|
||||
data-tip="Cizgi kalinligi (mm)"
|
||||
@update:model-value="(v) => updateStyle('strokeWidth', v)"
|
||||
/>
|
||||
<PropColorInput
|
||||
label="Renk"
|
||||
:model-value="element.style.strokeColor ?? '#000000'"
|
||||
data-tip="Cizgi rengi"
|
||||
@update:model-value="(v) => updateStyle('strokeColor', v)"
|
||||
/>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
@@ -1,73 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import type { PageNumberElement, TextStyle, TemplateElement } from '../../core/types'
|
||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropSelect from './shared/PropSelect.vue'
|
||||
import PropTextStyleGroup from './shared/PropTextStyleGroup.vue'
|
||||
import type { PageNumberElement, TextStyle } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: PageNumberElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||
const style = () => props.element.style as TextStyle
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
update({ style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
|
||||
}
|
||||
const formatOptions = [
|
||||
{ value: '{current} / {total}', label: '1 / 5' },
|
||||
{ value: '{current}', label: '1' },
|
||||
{ value: 'Sayfa {current}', label: 'Sayfa 1' },
|
||||
{ value: 'Sayfa {current} / {total}', label: 'Sayfa 1 / 5' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Sayfa Numarasi</div>
|
||||
<div class="prop-row" data-tip="Sayfa numarasi gosterim formati">
|
||||
<label class="prop-label">Format</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.format ?? '{current} / {total}'"
|
||||
@change="(e) => update({ format: (e.target as HTMLSelectElement).value } as any)"
|
||||
>
|
||||
<option value="{current} / {total}">1 / 5</option>
|
||||
<option value="{current}">1</option>
|
||||
<option value="Sayfa {current}">Sayfa 1</option>
|
||||
<option value="Sayfa {current} / {total}">Sayfa 1 / 5</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Yazi tipi boyutu (point)">
|
||||
<label class="prop-label">Boyut (pt)</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="(element.style as TextStyle).fontSize ?? 10"
|
||||
@input="
|
||||
(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metin rengi">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="(element.style as TextStyle).color ?? '#666666'"
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metnin yatay hizalamasi">
|
||||
<label class="prop-label">Hizalama</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="(element.style as TextStyle).align ?? 'center'"
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="left">Sol</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="right">Sag</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<PropSection title="Sayfa Numarasi">
|
||||
<PropSelect
|
||||
label="Format"
|
||||
:model-value="element.format ?? '{current} / {total}'"
|
||||
:options="formatOptions"
|
||||
data-tip="Sayfa numarasi gosterim formati"
|
||||
@update:model-value="(v) => update({ format: v } as any)"
|
||||
/>
|
||||
<PropTextStyleGroup
|
||||
:font-size="style().fontSize ?? 10"
|
||||
:color="style().color ?? '#666666'"
|
||||
:align="style().align ?? 'center'"
|
||||
:show-weight="false"
|
||||
@update:font-size="(v) => updateStyle('fontSize', v)"
|
||||
@update:color="(v) => updateStyle('color', v)"
|
||||
@update:align="(v) => updateStyle('align', v)"
|
||||
/>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropSelect from './shared/PropSelect.vue'
|
||||
import PropNumberInput from './shared/PropNumberInput.vue'
|
||||
import type { TemplateElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: TemplateElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
|
||||
function togglePositioning() {
|
||||
if (props.element.position.type === 'flow') {
|
||||
const positionOptions = [
|
||||
{ value: 'flow', label: 'Flow' },
|
||||
{ value: 'absolute', label: 'Absolute' },
|
||||
]
|
||||
|
||||
function togglePositioning(value: string) {
|
||||
if (value === 'absolute') {
|
||||
templateStore.updateElementPosition(props.element.id, { type: 'absolute', x: 0, y: 0 })
|
||||
} else {
|
||||
templateStore.updateElementPosition(props.element.id, { type: 'flow' })
|
||||
@@ -16,54 +24,43 @@ function togglePositioning() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Pozisyon</div>
|
||||
<div class="prop-row" data-tip="Flow: otomatik dizilim, Absolute: sabit konum">
|
||||
<label class="prop-label">Mod</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.position.type"
|
||||
@change="togglePositioning"
|
||||
>
|
||||
<option value="flow">Flow</option>
|
||||
<option value="absolute">Absolute</option>
|
||||
</select>
|
||||
</div>
|
||||
<PropSection title="Pozisyon">
|
||||
<PropSelect
|
||||
label="Mod"
|
||||
:model-value="element.position.type"
|
||||
:options="positionOptions"
|
||||
data-tip="Flow: otomatik dizilim, Absolute: sabit konum"
|
||||
@update:model-value="togglePositioning"
|
||||
/>
|
||||
<template v-if="element.position.type === 'absolute'">
|
||||
<div class="prop-row" data-tip="Yatay pozisyon — parent sol kenardan uzaklik (mm)">
|
||||
<label class="prop-label">X (mm)</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="0.5"
|
||||
:value="element.position.x"
|
||||
@input="
|
||||
(e) =>
|
||||
templateStore.updateElementPosition(element.id, {
|
||||
type: 'absolute',
|
||||
x: parseFloat((e.target as HTMLInputElement).value) || 0,
|
||||
y: (element.position as any).y ?? 0,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Dikey pozisyon — parent ust kenardan uzaklik (mm)">
|
||||
<label class="prop-label">Y (mm)</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="0.5"
|
||||
:value="element.position.y"
|
||||
@input="
|
||||
(e) =>
|
||||
templateStore.updateElementPosition(element.id, {
|
||||
type: 'absolute',
|
||||
x: (element.position as any).x ?? 0,
|
||||
y: parseFloat((e.target as HTMLInputElement).value) || 0,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<PropNumberInput
|
||||
label="X (mm)"
|
||||
:model-value="(element.position as any).x ?? 0"
|
||||
:step="0.5"
|
||||
data-tip="Yatay pozisyon — parent sol kenardan uzaklik (mm)"
|
||||
@update:model-value="
|
||||
(v) =>
|
||||
templateStore.updateElementPosition(element.id, {
|
||||
type: 'absolute',
|
||||
x: v,
|
||||
y: (element.position as any).y ?? 0,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<PropNumberInput
|
||||
label="Y (mm)"
|
||||
:model-value="(element.position as any).y ?? 0"
|
||||
:step="0.5"
|
||||
data-tip="Dikey pozisyon — parent ust kenardan uzaklik (mm)"
|
||||
@update:model-value="
|
||||
(v) =>
|
||||
templateStore.updateElementPosition(element.id, {
|
||||
type: 'absolute',
|
||||
x: (element.position as any).x ?? 0,
|
||||
y: v,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||
import { useSchemaStore } from '../../stores/schema'
|
||||
import { sz } from '../../core/types'
|
||||
import { schemaFormatToFormatType, defaultAlignForSchema } from '../../core/schema-parser'
|
||||
import type {
|
||||
RepeatingTableElement,
|
||||
TableColumn,
|
||||
FormatType,
|
||||
TemplateElement,
|
||||
} from '../../core/types'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropFieldSelect from './shared/PropFieldSelect.vue'
|
||||
import TableColumnEditor from './table/TableColumnEditor.vue'
|
||||
import TableStyleEditor from './table/TableStyleEditor.vue'
|
||||
import type { RepeatingTableElement, TableColumn, TableStyle } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: RepeatingTableElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
const { update } = usePropertyUpdate(() => props.element)
|
||||
const schemaStore = useSchemaStore()
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
|
||||
let colIdCounter = Date.now()
|
||||
function nextColId() {
|
||||
return `col_${(++colIdCounter).toString(36)}`
|
||||
@@ -40,24 +31,21 @@ function updateTableDataSource(path: string) {
|
||||
align: defaultAlignForSchema(field),
|
||||
format: schemaFormatToFormatType(field.format),
|
||||
}))
|
||||
update({
|
||||
dataSource: { type: 'array', path },
|
||||
columns,
|
||||
} as Partial<TemplateElement>)
|
||||
update({ dataSource: { type: 'array', path }, columns } as any)
|
||||
} else {
|
||||
update({ dataSource: { type: 'array', path } } as Partial<TemplateElement>)
|
||||
update({ dataSource: { type: 'array', path } } as any)
|
||||
}
|
||||
}
|
||||
|
||||
function updateTableStyle(key: string, value: unknown) {
|
||||
const newStyle = { ...props.element.style, [key]: value }
|
||||
if (value === undefined || value === '') delete (newStyle as Record<string, unknown>)[key]
|
||||
update({ style: newStyle } as Partial<TemplateElement>)
|
||||
update({ style: newStyle } as any)
|
||||
}
|
||||
|
||||
function updateColumn(colId: string, updates: Partial<TableColumn>) {
|
||||
const columns = props.element.columns.map((c) => (c.id === colId ? { ...c, ...updates } : c))
|
||||
update({ columns } as Partial<TemplateElement>)
|
||||
update({ columns } as any)
|
||||
}
|
||||
|
||||
function addColumn() {
|
||||
@@ -68,13 +56,11 @@ function addColumn() {
|
||||
width: sz.auto(),
|
||||
align: 'left',
|
||||
}
|
||||
update({ columns: [...props.element.columns, newCol] } as Partial<TemplateElement>)
|
||||
update({ columns: [...props.element.columns, newCol] } as any)
|
||||
}
|
||||
|
||||
function removeColumn(colId: string) {
|
||||
update({
|
||||
columns: props.element.columns.filter((c) => c.id !== colId),
|
||||
} as Partial<TemplateElement>)
|
||||
update({ columns: props.element.columns.filter((c) => c.id !== colId) } as any)
|
||||
}
|
||||
|
||||
function moveColumn(colId: string, direction: -1 | 1) {
|
||||
@@ -83,7 +69,7 @@ function moveColumn(colId: string, direction: -1 | 1) {
|
||||
const newIdx = idx + direction
|
||||
if (newIdx < 0 || newIdx >= cols.length) return
|
||||
;[cols[idx], cols[newIdx]] = [cols[newIdx], cols[idx]]
|
||||
update({ columns: cols } as Partial<TemplateElement>)
|
||||
update({ columns: cols } as any)
|
||||
}
|
||||
|
||||
const tableItemFields = computed(() => {
|
||||
@@ -93,864 +79,39 @@ const tableItemFields = computed(() => {
|
||||
|
||||
<template>
|
||||
<!-- Data source -->
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Veri Kaynagi</div>
|
||||
<div class="prop-row" data-tip="Tablonun baglanacagi array veri kaynagi">
|
||||
<label class="prop-label">Kaynak</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.dataSource.path"
|
||||
@change="(e) => updateTableDataSource((e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="" disabled>Secin...</option>
|
||||
<option v-for="arr in schemaStore.arrayFields" :key="arr.path" :value="arr.path">
|
||||
{{ arr.title }} ({{ arr.path }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<PropSection title="Veri Kaynagi">
|
||||
<PropFieldSelect
|
||||
label="Kaynak"
|
||||
:model-value="element.dataSource.path"
|
||||
:fields="schemaStore.arrayFields"
|
||||
data-tip="Tablonun baglanacagi array veri kaynagi"
|
||||
@update:model-value="updateTableDataSource"
|
||||
/>
|
||||
</PropSection>
|
||||
|
||||
<!-- Columns -->
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">
|
||||
Sutunlar
|
||||
<PropSection title="Sutunlar">
|
||||
<template #actions>
|
||||
<button class="prop-add-btn" @click="addColumn">+</button>
|
||||
</div>
|
||||
<div v-for="col in element.columns" :key="col.id" class="tbl-col">
|
||||
<!-- Row 1: title + actions -->
|
||||
<div class="tbl-col__head">
|
||||
<input
|
||||
class="tbl-col__title"
|
||||
type="text"
|
||||
:value="col.title"
|
||||
@change="(e) => updateColumn(col.id, { title: (e.target as HTMLInputElement).value })"
|
||||
:placeholder="col.field"
|
||||
data-tip="Sutun basligi"
|
||||
/>
|
||||
<div class="tbl-col__actions">
|
||||
<button class="tbl-col__act" @click="moveColumn(col.id, -1)" data-tip="Yukari tasi">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path d="M5 2L2 6h6L5 2z" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="tbl-col__act" @click="moveColumn(col.id, 1)" data-tip="Asagi tasi">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path d="M5 8L2 4h6L5 8z" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="tbl-col__act tbl-col__act--del"
|
||||
@click="removeColumn(col.id)"
|
||||
data-tip="Sutunu sil"
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path
|
||||
d="M2 2l6 6M8 2l-6 6"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: field + align + format + width compact -->
|
||||
<div class="tbl-col__controls">
|
||||
<!-- Field -->
|
||||
<select
|
||||
v-if="tableItemFields.length > 0"
|
||||
class="tbl-col__field"
|
||||
:value="col.field"
|
||||
data-tip="Veri alani"
|
||||
@change="
|
||||
(e) => {
|
||||
const field = (e.target as HTMLSelectElement).value
|
||||
const node = tableItemFields.find((f) => f.key === field)
|
||||
if (node) {
|
||||
updateColumn(col.id, {
|
||||
field,
|
||||
title: node.title,
|
||||
align: defaultAlignForSchema(node),
|
||||
format: schemaFormatToFormatType(node.format),
|
||||
})
|
||||
} else {
|
||||
updateColumn(col.id, { field })
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<option v-for="f in tableItemFields" :key="f.key" :value="f.key">{{ f.key }}</option>
|
||||
</select>
|
||||
<input
|
||||
v-else
|
||||
class="tbl-col__field"
|
||||
type="text"
|
||||
:value="col.field"
|
||||
@change="(e) => updateColumn(col.id, { field: (e.target as HTMLInputElement).value })"
|
||||
data-tip="Veri alani"
|
||||
/>
|
||||
|
||||
<!-- Alignment icons -->
|
||||
<div class="tbl-col__align">
|
||||
<button
|
||||
class="tbl-col__align-btn"
|
||||
:class="{ 'tbl-col__align-btn--on': col.align === 'left' }"
|
||||
@click="updateColumn(col.id, { align: 'left' })"
|
||||
data-tip="Sola hizala"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12">
|
||||
<line
|
||||
x1="1"
|
||||
y1="3"
|
||||
x2="11"
|
||||
y2="3"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="1"
|
||||
y1="6"
|
||||
x2="8"
|
||||
y2="6"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="1"
|
||||
y1="9"
|
||||
x2="10"
|
||||
y2="9"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="tbl-col__align-btn"
|
||||
:class="{ 'tbl-col__align-btn--on': col.align === 'center' }"
|
||||
@click="updateColumn(col.id, { align: 'center' })"
|
||||
data-tip="Ortala"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12">
|
||||
<line
|
||||
x1="1"
|
||||
y1="3"
|
||||
x2="11"
|
||||
y2="3"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="2.5"
|
||||
y1="6"
|
||||
x2="9.5"
|
||||
y2="6"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="1.5"
|
||||
y1="9"
|
||||
x2="10.5"
|
||||
y2="9"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="tbl-col__align-btn"
|
||||
:class="{ 'tbl-col__align-btn--on': col.align === 'right' }"
|
||||
@click="updateColumn(col.id, { align: 'right' })"
|
||||
data-tip="Saga hizala"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12">
|
||||
<line
|
||||
x1="1"
|
||||
y1="3"
|
||||
x2="11"
|
||||
y2="3"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="4"
|
||||
y1="6"
|
||||
x2="11"
|
||||
y2="6"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="2"
|
||||
y1="9"
|
||||
x2="11"
|
||||
y2="9"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 3: format + width -->
|
||||
<div class="tbl-col__extra" data-tip="Veri gosterim formati">
|
||||
<label class="tbl-col__elabel">Format</label>
|
||||
<select
|
||||
class="tbl-col__fmt"
|
||||
:value="col.format ?? ''"
|
||||
@change="
|
||||
(e) =>
|
||||
updateColumn(col.id, {
|
||||
format: ((e.target as HTMLSelectElement).value || undefined) as
|
||||
| FormatType
|
||||
| undefined,
|
||||
})
|
||||
"
|
||||
>
|
||||
<option value="">Yok</option>
|
||||
<option value="currency">Para birimi</option>
|
||||
<option value="number">Sayi</option>
|
||||
<option value="date">Tarih</option>
|
||||
<option value="percentage">Yuzde</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="tbl-col__extra" data-tip="Sutun genislik modu">
|
||||
<label class="tbl-col__elabel">Genislik</label>
|
||||
<select
|
||||
class="tbl-col__wtype"
|
||||
:value="col.width.type"
|
||||
@change="
|
||||
(e) => {
|
||||
const t = (e.target as HTMLSelectElement).value
|
||||
if (t === 'auto') updateColumn(col.id, { width: { type: 'auto' } })
|
||||
else if (t === 'fr') updateColumn(col.id, { width: { type: 'fr', value: 1 } })
|
||||
else updateColumn(col.id, { width: { type: 'fixed', value: 30 } })
|
||||
}
|
||||
"
|
||||
>
|
||||
<option value="auto">Otomatik</option>
|
||||
<option value="fixed">Sabit (mm)</option>
|
||||
<option value="fr">Oran (fr)</option>
|
||||
</select>
|
||||
<span
|
||||
v-if="col.width.type === 'fixed' || col.width.type === 'fr'"
|
||||
class="ts-tip-wrap"
|
||||
:data-tip="col.width.type === 'fixed' ? 'Sabit genislik (mm)' : 'Oran degeri (fr)'"
|
||||
>
|
||||
<input
|
||||
class="tbl-col__wval"
|
||||
type="number"
|
||||
step="1"
|
||||
:min="col.width.type === 'fixed' ? 5 : 1"
|
||||
:value="(col.width as any).value"
|
||||
@change="
|
||||
(e) =>
|
||||
updateColumn(col.id, {
|
||||
width: {
|
||||
type: col.width.type,
|
||||
value:
|
||||
parseFloat((e.target as HTMLInputElement).value) ||
|
||||
(col.width.type === 'fixed' ? 30 : 1),
|
||||
} as any,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<TableColumnEditor
|
||||
v-for="col in element.columns"
|
||||
:key="col.id"
|
||||
:column="col"
|
||||
:item-fields="tableItemFields"
|
||||
@update="updateColumn"
|
||||
@remove="removeColumn"
|
||||
@move="moveColumn"
|
||||
/>
|
||||
</PropSection>
|
||||
|
||||
<!-- Table style -->
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Tablo Stili</div>
|
||||
|
||||
<div class="ts-form">
|
||||
<!-- Font sizes -->
|
||||
<label class="ts-lbl" data-tip="Icerik ve header yazi boyutu (pt)">Yazi boyutu</label>
|
||||
<div class="ts-val ts-val--pair">
|
||||
<span class="ts-sep">Icerik</span>
|
||||
<span class="ts-tip-wrap" data-tip="Icerik yazi boyutu (pt)">
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="1"
|
||||
min="6"
|
||||
max="99"
|
||||
:value="element.style.fontSize ?? 10"
|
||||
@input="
|
||||
(e) =>
|
||||
updateTableStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span class="ts-sep">Header</span>
|
||||
<span class="ts-tip-wrap" data-tip="Header yazi boyutu (pt)">
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="1"
|
||||
min="6"
|
||||
max="99"
|
||||
:value="element.style.headerFontSize ?? element.style.fontSize ?? 10"
|
||||
@input="
|
||||
(e) =>
|
||||
updateTableStyle(
|
||||
'headerFontSize',
|
||||
parseFloat((e.target as HTMLInputElement).value) || 10,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Colors -->
|
||||
<label class="ts-lbl" data-tip="Header, metin ve zebra satirlari renkleri">Renkler</label>
|
||||
<div class="ts-val ts-val--colors">
|
||||
<div class="ts-color-item" data-tip="Header arkaplan rengi">
|
||||
<input
|
||||
class="ts-swatch"
|
||||
type="color"
|
||||
:value="element.style.headerBg ?? '#f0f0f0'"
|
||||
@input="(e) => updateTableStyle('headerBg', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<span class="ts-clbl">Arkaplan</span>
|
||||
</div>
|
||||
<div class="ts-color-item" data-tip="Header metin rengi">
|
||||
<input
|
||||
class="ts-swatch"
|
||||
type="color"
|
||||
:value="element.style.headerColor ?? '#000000'"
|
||||
@input="(e) => updateTableStyle('headerColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<span class="ts-clbl">Metin</span>
|
||||
</div>
|
||||
<div class="ts-color-item" data-tip="Zebra satir rengi — tek satirlar">
|
||||
<div class="ts-swatch-wrap">
|
||||
<input
|
||||
class="ts-swatch"
|
||||
type="color"
|
||||
:value="element.style.zebraOdd ?? '#fafafa'"
|
||||
@input="(e) => updateTableStyle('zebraOdd', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button
|
||||
v-if="element.style.zebraOdd"
|
||||
class="ts-swatch-clr"
|
||||
@click="updateTableStyle('zebraOdd', undefined)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<span class="ts-clbl">Zebra</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Border -->
|
||||
<label class="ts-lbl" data-tip="Tablo kenarlik rengi ve kalinligi">Kenarlik</label>
|
||||
<div class="ts-val ts-val--pair">
|
||||
<div class="ts-swatch-wrap" data-tip="Kenarlik rengi">
|
||||
<input
|
||||
class="ts-swatch"
|
||||
type="color"
|
||||
:value="element.style.borderColor ?? '#cccccc'"
|
||||
@input="(e) => updateTableStyle('borderColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button
|
||||
v-if="element.style.borderColor"
|
||||
class="ts-swatch-clr"
|
||||
@click="updateTableStyle('borderColor', undefined)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<span class="ts-tip-wrap" data-tip="Kenarlik kalinligi (mm)">
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="99"
|
||||
:value="element.style.borderWidth ?? 0.5"
|
||||
@input="
|
||||
(e) =>
|
||||
updateTableStyle(
|
||||
'borderWidth',
|
||||
parseFloat((e.target as HTMLInputElement).value) || 0,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span class="ts-unit">mm</span>
|
||||
</div>
|
||||
|
||||
<!-- Cell padding -->
|
||||
<label class="ts-lbl" data-tip="Hucre ic bosluklari — yatay ve dikey (mm)">Ic bosluk</label>
|
||||
<div class="ts-val ts-val--pair">
|
||||
<span class="ts-pad-icon" data-tip="Yatay bosluk (mm)">↔</span>
|
||||
<span class="ts-tip-wrap" data-tip="Yatay ic bosluk (mm)">
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max="99"
|
||||
:value="element.style.cellPaddingH ?? 2"
|
||||
@input="
|
||||
(e) =>
|
||||
updateTableStyle(
|
||||
'cellPaddingH',
|
||||
parseFloat((e.target as HTMLInputElement).value) || 0,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span class="ts-pad-icon" data-tip="Dikey bosluk (mm)">↕</span>
|
||||
<span class="ts-tip-wrap" data-tip="Dikey ic bosluk (mm)">
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max="99"
|
||||
:value="element.style.cellPaddingV ?? 1"
|
||||
@input="
|
||||
(e) =>
|
||||
updateTableStyle(
|
||||
'cellPaddingV',
|
||||
parseFloat((e.target as HTMLInputElement).value) || 0,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Header padding -->
|
||||
<label class="ts-lbl" data-tip="Header hucre bosluklari — yatay ve dikey (mm)"
|
||||
>Header bosluk</label
|
||||
>
|
||||
<div class="ts-val ts-val--pair">
|
||||
<span class="ts-pad-icon" data-tip="Yatay bosluk (mm)">↔</span>
|
||||
<span class="ts-tip-wrap" data-tip="Header yatay bosluk (mm)">
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max="99"
|
||||
:value="element.style.headerPaddingH ?? element.style.cellPaddingH ?? 2"
|
||||
@input="
|
||||
(e) =>
|
||||
updateTableStyle(
|
||||
'headerPaddingH',
|
||||
parseFloat((e.target as HTMLInputElement).value) || 0,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span class="ts-pad-icon" data-tip="Dikey bosluk (mm)">↕</span>
|
||||
<span class="ts-tip-wrap" data-tip="Header dikey bosluk (mm)">
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max="99"
|
||||
:value="element.style.headerPaddingV ?? element.style.cellPaddingV ?? 1"
|
||||
@input="
|
||||
(e) =>
|
||||
updateTableStyle(
|
||||
'headerPaddingV',
|
||||
parseFloat((e.target as HTMLInputElement).value) || 0,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Repeat header -->
|
||||
<label class="ts-lbl" data-tip="Cok sayfali tablolarda header'i her sayfada tekrarla"
|
||||
>Header tekrarla</label
|
||||
>
|
||||
<div class="ts-val">
|
||||
<label class="ts-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="element.repeatHeader !== false"
|
||||
@change="(e) => update({ repeatHeader: (e.target as HTMLInputElement).checked } as any)"
|
||||
/>
|
||||
<span class="ts-toggle__track"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PropSection title="Tablo Stili">
|
||||
<TableStyleEditor
|
||||
:style="element.style as TableStyle"
|
||||
:repeat-header="element.repeatHeader !== false"
|
||||
@update:style="updateTableStyle"
|
||||
@update:repeat-header="(v) => update({ repeatHeader: v } as any)"
|
||||
/>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Column card - compact */
|
||||
.tbl-col {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 5px;
|
||||
padding: 5px 6px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.tbl-col__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tbl-col__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
padding: 1px 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tbl-col__title:focus {
|
||||
border-bottom: 1px solid #93c5fd;
|
||||
}
|
||||
|
||||
.tbl-col__actions {
|
||||
display: flex;
|
||||
gap: 1px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tbl-col__act {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tbl-col__act:hover {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.tbl-col__act--del:hover {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.tbl-col__controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.tbl-col__field {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
background: white;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.tbl-col__field:focus {
|
||||
outline: none;
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
|
||||
.tbl-col__align {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tbl-col__align-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: white;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tbl-col__align-btn:first-child {
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
.tbl-col__align-btn:last-child {
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
.tbl-col__align-btn:not(:first-child) {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.tbl-col__align-btn--on {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.tbl-col__extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.tbl-col__elabel {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tbl-col__fmt {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
background: white;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tbl-col__wtype {
|
||||
width: 80px;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
background: white;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tbl-col__wval {
|
||||
width: 36px;
|
||||
padding: 2px 3px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
background: white;
|
||||
color: #334155;
|
||||
text-align: center;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.tbl-col__wval::-webkit-inner-spin-button,
|
||||
.tbl-col__wval::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tbl-col__wval:focus {
|
||||
outline: none;
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
|
||||
/* Table style — aligned 2-column form */
|
||||
.ts-form {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 5px 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ts-lbl {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ts-val {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.ts-val--pair {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ts-val--colors {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ts-sep {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.ts-num {
|
||||
width: 32px;
|
||||
padding: 2px 3px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
background: white;
|
||||
color: #334155;
|
||||
text-align: center;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.ts-num::-webkit-inner-spin-button,
|
||||
.ts-num::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ts-num:focus {
|
||||
outline: none;
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
|
||||
.ts-unit {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Color swatches */
|
||||
.ts-color-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.ts-clbl {
|
||||
font-size: 9px;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ts-swatch {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.ts-swatch-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.ts-swatch-clr {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
font-size: 9px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ts-swatch-clr:hover {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
.ts-pad-icon {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ts-tip-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* Toggle switch */
|
||||
.ts-toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ts-toggle input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.ts-toggle__track {
|
||||
display: block;
|
||||
width: 28px;
|
||||
height: 16px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 8px;
|
||||
transition: background 0.15s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ts-toggle__track::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.15s;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.ts-toggle input:checked + .ts-toggle__track {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.ts-toggle input:checked + .ts-toggle__track::after {
|
||||
transform: translateX(12px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropColorInput from './shared/PropColorInput.vue'
|
||||
import PropSelect from './shared/PropSelect.vue'
|
||||
import PropTextStyleGroup from './shared/PropTextStyleGroup.vue'
|
||||
import type { RichTextElement, RichTextSpan, TextStyle } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: RichTextElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
|
||||
function update(updates: Partial<RichTextElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates as any)
|
||||
}
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
update({ style: { ...props.element.style, [key]: value } } as Partial<RichTextElement>)
|
||||
}
|
||||
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||
|
||||
function updateSpan(index: number, updates: Partial<RichTextSpan>) {
|
||||
const content = [...props.element.content]
|
||||
content[index] = { ...content[index], ...updates }
|
||||
update({ content })
|
||||
update({ content } as any)
|
||||
}
|
||||
|
||||
function updateSpanStyle(index: number, key: string, value: unknown) {
|
||||
@@ -31,60 +23,39 @@ function updateSpanStyle(index: number, key: string, value: unknown) {
|
||||
|
||||
function addSpan() {
|
||||
const content = [...props.element.content, { text: 'yeni', style: {} }]
|
||||
update({ content })
|
||||
update({ content } as any)
|
||||
}
|
||||
|
||||
function removeSpan(index: number) {
|
||||
if (props.element.content.length <= 1) return
|
||||
const content = props.element.content.filter((_, i) => i !== index)
|
||||
update({ content })
|
||||
update({ content } as any)
|
||||
}
|
||||
|
||||
const weightOptions = [
|
||||
{ value: '', label: 'Varsayilan' },
|
||||
{ value: 'normal', label: 'Normal' },
|
||||
{ value: 'bold', label: 'Kalin' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Varsayilan Stil</div>
|
||||
<div class="prop-row" data-tip="Varsayilan yazi tipi boyutu (point)">
|
||||
<label class="prop-label">Boyut (pt)</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="element.style.fontSize ?? 11"
|
||||
@input="
|
||||
(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 11)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Varsayilan metin rengi">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.color ?? '#000000'"
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metnin yatay hizalamasi">
|
||||
<label class="prop-label">Hizalama</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.style.align ?? 'left'"
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="left">Sol</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="right">Sag</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<PropSection title="Varsayilan Stil">
|
||||
<PropTextStyleGroup
|
||||
:font-size="element.style.fontSize ?? 11"
|
||||
:color="element.style.color ?? '#000000'"
|
||||
:align="element.style.align ?? 'left'"
|
||||
:show-weight="false"
|
||||
@update:font-size="(v) => updateStyle('fontSize', v)"
|
||||
@update:color="(v) => updateStyle('color', v)"
|
||||
@update:align="(v) => updateStyle('align', v)"
|
||||
/>
|
||||
</PropSection>
|
||||
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">
|
||||
Span'lar
|
||||
<PropSection title="Span'lar">
|
||||
<template #actions>
|
||||
<button class="prop-add-btn" @click="addSpan" title="Span ekle">+</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-for="(span, idx) in element.content" :key="idx" class="prop-span-card">
|
||||
<div class="prop-span-card__header">
|
||||
@@ -125,57 +96,24 @@ function removeSpan(index: number) {
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Span yazi kalinligi">
|
||||
<label class="prop-label">Kalinlik</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="(span.style as TextStyle).fontWeight ?? ''"
|
||||
@change="
|
||||
(e) => {
|
||||
const v = (e.target as HTMLSelectElement).value
|
||||
updateSpanStyle(idx, 'fontWeight', v || undefined)
|
||||
}
|
||||
"
|
||||
>
|
||||
<option value="">Varsayilan</option>
|
||||
<option value="normal">Normal</option>
|
||||
<option value="bold">Kalin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Span metin rengi">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="(span.style as TextStyle).color ?? element.style.color ?? '#000000'"
|
||||
@input="(e) => updateSpanStyle(idx, 'color', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<PropSelect
|
||||
label="Kalinlik"
|
||||
:model-value="(span.style as TextStyle).fontWeight ?? ''"
|
||||
:options="weightOptions"
|
||||
data-tip="Span yazi kalinligi"
|
||||
@update:model-value="(v) => updateSpanStyle(idx, 'fontWeight', v || undefined)"
|
||||
/>
|
||||
<PropColorInput
|
||||
label="Renk"
|
||||
:model-value="(span.style as TextStyle).color ?? element.style.color ?? '#000000'"
|
||||
data-tip="Span metin rengi"
|
||||
@update:model-value="(v) => updateSpanStyle(idx, 'color', v)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.prop-add-btn {
|
||||
float: right;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.prop-add-btn:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.prop-span-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
||||
@@ -1,86 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import type { ShapeElement, TemplateElement } from '../../core/types'
|
||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropSelect from './shared/PropSelect.vue'
|
||||
import PropNumberInput from './shared/PropNumberInput.vue'
|
||||
import PropColorInput from './shared/PropColorInput.vue'
|
||||
import type { ShapeElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: ShapeElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
update({ style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
|
||||
}
|
||||
const shapeOptions = [
|
||||
{ value: 'rectangle', label: 'Dikdortgen' },
|
||||
{ value: 'rounded_rectangle', label: 'Yuvarlak Dikdortgen' },
|
||||
{ value: 'ellipse', label: 'Elips' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Sekil</div>
|
||||
<div class="prop-row" data-tip="Sekil tipi">
|
||||
<label class="prop-label">Tip</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.shapeType"
|
||||
@change="(e) => update({ shapeType: (e.target as HTMLSelectElement).value } as any)"
|
||||
>
|
||||
<option value="rectangle">Dikdortgen</option>
|
||||
<option value="rounded_rectangle">Yuvarlak Dikdortgen</option>
|
||||
<option value="ellipse">Elips</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Sekil arka plan rengi">
|
||||
<label class="prop-label">Arka Plan</label>
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.backgroundColor ?? '#f0f0f0'"
|
||||
@input="(e) => updateStyle('backgroundColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Kenarlik cizgisi rengi">
|
||||
<label class="prop-label">Kenar Rengi</label>
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.borderColor ?? '#333333'"
|
||||
@input="(e) => updateStyle('borderColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Kenarlik cizgi kalinligi (mm)">
|
||||
<label class="prop-label">Kenar Kalinligi</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="0.25"
|
||||
min="0"
|
||||
:value="element.style.borderWidth ?? 0.5"
|
||||
@input="
|
||||
(e) => updateStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
<PropSection title="Sekil">
|
||||
<PropSelect
|
||||
label="Tip"
|
||||
:model-value="element.shapeType"
|
||||
:options="shapeOptions"
|
||||
data-tip="Sekil tipi"
|
||||
@update:model-value="(v) => update({ shapeType: v } as any)"
|
||||
/>
|
||||
<PropColorInput
|
||||
label="Arka Plan"
|
||||
:model-value="element.style.backgroundColor ?? '#f0f0f0'"
|
||||
data-tip="Sekil arka plan rengi"
|
||||
@update:model-value="(v) => updateStyle('backgroundColor', v)"
|
||||
/>
|
||||
<PropColorInput
|
||||
label="Kenar Rengi"
|
||||
:model-value="element.style.borderColor ?? '#333333'"
|
||||
data-tip="Kenarlik cizgisi rengi"
|
||||
@update:model-value="(v) => updateStyle('borderColor', v)"
|
||||
/>
|
||||
<PropNumberInput
|
||||
label="Kenar Kalinligi"
|
||||
:model-value="element.style.borderWidth ?? 0.5"
|
||||
:step="0.25"
|
||||
:min="0"
|
||||
data-tip="Kenarlik cizgi kalinligi (mm)"
|
||||
@update:model-value="(v) => updateStyle('borderWidth', v)"
|
||||
/>
|
||||
<PropNumberInput
|
||||
v-if="element.shapeType === 'rounded_rectangle'"
|
||||
class="prop-row"
|
||||
label="Kose Yuvarlakligi"
|
||||
:model-value="element.style.borderRadius ?? 2"
|
||||
:step="0.5"
|
||||
:min="0"
|
||||
data-tip="Kose yuvarlakligi (mm)"
|
||||
>
|
||||
<label class="prop-label">Kose Yuvarlakligi</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
:value="element.style.borderRadius ?? 2"
|
||||
@input="
|
||||
(e) => updateStyle('borderRadius', parseFloat((e.target as HTMLInputElement).value) || 0)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@update:model-value="(v) => updateStyle('borderRadius', v)"
|
||||
/>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
@@ -1,120 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropNumberInput from './shared/PropNumberInput.vue'
|
||||
import type { TemplateElement, SizeValue } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: TemplateElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
|
||||
const sizeOptions = [
|
||||
{ value: 'auto', label: 'Otomatik' },
|
||||
{ value: 'fixed', label: 'Sabit (mm)' },
|
||||
{ value: 'fr', label: 'Oran (fr)' },
|
||||
]
|
||||
|
||||
function updateSize(axis: 'width' | 'height', sv: SizeValue) {
|
||||
templateStore.updateElementSize(props.element.id, { [axis]: sv })
|
||||
}
|
||||
|
||||
function onTypeChange(axis: 'width' | 'height', type: string) {
|
||||
if (type === 'auto') updateSize(axis, { type: 'auto' })
|
||||
else if (type === 'fr') updateSize(axis, { type: 'fr', value: 1 })
|
||||
else updateSize(axis, { type: 'fixed', value: axis === 'width' ? 50 : 20 })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Boyut</div>
|
||||
<PropSection title="Boyut">
|
||||
<div class="prop-row" data-tip="Genislik boyutlandirma modu">
|
||||
<label class="prop-label">Genislik</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.size.width.type"
|
||||
@change="
|
||||
(e) => {
|
||||
const t = (e.target as HTMLSelectElement).value
|
||||
if (t === 'auto') updateSize('width', { type: 'auto' })
|
||||
else if (t === 'fr') updateSize('width', { type: 'fr', value: 1 })
|
||||
else updateSize('width', { type: 'fixed', value: 50 })
|
||||
}
|
||||
"
|
||||
@change="(e) => onTypeChange('width', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="auto">Otomatik</option>
|
||||
<option value="fixed">Sabit (mm)</option>
|
||||
<option value="fr">Oran (fr)</option>
|
||||
<option v-for="opt in sizeOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div
|
||||
<PropNumberInput
|
||||
v-if="element.size.width.type === 'fixed'"
|
||||
class="prop-row"
|
||||
label="mm"
|
||||
:model-value="(element.size.width as any).value"
|
||||
:step="1"
|
||||
:min="1"
|
||||
data-tip="Sabit genislik degeri (mm)"
|
||||
>
|
||||
<label class="prop-label">mm</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="(element.size.width as any).value"
|
||||
@input="
|
||||
(e) =>
|
||||
updateSize('width', {
|
||||
type: 'fixed',
|
||||
value: parseFloat((e.target as HTMLInputElement).value) || 10,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@update:model-value="(v) => updateSize('width', { type: 'fixed', value: v })"
|
||||
/>
|
||||
<PropNumberInput
|
||||
v-if="element.size.width.type === 'fr'"
|
||||
class="prop-row"
|
||||
label="fr"
|
||||
:model-value="(element.size.width as any).value"
|
||||
:step="1"
|
||||
:min="1"
|
||||
data-tip="Kalan alani oransal doldurma degeri"
|
||||
>
|
||||
<label class="prop-label">fr</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="(element.size.width as any).value"
|
||||
@input="
|
||||
(e) =>
|
||||
updateSize('width', {
|
||||
type: 'fr',
|
||||
value: parseFloat((e.target as HTMLInputElement).value) || 1,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@update:model-value="(v) => updateSize('width', { type: 'fr', value: v })"
|
||||
/>
|
||||
|
||||
<div class="prop-row" data-tip="Yukseklik boyutlandirma modu">
|
||||
<label class="prop-label">Yukseklik</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.size.height.type"
|
||||
@change="
|
||||
(e) => {
|
||||
const t = (e.target as HTMLSelectElement).value
|
||||
if (t === 'auto') updateSize('height', { type: 'auto' })
|
||||
else if (t === 'fr') updateSize('height', { type: 'fr', value: 1 })
|
||||
else updateSize('height', { type: 'fixed', value: 20 })
|
||||
}
|
||||
"
|
||||
@change="(e) => onTypeChange('height', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="auto">Otomatik</option>
|
||||
<option value="fixed">Sabit (mm)</option>
|
||||
<option value="fr">Oran (fr)</option>
|
||||
<option v-for="opt in sizeOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div
|
||||
<PropNumberInput
|
||||
v-if="element.size.height.type === 'fixed'"
|
||||
class="prop-row"
|
||||
label="mm"
|
||||
:model-value="(element.size.height as any).value"
|
||||
:step="1"
|
||||
:min="1"
|
||||
data-tip="Sabit yukseklik degeri (mm)"
|
||||
>
|
||||
<label class="prop-label">mm</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="(element.size.height as any).value"
|
||||
@input="
|
||||
(e) =>
|
||||
updateSize('height', {
|
||||
type: 'fixed',
|
||||
value: parseFloat((e.target as HTMLInputElement).value) || 10,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@update:model-value="(v) => updateSize('height', { type: 'fixed', value: v })"
|
||||
/>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '../../stores/template'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||
import PropSection from './shared/PropSection.vue'
|
||||
import PropTextStyleGroup from './shared/PropTextStyleGroup.vue'
|
||||
import type { StaticTextElement, TextStyle, TemplateElement } from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: TemplateElement }>()
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
update({ style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
|
||||
}
|
||||
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||
const style = () => props.element.style as TextStyle
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Metin Stili</div>
|
||||
|
||||
<PropSection title="Metin Stili">
|
||||
<div v-if="element.type === 'static_text'" class="prop-row" data-tip="Sabit metin icerigi">
|
||||
<label class="prop-label">Metin</label>
|
||||
<input
|
||||
@@ -32,51 +21,15 @@ function updateStyle(key: string, value: unknown) {
|
||||
@input="(e) => update({ content: (e.target as HTMLInputElement).value } as any)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="prop-row" data-tip="Yazi tipi boyutu (point)">
|
||||
<label class="prop-label">Boyut (pt)</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="(element.style as TextStyle).fontSize ?? 11"
|
||||
@input="
|
||||
(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 11)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Yazi tipi kalinligi">
|
||||
<label class="prop-label">Kalinlik</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="(element.style as TextStyle).fontWeight ?? 'normal'"
|
||||
@change="(e) => updateStyle('fontWeight', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="normal">Normal</option>
|
||||
<option value="bold">Kalin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metin rengi">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="(element.style as TextStyle).color ?? '#000000'"
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metnin yatay hizalamasi">
|
||||
<label class="prop-label">Hizalama</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="(element.style as TextStyle).align ?? 'left'"
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="left">Sol</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="right">Sag</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<PropTextStyleGroup
|
||||
:font-size="style().fontSize ?? 11"
|
||||
:font-weight="style().fontWeight ?? 'normal'"
|
||||
:color="style().color ?? '#000000'"
|
||||
:align="style().align ?? 'left'"
|
||||
@update:font-size="(v) => updateStyle('fontSize', v)"
|
||||
@update:font-weight="(v) => updateStyle('fontWeight', v)"
|
||||
@update:color="(v) => updateStyle('color', v)"
|
||||
@update:align="(v) => updateStyle('align', v)"
|
||||
/>
|
||||
</PropSection>
|
||||
</template>
|
||||
|
||||
20
frontend/src/components/properties/shared/PropCheckbox.vue
Normal file
20
frontend/src/components/properties/shared/PropCheckbox.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string
|
||||
modelValue: boolean
|
||||
dataTip?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-row" :data-tip="dataTip">
|
||||
<label class="prop-label">{{ label }}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="modelValue"
|
||||
@change="(e) => emit('update:modelValue', (e.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
38
frontend/src/components/properties/shared/PropColorInput.vue
Normal file
38
frontend/src/components/properties/shared/PropColorInput.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label: string
|
||||
modelValue: string | undefined
|
||||
defaultColor?: string
|
||||
clearable?: boolean
|
||||
dataTip?: string
|
||||
}>(),
|
||||
{ defaultColor: '#000000', clearable: false },
|
||||
)
|
||||
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string | undefined] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-row" :data-tip="dataTip">
|
||||
<label class="prop-label">{{ label }}</label>
|
||||
<div v-if="clearable" class="prop-row-inline">
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="modelValue ?? defaultColor"
|
||||
@input="(e) => emit('update:modelValue', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button v-if="modelValue" class="prop-clear" @click="emit('update:modelValue', undefined)">
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
v-else
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="modelValue ?? defaultColor"
|
||||
@input="(e) => emit('update:modelValue', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label: string
|
||||
modelValue: string
|
||||
fields: Array<{ path?: string; key?: string; title?: string; type?: string }>
|
||||
placeholder?: string
|
||||
allowEmpty?: boolean
|
||||
emptyLabel?: string
|
||||
dataTip?: string
|
||||
}>(),
|
||||
{ placeholder: 'Secin...', allowEmpty: false, emptyLabel: 'Yok' },
|
||||
)
|
||||
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-row" :data-tip="dataTip">
|
||||
<label class="prop-label">{{ label }}</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="modelValue"
|
||||
@change="(e) => emit('update:modelValue', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option v-if="allowEmpty" value="">{{ emptyLabel }}</option>
|
||||
<option v-else value="" disabled>{{ placeholder }}</option>
|
||||
<option
|
||||
v-for="field in fields"
|
||||
:key="field.path ?? field.key"
|
||||
:value="field.path ?? field.key"
|
||||
>
|
||||
{{ field.title ?? field.path ?? field.key }}
|
||||
<template v-if="field.path">({{ field.path }})</template>
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label: string
|
||||
modelValue: number
|
||||
step?: number
|
||||
min?: number
|
||||
max?: number
|
||||
dataTip?: string
|
||||
}>(),
|
||||
{ step: 1, min: 0 },
|
||||
)
|
||||
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: number] }>()
|
||||
|
||||
function onInput(e: Event) {
|
||||
const val = parseFloat((e.target as HTMLInputElement).value)
|
||||
if (!isNaN(val)) emit('update:modelValue', val)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-row" :data-tip="dataTip">
|
||||
<label class="prop-label">{{ label }}</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
:step="step"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
46
frontend/src/components/properties/shared/PropSection.vue
Normal file
46
frontend/src/components/properties/shared/PropSection.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{ title: string; defaultOpen?: boolean }>(), {
|
||||
defaultOpen: true,
|
||||
})
|
||||
|
||||
const open = ref(props.defaultOpen)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title prop-section__title--collapsible" @click="open = !open">
|
||||
<span class="prop-section__chevron" :class="{ 'prop-section__chevron--closed': !open }"
|
||||
>▾</span
|
||||
>
|
||||
{{ title }}
|
||||
<span class="prop-section__actions" @click.stop><slot name="actions" /></span>
|
||||
</div>
|
||||
<template v-if="open"><slot /></template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.prop-section__title--collapsible {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.prop-section__chevron {
|
||||
font-size: 8px;
|
||||
transition: transform 0.15s;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.prop-section__chevron--closed {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.prop-section__actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
23
frontend/src/components/properties/shared/PropSelect.vue
Normal file
23
frontend/src/components/properties/shared/PropSelect.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string
|
||||
modelValue: string
|
||||
options: Array<{ value: string; label: string }>
|
||||
dataTip?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prop-row" :data-tip="dataTip">
|
||||
<label class="prop-label">{{ label }}</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="modelValue"
|
||||
@change="(e) => emit('update:modelValue', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option v-for="opt in options" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import PropNumberInput from './PropNumberInput.vue'
|
||||
import PropColorInput from './PropColorInput.vue'
|
||||
import PropSelect from './PropSelect.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
fontSize: number
|
||||
fontWeight?: string
|
||||
color: string
|
||||
align: string
|
||||
showWeight?: boolean
|
||||
}>(),
|
||||
{ fontWeight: 'normal', showWeight: true },
|
||||
)
|
||||
|
||||
defineEmits<{
|
||||
'update:fontSize': [value: number]
|
||||
'update:fontWeight': [value: string]
|
||||
'update:color': [value: string]
|
||||
'update:align': [value: string]
|
||||
}>()
|
||||
|
||||
const weightOptions = [
|
||||
{ value: 'normal', label: 'Normal' },
|
||||
{ value: 'bold', label: 'Kalin' },
|
||||
]
|
||||
|
||||
const alignOptions = [
|
||||
{ value: 'left', label: 'Sol' },
|
||||
{ value: 'center', label: 'Orta' },
|
||||
{ value: 'right', label: 'Sag' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PropNumberInput
|
||||
label="Boyut (pt)"
|
||||
:model-value="fontSize"
|
||||
:step="1"
|
||||
:min="1"
|
||||
data-tip="Yazi tipi boyutu (point)"
|
||||
@update:model-value="$emit('update:fontSize', $event)"
|
||||
/>
|
||||
<PropSelect
|
||||
v-if="showWeight"
|
||||
label="Kalinlik"
|
||||
:model-value="fontWeight!"
|
||||
:options="weightOptions"
|
||||
data-tip="Yazi tipi kalinligi"
|
||||
@update:model-value="$emit('update:fontWeight', $event)"
|
||||
/>
|
||||
<PropColorInput
|
||||
label="Renk"
|
||||
:model-value="color"
|
||||
data-tip="Metin rengi"
|
||||
@update:model-value="$emit('update:color', $event!)"
|
||||
/>
|
||||
<PropSelect
|
||||
label="Hizalama"
|
||||
:model-value="align"
|
||||
:options="alignOptions"
|
||||
data-tip="Metnin yatay hizalamasi"
|
||||
@update:model-value="$emit('update:align', $event)"
|
||||
/>
|
||||
</template>
|
||||
387
frontend/src/components/properties/table/TableColumnEditor.vue
Normal file
387
frontend/src/components/properties/table/TableColumnEditor.vue
Normal file
@@ -0,0 +1,387 @@
|
||||
<script setup lang="ts">
|
||||
import { defaultAlignForSchema, schemaFormatToFormatType } from '../../../core/schema-parser'
|
||||
import type { TableColumn, FormatType } from '../../../core/types'
|
||||
|
||||
type ItemField = { key: string; title: string; type?: string; format?: string }
|
||||
import '../../../styles/properties.css'
|
||||
|
||||
defineProps<{
|
||||
column: TableColumn
|
||||
itemFields: ItemField[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [colId: string, updates: Partial<TableColumn>]
|
||||
remove: [colId: string]
|
||||
move: [colId: string, direction: -1 | 1]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tbl-col">
|
||||
<!-- Row 1: title + actions -->
|
||||
<div class="tbl-col__head">
|
||||
<input
|
||||
class="tbl-col__title"
|
||||
type="text"
|
||||
:value="column.title"
|
||||
@change="(e) => emit('update', column.id, { title: (e.target as HTMLInputElement).value })"
|
||||
:placeholder="column.field"
|
||||
data-tip="Sutun basligi"
|
||||
/>
|
||||
<div class="tbl-col__actions">
|
||||
<button class="tbl-col__act" @click="emit('move', column.id, -1)" data-tip="Yukari tasi">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path d="M5 2L2 6h6L5 2z" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="tbl-col__act" @click="emit('move', column.id, 1)" data-tip="Asagi tasi">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path d="M5 8L2 4h6L5 8z" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="tbl-col__act tbl-col__act--del"
|
||||
@click="emit('remove', column.id)"
|
||||
data-tip="Sutunu sil"
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path
|
||||
d="M2 2l6 6M8 2l-6 6"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: field + align -->
|
||||
<div class="tbl-col__controls">
|
||||
<select
|
||||
v-if="itemFields.length > 0"
|
||||
class="tbl-col__field"
|
||||
:value="column.field"
|
||||
data-tip="Veri alani"
|
||||
@change="
|
||||
(e) => {
|
||||
const field = (e.target as HTMLSelectElement).value
|
||||
const node = itemFields.find((f) => f.key === field)
|
||||
if (node) {
|
||||
emit('update', column.id, {
|
||||
field,
|
||||
title: node.title,
|
||||
align: defaultAlignForSchema(node as any),
|
||||
format: schemaFormatToFormatType(node.format),
|
||||
})
|
||||
} else {
|
||||
emit('update', column.id, { field })
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<option v-for="f in itemFields" :key="f.key" :value="f.key">{{ f.key }}</option>
|
||||
</select>
|
||||
<input
|
||||
v-else
|
||||
class="tbl-col__field"
|
||||
type="text"
|
||||
:value="column.field"
|
||||
@change="(e) => emit('update', column.id, { field: (e.target as HTMLInputElement).value })"
|
||||
data-tip="Veri alani"
|
||||
/>
|
||||
|
||||
<!-- Alignment icons -->
|
||||
<div class="tbl-col__align">
|
||||
<button
|
||||
class="tbl-col__align-btn"
|
||||
:class="{ 'tbl-col__align-btn--on': column.align === 'left' }"
|
||||
@click="emit('update', column.id, { align: 'left' })"
|
||||
data-tip="Sola hizala"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12">
|
||||
<line x1="1" y1="3" x2="11" y2="3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||
<line x1="1" y1="6" x2="8" y2="6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||
<line x1="1" y1="9" x2="10" y2="9" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="tbl-col__align-btn"
|
||||
:class="{ 'tbl-col__align-btn--on': column.align === 'center' }"
|
||||
@click="emit('update', column.id, { align: 'center' })"
|
||||
data-tip="Ortala"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12">
|
||||
<line x1="1" y1="3" x2="11" y2="3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||
<line x1="2.5" y1="6" x2="9.5" y2="6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||
<line x1="1.5" y1="9" x2="10.5" y2="9" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="tbl-col__align-btn"
|
||||
:class="{ 'tbl-col__align-btn--on': column.align === 'right' }"
|
||||
@click="emit('update', column.id, { align: 'right' })"
|
||||
data-tip="Saga hizala"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12">
|
||||
<line x1="1" y1="3" x2="11" y2="3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||
<line x1="4" y1="6" x2="11" y2="6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||
<line x1="2" y1="9" x2="11" y2="9" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 3: format + width -->
|
||||
<div class="tbl-col__extra" data-tip="Veri gosterim formati">
|
||||
<label class="tbl-col__elabel">Format</label>
|
||||
<select
|
||||
class="tbl-col__fmt"
|
||||
:value="column.format ?? ''"
|
||||
@change="
|
||||
(e) =>
|
||||
emit('update', column.id, {
|
||||
format: ((e.target as HTMLSelectElement).value || undefined) as FormatType | undefined,
|
||||
})
|
||||
"
|
||||
>
|
||||
<option value="">Yok</option>
|
||||
<option value="currency">Para birimi</option>
|
||||
<option value="number">Sayi</option>
|
||||
<option value="date">Tarih</option>
|
||||
<option value="percentage">Yuzde</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="tbl-col__extra" data-tip="Sutun genislik modu">
|
||||
<label class="tbl-col__elabel">Genislik</label>
|
||||
<select
|
||||
class="tbl-col__wtype"
|
||||
:value="column.width.type"
|
||||
@change="
|
||||
(e) => {
|
||||
const t = (e.target as HTMLSelectElement).value
|
||||
if (t === 'auto') emit('update', column.id, { width: { type: 'auto' } })
|
||||
else if (t === 'fr') emit('update', column.id, { width: { type: 'fr', value: 1 } })
|
||||
else emit('update', column.id, { width: { type: 'fixed', value: 30 } })
|
||||
}
|
||||
"
|
||||
>
|
||||
<option value="auto">Otomatik</option>
|
||||
<option value="fixed">Sabit (mm)</option>
|
||||
<option value="fr">Oran (fr)</option>
|
||||
</select>
|
||||
<span
|
||||
v-if="column.width.type === 'fixed' || column.width.type === 'fr'"
|
||||
class="ts-tip-wrap"
|
||||
:data-tip="column.width.type === 'fixed' ? 'Sabit genislik (mm)' : 'Oran degeri (fr)'"
|
||||
>
|
||||
<input
|
||||
class="tbl-col__wval"
|
||||
type="number"
|
||||
step="1"
|
||||
:min="column.width.type === 'fixed' ? 5 : 1"
|
||||
:value="(column.width as any).value"
|
||||
@change="
|
||||
(e) =>
|
||||
emit('update', column.id, {
|
||||
width: {
|
||||
type: column.width.type,
|
||||
value:
|
||||
parseFloat((e.target as HTMLInputElement).value) ||
|
||||
(column.width.type === 'fixed' ? 30 : 1),
|
||||
} as any,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tbl-col {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 5px;
|
||||
padding: 5px 6px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.tbl-col__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tbl-col__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
padding: 1px 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tbl-col__title:focus {
|
||||
border-bottom: 1px solid #93c5fd;
|
||||
}
|
||||
|
||||
.tbl-col__actions {
|
||||
display: flex;
|
||||
gap: 1px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tbl-col__act {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tbl-col__act:hover {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.tbl-col__act--del:hover {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.tbl-col__controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.tbl-col__field {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
background: white;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.tbl-col__field:focus {
|
||||
outline: none;
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
|
||||
.tbl-col__align {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tbl-col__align-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: white;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tbl-col__align-btn:first-child {
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
.tbl-col__align-btn:last-child {
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
.tbl-col__align-btn:not(:first-child) {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.tbl-col__align-btn--on {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.tbl-col__extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.tbl-col__elabel {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tbl-col__fmt {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
background: white;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tbl-col__wtype {
|
||||
width: 80px;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
background: white;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tbl-col__wval {
|
||||
width: 36px;
|
||||
padding: 2px 3px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
background: white;
|
||||
color: #334155;
|
||||
text-align: center;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.tbl-col__wval::-webkit-inner-spin-button,
|
||||
.tbl-col__wval::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tbl-col__wval:focus {
|
||||
outline: none;
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
|
||||
.ts-tip-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
</style>
|
||||
366
frontend/src/components/properties/table/TableStyleEditor.vue
Normal file
366
frontend/src/components/properties/table/TableStyleEditor.vue
Normal file
@@ -0,0 +1,366 @@
|
||||
<script setup lang="ts">
|
||||
import type { TableStyle } from '../../../core/types'
|
||||
import '../../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{
|
||||
style: TableStyle
|
||||
repeatHeader: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:style': [key: string, value: unknown]
|
||||
'update:repeatHeader': [value: boolean]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ts-form">
|
||||
<!-- Font sizes -->
|
||||
<label class="ts-lbl" data-tip="Icerik ve header yazi boyutu (pt)">Yazi boyutu</label>
|
||||
<div class="ts-val ts-val--pair">
|
||||
<span class="ts-sep">Icerik</span>
|
||||
<span class="ts-tip-wrap" data-tip="Icerik yazi boyutu (pt)">
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="1"
|
||||
min="6"
|
||||
max="99"
|
||||
:value="style.fontSize ?? 10"
|
||||
@input="(e) => emit('update:style', 'fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)"
|
||||
/>
|
||||
</span>
|
||||
<span class="ts-sep">Header</span>
|
||||
<span class="ts-tip-wrap" data-tip="Header yazi boyutu (pt)">
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="1"
|
||||
min="6"
|
||||
max="99"
|
||||
:value="style.headerFontSize ?? style.fontSize ?? 10"
|
||||
@input="(e) => emit('update:style', 'headerFontSize', parseFloat((e.target as HTMLInputElement).value) || 10)"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Colors -->
|
||||
<label class="ts-lbl" data-tip="Header, metin ve zebra satirlari renkleri">Renkler</label>
|
||||
<div class="ts-val ts-val--colors">
|
||||
<div class="ts-color-item" data-tip="Header arkaplan rengi">
|
||||
<input
|
||||
class="ts-swatch"
|
||||
type="color"
|
||||
:value="style.headerBg ?? '#f0f0f0'"
|
||||
@input="(e) => emit('update:style', 'headerBg', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<span class="ts-clbl">Arkaplan</span>
|
||||
</div>
|
||||
<div class="ts-color-item" data-tip="Header metin rengi">
|
||||
<input
|
||||
class="ts-swatch"
|
||||
type="color"
|
||||
:value="style.headerColor ?? '#000000'"
|
||||
@input="(e) => emit('update:style', 'headerColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<span class="ts-clbl">Metin</span>
|
||||
</div>
|
||||
<div class="ts-color-item" data-tip="Zebra satir rengi — tek satirlar">
|
||||
<div class="ts-swatch-wrap">
|
||||
<input
|
||||
class="ts-swatch"
|
||||
type="color"
|
||||
:value="style.zebraOdd ?? '#fafafa'"
|
||||
@input="(e) => emit('update:style', 'zebraOdd', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button
|
||||
v-if="style.zebraOdd"
|
||||
class="ts-swatch-clr"
|
||||
@click="emit('update:style', 'zebraOdd', undefined)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<span class="ts-clbl">Zebra</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Border -->
|
||||
<label class="ts-lbl" data-tip="Tablo kenarlik rengi ve kalinligi">Kenarlik</label>
|
||||
<div class="ts-val ts-val--pair">
|
||||
<div class="ts-swatch-wrap" data-tip="Kenarlik rengi">
|
||||
<input
|
||||
class="ts-swatch"
|
||||
type="color"
|
||||
:value="style.borderColor ?? '#cccccc'"
|
||||
@input="(e) => emit('update:style', 'borderColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button
|
||||
v-if="style.borderColor"
|
||||
class="ts-swatch-clr"
|
||||
@click="emit('update:style', 'borderColor', undefined)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<span class="ts-tip-wrap" data-tip="Kenarlik kalinligi (mm)">
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="99"
|
||||
:value="style.borderWidth ?? 0.5"
|
||||
@input="(e) => emit('update:style', 'borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)"
|
||||
/>
|
||||
</span>
|
||||
<span class="ts-unit">mm</span>
|
||||
</div>
|
||||
|
||||
<!-- Cell padding -->
|
||||
<label class="ts-lbl" data-tip="Hucre ic bosluklari — yatay ve dikey (mm)">Ic bosluk</label>
|
||||
<div class="ts-val ts-val--pair">
|
||||
<span class="ts-pad-icon" data-tip="Yatay bosluk (mm)">↔</span>
|
||||
<span class="ts-tip-wrap" data-tip="Yatay ic bosluk (mm)">
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max="99"
|
||||
:value="style.cellPaddingH ?? 2"
|
||||
@input="(e) => emit('update:style', 'cellPaddingH', parseFloat((e.target as HTMLInputElement).value) || 0)"
|
||||
/>
|
||||
</span>
|
||||
<span class="ts-pad-icon" data-tip="Dikey bosluk (mm)">↕</span>
|
||||
<span class="ts-tip-wrap" data-tip="Dikey ic bosluk (mm)">
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max="99"
|
||||
:value="style.cellPaddingV ?? 1"
|
||||
@input="(e) => emit('update:style', 'cellPaddingV', parseFloat((e.target as HTMLInputElement).value) || 0)"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Header padding -->
|
||||
<label class="ts-lbl" data-tip="Header hucre bosluklari — yatay ve dikey (mm)">Header bosluk</label>
|
||||
<div class="ts-val ts-val--pair">
|
||||
<span class="ts-pad-icon" data-tip="Yatay bosluk (mm)">↔</span>
|
||||
<span class="ts-tip-wrap" data-tip="Header yatay bosluk (mm)">
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max="99"
|
||||
:value="style.headerPaddingH ?? style.cellPaddingH ?? 2"
|
||||
@input="(e) => emit('update:style', 'headerPaddingH', parseFloat((e.target as HTMLInputElement).value) || 0)"
|
||||
/>
|
||||
</span>
|
||||
<span class="ts-pad-icon" data-tip="Dikey bosluk (mm)">↕</span>
|
||||
<span class="ts-tip-wrap" data-tip="Header dikey bosluk (mm)">
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max="99"
|
||||
:value="style.headerPaddingV ?? style.cellPaddingV ?? 1"
|
||||
@input="(e) => emit('update:style', 'headerPaddingV', parseFloat((e.target as HTMLInputElement).value) || 0)"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Repeat header -->
|
||||
<label class="ts-lbl" data-tip="Cok sayfali tablolarda header'i her sayfada tekrarla">Header tekrarla</label>
|
||||
<div class="ts-val">
|
||||
<label class="ts-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="repeatHeader"
|
||||
@change="(e) => emit('update:repeatHeader', (e.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
<span class="ts-toggle__track"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ts-form {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 5px 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ts-lbl {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ts-val {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.ts-val--pair {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ts-val--colors {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ts-sep {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.ts-num {
|
||||
width: 32px;
|
||||
padding: 2px 3px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
background: white;
|
||||
color: #334155;
|
||||
text-align: center;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.ts-num::-webkit-inner-spin-button,
|
||||
.ts-num::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ts-num:focus {
|
||||
outline: none;
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
|
||||
.ts-unit {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.ts-color-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.ts-clbl {
|
||||
font-size: 9px;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ts-swatch {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.ts-swatch-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.ts-swatch-clr {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
font-size: 9px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ts-swatch-clr:hover {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
.ts-pad-icon {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ts-tip-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.ts-toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ts-toggle input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.ts-toggle__track {
|
||||
display: block;
|
||||
width: 28px;
|
||||
height: 16px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 8px;
|
||||
transition: background 0.15s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ts-toggle__track::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.15s;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.ts-toggle input:checked + .ts-toggle__track {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.ts-toggle input:checked + .ts-toggle__track::after {
|
||||
transform: translateX(12px);
|
||||
}
|
||||
</style>
|
||||
30
frontend/src/composables/usePropertyUpdate.ts
Normal file
30
frontend/src/composables/usePropertyUpdate.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useTemplateStore } from '../stores/template'
|
||||
import { useEditorStore } from '../stores/editor'
|
||||
import type { TemplateElement } from '../core/types'
|
||||
|
||||
export function usePropertyUpdate(elementRef: () => TemplateElement) {
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
|
||||
function update(updates: Partial<TemplateElement>) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, updates)
|
||||
}
|
||||
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
update({ style: { ...elementRef().style, [key]: value } } as Partial<TemplateElement>)
|
||||
}
|
||||
|
||||
function updateNested(
|
||||
field: string,
|
||||
key: string,
|
||||
value: unknown,
|
||||
defaults: Record<string, unknown> = {},
|
||||
) {
|
||||
const current = (elementRef() as any)[field] ?? defaults
|
||||
update({ [field]: { ...current, [key]: value } } as any)
|
||||
}
|
||||
|
||||
return { update, updateStyle, updateNested }
|
||||
}
|
||||
119
frontend/src/styles/toolbar.css
Normal file
119
frontend/src/styles/toolbar.css
Normal file
@@ -0,0 +1,119 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
Reference in New Issue
Block a user