refactor & improvements

This commit is contained in:
2026-03-29 22:35:57 +03:00
parent cdaf91927b
commit f0a1835fa2
63 changed files with 4803 additions and 7387 deletions

View File

@@ -147,12 +147,7 @@ function applyZoom(delta: number, clientX: number, clientY: number) {
const mousePageMmX = (clientX - pageRect.left) / oldScale
const mousePageMmY = (clientY - pageRect.top) / oldScale
// Flex centering kayması: sayfa genişliği değişince ortalama kayar
// X ekseni: justify-content: center → kayma = (eskiBoyut - yeniBoyut) / 2
const pageW = templateStore.template.page.width
const centerShiftX = pageW * (oldScale - newScale) / 2
// Y ekseni: align-items: flex-start → kayma yok
const centerShiftY = 0
// Yeni pan: mouse'un gösterdiği mm noktası aynı ekran pozisyonunda kalmalı
const newPanX = editorStore.panX + (mousePageMmX - pageW / 2) * (oldScale - newScale)

View File

@@ -17,7 +17,7 @@ const isSelected = computed(() => editorStore.selectedElementId === props.elemen
const isContainerEl = computed(() => isContainer(props.element))
const isAbsolute = computed(() => props.element.position.type === 'absolute')
// --- CSS style: layout'u Typst ile eşleştir ---
// --- CSS style: layout engine sonuçlarına göre ---
const layoutStyle = computed(() => {
const el = props.element
const s = props.scale

View File

@@ -6,6 +6,7 @@ import type { ElementLayout } from '../../core/layout-types'
import type { TemplateElement, SizeValue, ContainerElement } from '../../core/types'
import { isContainer, sz } from '../../core/types'
import ElementToolbar from './ElementToolbar.vue'
import { useSnapGuides } from '../../composables/useSnapGuides'
const props = defineProps<{
scale: number
@@ -14,6 +15,7 @@ const props = defineProps<{
const templateStore = useTemplateStore()
const editorStore = useEditorStore()
const { activeGuides, collectEdges, calculateSnap, calculateResizeSnap, clearGuides } = useSnapGuides()
// Tüm elemanları flat olarak topla (root hariç)
const flatElements = computed(() => {
@@ -382,6 +384,8 @@ function onAbsoluteDragStart(e: PointerEvent, el: TemplateElement) {
elY: el.position.y,
}
collectEdges(props.layoutMap, el.id, templateStore.template.page.width, templateStore.template.page.height)
window.addEventListener('pointermove', onAbsoluteDragMove)
window.addEventListener('pointerup', onAbsoluteDragEnd)
}
@@ -400,8 +404,16 @@ function onAbsoluteDragMove(e: PointerEvent) {
}
const pxToMm = 1 / props.scale
const newX = Math.max(0, absoluteDragStart.value.elX + dx * pxToMm)
const newY = Math.max(0, absoluteDragStart.value.elY + dy * pxToMm)
const proposedX = Math.max(0, absoluteDragStart.value.elX + dx * pxToMm)
const proposedY = Math.max(0, absoluteDragStart.value.elY + dy * pxToMm)
const layout = props.layoutMap[absoluteDragId.value]
const elW = layout ? layout.width_mm : 0
const elH = layout ? layout.height_mm : 0
const snap = calculateSnap(proposedX, proposedY, elW, elH)
const newX = snap.snappedX_mm
const newY = snap.snappedY_mm
templateStore.updateElementPosition(absoluteDragId.value, {
type: 'absolute',
@@ -417,6 +429,7 @@ function onAbsoluteDragEnd() {
isDragging.value = false
absoluteDragId.value = null
editorStore.setDragging(false)
clearGuides()
setTimeout(() => { didDrag.value = false }, 50)
}
@@ -455,6 +468,8 @@ function onResizeStart(e: PointerEvent, elId: string, handle: string) {
resizeGhost.value = { x: l.x_mm * s, y: l.y_mm * s, width: l.width_mm * s, height: l.height_mm * s }
resizeFinalMm.value = { width: l.width_mm, height: l.height_mm }
collectEdges(props.layoutMap, elId, templateStore.template.page.width, templateStore.template.page.height)
window.addEventListener('pointermove', onResizeMove)
window.addEventListener('pointerup', onResizeEnd)
}
@@ -485,11 +500,25 @@ function onResizeMove(e: PointerEvent) {
const startWMm = resizeStart.value.width * pxToMm
const startHMm = resizeStart.value.height * pxToMm
const startXMm = resizeStart.value.x * pxToMm
const startYMm = resizeStart.value.y * pxToMm
let wMm = startWMm, hMm = startHMm
if (handle.includes('e')) wMm = Math.max(5, startWMm + dx * pxToMm)
if (handle.includes('w')) wMm = Math.max(5, startWMm - dx * pxToMm)
if (handle.includes('s')) hMm = Math.max(3, startHMm + dy * pxToMm)
if (handle.includes('n')) hMm = Math.max(3, startHMm - dy * pxToMm)
if (handle.includes('e')) {
const rightEdge = calculateResizeSnap('right', startXMm + startWMm + dx * pxToMm)
wMm = Math.max(5, rightEdge - startXMm)
}
if (handle.includes('w')) {
const leftEdge = calculateResizeSnap('left', startXMm + dx * pxToMm)
wMm = Math.max(5, startXMm + startWMm - leftEdge)
}
if (handle.includes('s')) {
const bottomEdge = calculateResizeSnap('bottom', startYMm + startHMm + dy * pxToMm)
hMm = Math.max(3, bottomEdge - startYMm)
}
if (handle.includes('n')) {
const topEdge = calculateResizeSnap('top', startYMm + dy * pxToMm)
hMm = Math.max(3, startYMm + startHMm - topEdge)
}
if (ar > 0) {
hMm = wMm / ar
@@ -519,6 +548,7 @@ function onResizeEnd() {
isResizing.value = false
resizeElementId.value = null
resizeHandle.value = ''
clearGuides()
}
// ============================================================
@@ -629,6 +659,23 @@ const isAnyDragActive = computed(() =>
<!-- Drop indicator (ortak hem eleman hem toolbox sürükleme) -->
<div v-if="isAnyDragActive" :style="dropIndicatorStyle" />
<!-- Snap guides -->
<div
v-for="(guide, gi) in activeGuides"
:key="'guide-' + gi"
class="snap-guide"
:style="{
position: 'absolute',
...(guide.type === 'vertical'
? { left: `${guide.position_mm * scale}px`, top: '0', bottom: '0', width: '1px' }
: { top: `${guide.position_mm * scale}px`, left: '0', right: '0', height: '1px' }),
background: '#3b82f6',
opacity: 0.7,
pointerEvents: 'none',
zIndex: 9999,
}"
/>
<!-- Element toolbar seçili elemanın üstünde -->
<ElementToolbar
v-if="!isDragging && !isResizing"

View File

@@ -8,7 +8,7 @@ const props = defineProps<{
}>()
// WASM barcode üretme fonksiyonu (EditorCanvas'tan provide edilir)
const generateBarcode = inject<(format: string, value: string, width: number, height: number) => Promise<{ width: number; height: number; rgba: ArrayBuffer } | null>>('generateBarcode')
const generateBarcode = inject<(format: string, value: string, width: number, height: number, includeText: boolean) => Promise<{ width: number; height: number; rgba: ArrayBuffer } | null>>('generateBarcode')
const pageElements = computed(() => {
if (!props.layout || props.layout.pages.length === 0) return []

View File

@@ -1,35 +0,0 @@
<script setup lang="ts">
defineProps<{
svg: string | null
}>()
</script>
<template>
<div class="typst-svg-layer" v-if="svg" v-html="svg" />
<div class="typst-svg-layer typst-svg-layer--empty" v-else>
<span>Derleniyor...</span>
</div>
</template>
<style scoped>
.typst-svg-layer {
position: absolute;
inset: 0;
pointer-events: none;
user-select: none;
}
.typst-svg-layer :deep(svg) {
width: 100%;
height: 100%;
display: block;
}
.typst-svg-layer--empty {
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 14px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,266 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { SchemaNode } from '../../core/schema-parser'
import { schemaFormatToFormatType, defaultAlignForSchema } from '../../core/schema-parser'
import type { TemplateElement, RepeatingTableElement, TableColumn } from '../../core/types'
import { sz } from '../../core/types'
import { useEditorStore } from '../../stores/editor'
import { useSchemaStore } from '../../stores/schema'
const props = withDefaults(defineProps<{
node: SchemaNode
depth?: number
}>(), {
depth: 0,
})
const editorStore = useEditorStore()
const schemaStore = useSchemaStore()
const expanded = ref(props.depth < 2)
let colIdCounter = 0
const isScalar = ['string', 'number', 'integer', 'boolean'].includes(props.node.type)
const isArray = props.node.type === 'array'
const isObject = props.node.type === 'object'
const isDraggable = isScalar || isArray
const hasChildren = isObject
? props.node.children.length > 0
: isArray
? (props.node.itemProperties?.length ?? 0) > 0
: false
const typeIcon: Record<string, string> = {
string: 'Aa',
number: '#',
integer: '#',
boolean: '\u2713',
object: '{ }',
array: '[ ]',
}
const borderColor: Record<string, string> = {
string: '#3b82f6',
number: '#22c55e',
integer: '#22c55e',
boolean: '#f59e0b',
object: '#94a3b8',
array: '#8b5cf6',
}
function toggle() {
if (hasChildren) {
expanded.value = !expanded.value
}
}
function createBoundTextElement(node: SchemaNode): TemplateElement {
return {
id: `txt_${Date.now().toString(36)}`,
type: 'text',
position: { type: 'flow' },
size: { width: sz.auto(), height: sz.auto() },
style: { fontSize: 11, color: '#000000' },
binding: { type: 'scalar', path: node.path },
}
}
function createBoundTableElement(node: SchemaNode): RepeatingTableElement {
const itemFields = schemaStore.getArrayItemFields(node.path)
const columns: TableColumn[] = itemFields.map(field => ({
id: `col_${(++colIdCounter).toString(36)}`,
field: field.key,
title: field.title,
width: sz.auto(),
align: defaultAlignForSchema(field),
format: schemaFormatToFormatType(field.format),
}))
return {
id: `tbl_${Date.now().toString(36)}`,
type: 'repeating_table',
position: { type: 'flow' },
size: { width: sz.fr(1), height: sz.auto() },
dataSource: { type: 'array', path: node.path },
columns,
style: { headerBg: '#f0f0f0', headerColor: '#000000', fontSize: 10, headerFontSize: 10 },
}
}
function onDragStart(e: DragEvent) {
if (!isDraggable) return
let el: TemplateElement
if (isScalar) {
el = createBoundTextElement(props.node)
} else {
el = createBoundTableElement(props.node)
}
editorStore.startDragNewElement(el)
e.dataTransfer?.setData('text/plain', el.id)
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'copy'
}
}
function onDragEnd() {
editorStore.endDragNewElement()
}
const displayChildren = isArray
? (props.node.itemProperties ?? [])
: props.node.children
</script>
<template>
<div class="schema-node">
<div
class="schema-node__row"
:class="{
'schema-node__row--draggable': isDraggable,
'schema-node__row--object': isObject,
}"
:style="{
paddingLeft: `${depth * 16 + 8}px`,
borderLeftColor: borderColor[node.type] ?? '#94a3b8',
}"
:draggable="isDraggable"
:title="node.path || node.key"
@click="toggle"
@dragstart="onDragStart"
@dragend="onDragEnd"
>
<span v-if="hasChildren" class="schema-node__arrow" :class="{ 'schema-node__arrow--expanded': expanded }">
&#9654;
</span>
<span v-else class="schema-node__arrow-placeholder" />
<span class="schema-node__type-icon" :class="`schema-node__type-icon--${node.type}`">
{{ typeIcon[node.type] ?? '?' }}
</span>
<span class="schema-node__title">{{ node.title }}</span>
<span v-if="isScalar && node.path" class="schema-node__path">{{ node.path }}</span>
</div>
<div v-if="hasChildren && expanded" class="schema-node__children">
<SchemaTreeNode
v-for="child in displayChildren"
:key="child.path"
:node="child"
:depth="depth + 1"
/>
</div>
</div>
</template>
<style scoped>
.schema-node__row {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 8px;
border-left: 3px solid transparent;
cursor: default;
user-select: none;
font-size: 13px;
color: #334155;
border-radius: 0 4px 4px 0;
transition: background 0.12s;
}
.schema-node__row--draggable {
cursor: grab;
}
.schema-node__row--draggable:active {
cursor: grabbing;
}
.schema-node__row:hover {
background: #f1f5f9;
}
.schema-node__row--draggable:hover {
background: #eff6ff;
}
.schema-node__arrow {
width: 14px;
height: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
color: #94a3b8;
transition: transform 0.15s;
flex-shrink: 0;
}
.schema-node__arrow--expanded {
transform: rotate(90deg);
}
.schema-node__arrow-placeholder {
width: 14px;
flex-shrink: 0;
}
.schema-node__type-icon {
width: 22px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
font-size: 10px;
font-weight: 700;
flex-shrink: 0;
font-family: monospace;
}
.schema-node__type-icon--string {
background: #dbeafe;
color: #2563eb;
}
.schema-node__type-icon--number,
.schema-node__type-icon--integer {
background: #dcfce7;
color: #16a34a;
}
.schema-node__type-icon--boolean {
background: #fef3c7;
color: #d97706;
}
.schema-node__type-icon--object {
background: #f1f5f9;
color: #64748b;
}
.schema-node__type-icon--array {
background: #ede9fe;
color: #7c3aed;
}
.schema-node__title {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.schema-node__path {
font-size: 10px;
color: #94a3b8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 80px;
}
</style>

View File

@@ -0,0 +1,51 @@
<script setup lang="ts">
import { useSchemaStore } from '../../stores/schema'
import SchemaTreeNode from './SchemaTreeNode.vue'
const schemaStore = useSchemaStore()
</script>
<template>
<div class="schema-panel">
<div class="schema-panel__title">Schema</div>
<div v-if="schemaStore.schemaTree.children.length === 0" class="schema-panel__empty">
Schema yuklu degil
</div>
<div v-else class="schema-panel__tree">
<SchemaTreeNode
v-for="child in schemaStore.schemaTree.children"
:key="child.path"
:node="child"
:depth="0"
/>
</div>
</div>
</template>
<style scoped>
.schema-panel {
padding: 12px 0;
}
.schema-panel__title {
font-size: 11px;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
padding: 0 12px;
}
.schema-panel__empty {
padding: 20px 12px;
color: #94a3b8;
font-size: 13px;
text-align: center;
}
.schema-panel__tree {
display: flex;
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,152 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useTemplateStore } from '../../stores/template'
import { useEditorStore } from '../../stores/editor'
import { useSchemaStore } from '../../stores/schema'
import type { BarcodeElement, BarcodeFormat, TemplateElement } from '../../core/types'
import '../../styles/properties.css'
const props = defineProps<{ element: BarcodeElement }>()
const templateStore = useTemplateStore()
const editorStore = useEditorStore()
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 barcodeDefaults: Record<BarcodeFormat, string> = {
qr: 'https://example.com',
ean13: '5901234123457',
ean8: '96385074',
code128: 'DREPORT-001',
code39: 'DREPORT',
}
function eanCheckDigit(data: string): number {
let sum = 0
for (let i = 0; i < data.length; i++) {
const d = parseInt(data[i])
sum += d * (i % 2 === 0 ? 1 : 3)
}
return (10 - (sum % 10)) % 10
}
function validateBarcode(format: BarcodeFormat, value: string): boolean {
if (!value) return false
switch (format) {
case 'ean13':
if (!/^\d{13}$/.test(value)) return false
return eanCheckDigit(value.slice(0, 12)) === parseInt(value[12])
case 'ean8':
if (!/^\d{8}$/.test(value)) return false
return eanCheckDigit(value.slice(0, 7)) === parseInt(value[7])
case 'code39':
return /^[A-Z0-9\-. $/+%]+$/i.test(value)
case 'code128':
return value.length > 0 && [...value].every(c => c.charCodeAt(0) < 128)
case 'qr':
return value.length > 0
default:
return value.length > 0
}
}
const barcodeInputValue = ref('')
const barcodeInputInvalid = ref(false)
watch(() => props.element.value ?? '', (val) => {
barcodeInputValue.value = val
barcodeInputInvalid.value = false
}, { immediate: true })
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)
} else {
barcodeInputInvalid.value = true
}
}
function onBarcodeFormatChange(newFormat: BarcodeFormat) {
const currentValue = props.element.value ?? ''
if (validateBarcode(newFormat, currentValue)) {
update({ format: newFormat } as any)
} else {
const defaultVal = barcodeDefaults[newFormat]
barcodeInputValue.value = defaultVal
barcodeInputInvalid.value = false
update({ format: newFormat, value: defaultVal } as any)
}
}
</script>
<template>
<div class="prop-section">
<div class="prop-section__title">Barkod Ayarlari</div>
<div class="prop-row">
<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>
<div class="prop-row">
<label class="prop-label">Deger</label>
<input class="prop-input" type="text"
:class="{ 'prop-input--invalid': barcodeInputInvalid }"
:value="barcodeInputValue"
@input="onBarcodeValueInput" />
</div>
<div class="prop-row">
<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 v-if="element.format !== 'qr'" class="prop-row">
<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 v-if="schemaStore.scalarFields.length > 0" class="prop-row">
<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>
</template>

View File

@@ -0,0 +1,115 @@
<script setup lang="ts">
import { useTemplateStore } from '../../stores/template'
import { useEditorStore } from '../../stores/editor'
import PaddingBox from './PaddingBox.vue'
import type { ContainerElement, TemplateElement } from '../../core/types'
import '../../styles/properties.css'
const props = defineProps<{ element: ContainerElement }>()
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>)
}
</script>
<template>
<div class="prop-section">
<div class="prop-section__title">Container Ayarlari</div>
<div class="prop-row">
<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">
<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>
<div class="prop-row">
<label class="prop-label">{{ element.direction === 'column' ? 'Yatay Hizalama' : 'Dikey Hizalama' }}</label>
<select class="prop-input prop-select"
:value="element.align"
@change="(e) => update({ align: (e.target as HTMLSelectElement).value } as any)">
<option value="start">{{ element.direction === 'column' ? 'Sol' : 'Ust' }}</option>
<option value="center">Orta</option>
<option value="end">{{ element.direction === 'column' ? 'Sag' : 'Alt' }}</option>
<option value="stretch">Esnet</option>
</select>
</div>
<div class="prop-row">
<label class="prop-label">{{ element.direction === 'column' ? 'Dikey Dagilim' : 'Yatay Dagilim' }}</label>
<select class="prop-input prop-select"
:value="element.justify"
@change="(e) => update({ justify: (e.target as HTMLSelectElement).value } as any)">
<option value="start">{{ element.direction === 'column' ? 'Ust' : 'Sol' }}</option>
<option value="center">Orta</option>
<option value="end">{{ element.direction === 'column' ? 'Alt' : 'Sag' }}</option>
<option value="space-between">Esit Aralik</option>
</select>
</div>
<div class="prop-section__subtitle">Padding (mm)</div>
<PaddingBox
:top="element.padding.top"
:right="element.padding.right"
:bottom="element.padding.bottom"
:left="element.padding.left"
@update="(side, value) => update({ padding: { ...element.padding, [side]: value } } as any)"
/>
<div class="prop-section__subtitle">Stil</div>
<div class="prop-row">
<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">
<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">
<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">
<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">
<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>
</template>

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import { useTemplateStore } from '../../stores/template'
import { useEditorStore } from '../../stores/editor'
import type { ImageElement, TemplateElement } from '../../core/types'
import '../../styles/properties.css'
const props = defineProps<{ element: ImageElement }>()
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>)
}
function onImageFileSelect(e: Event) {
const input = e.target as HTMLInputElement
const file = input.files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = () => {
update({ src: reader.result as string } as Partial<TemplateElement>)
}
reader.readAsDataURL(file)
}
</script>
<template>
<div class="prop-section">
<div class="prop-section__title">Gorsel</div>
<div class="prop-row">
<label class="prop-label">Kaynak</label>
<label class="prop-file-btn">
Dosya Sec
<input type="file" accept="image/*" style="display: none" @change="onImageFileSelect" />
</label>
</div>
<div v-if="element.src" class="prop-row">
<label class="prop-label">Onizleme</label>
<img :src="element.src" class="prop-image-preview" />
</div>
<div v-if="element.src" class="prop-row">
<label class="prop-label"></label>
<button class="prop-clear" @click="update({ src: undefined } as any)">Gorseli kaldir</button>
</div>
<div class="prop-row">
<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>
</template>

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import { useTemplateStore } from '../../stores/template'
import { useEditorStore } from '../../stores/editor'
import type { LineElement, TemplateElement } 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>)
}
</script>
<template>
<div class="prop-section">
<div class="prop-section__title">Cizgi Stili</div>
<div class="prop-row">
<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">
<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>
</template>

View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import { useTemplateStore } from '../../stores/template'
import { useEditorStore } from '../../stores/editor'
import type { PageNumberElement, TextStyle, TemplateElement } from '../../core/types'
import '../../styles/properties.css'
const props = defineProps<{ element: PageNumberElement }>()
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>)
}
</script>
<template>
<div class="prop-section">
<div class="prop-section__title">Sayfa Numarasi</div>
<div class="prop-row">
<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">
<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">
<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">
<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>
</template>

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import { useTemplateStore } from '../../stores/template'
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') {
templateStore.updateElementPosition(props.element.id, { type: 'absolute', x: 0, y: 0 })
} else {
templateStore.updateElementPosition(props.element.id, { type: 'flow' })
}
}
</script>
<template>
<div class="prop-section">
<div class="prop-section__title">Pozisyon</div>
<div class="prop-row">
<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>
<template v-if="element.position.type === 'absolute'">
<div class="prop-row">
<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">
<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>
</template>
</div>
</template>

View File

@@ -0,0 +1,242 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useTemplateStore } from '../../stores/template'
import { useEditorStore } from '../../stores/editor'
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 '../../styles/properties.css'
const props = defineProps<{ element: RepeatingTableElement }>()
const templateStore = useTemplateStore()
const editorStore = useEditorStore()
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)}`
}
function updateTableDataSource(path: string) {
const itemFields = schemaStore.getArrayItemFields(path)
if (itemFields.length > 0) {
const columns: TableColumn[] = itemFields.map(field => ({
id: nextColId(),
field: field.key,
title: field.title,
width: sz.auto(),
align: defaultAlignForSchema(field),
format: schemaFormatToFormatType(field.format),
}))
update({
dataSource: { type: 'array', path },
columns,
} as Partial<TemplateElement>)
} else {
update({ dataSource: { type: 'array', path } } as Partial<TemplateElement>)
}
}
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>)
}
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>)
}
function addColumn() {
const newCol: TableColumn = {
id: nextColId(),
field: 'alan',
title: 'Yeni Sutun',
width: sz.auto(),
align: 'left',
}
update({ columns: [...props.element.columns, newCol] } as Partial<TemplateElement>)
}
function removeColumn(colId: string) {
update({ columns: props.element.columns.filter(c => c.id !== colId) } as Partial<TemplateElement>)
}
function moveColumn(colId: string, direction: -1 | 1) {
const cols = [...props.element.columns]
const idx = cols.findIndex(c => c.id === colId)
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>)
}
const tableItemFields = computed(() => {
return schemaStore.getArrayItemFields(props.element.dataSource.path)
})
</script>
<template>
<!-- Data source -->
<div class="prop-section">
<div class="prop-section__title">Veri Kaynagi</div>
<div class="prop-row">
<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>
<!-- Columns -->
<div class="prop-section">
<div class="prop-section__title">
Sutunlar
<button class="prop-add-btn" @click="addColumn">+</button>
</div>
<div
v-for="col in element.columns"
:key="col.id"
class="prop-column-card"
>
<div class="prop-column-header">
<span class="prop-column-title">{{ col.title || col.field }}</span>
<div class="prop-column-actions">
<button class="prop-icon-btn" @click="moveColumn(col.id, -1)" title="Yukari">&#8593;</button>
<button class="prop-icon-btn" @click="moveColumn(col.id, 1)" title="Asagi">&#8595;</button>
<button class="prop-icon-btn prop-icon-btn--danger" @click="removeColumn(col.id)" title="Sil">x</button>
</div>
</div>
<div class="prop-row">
<label class="prop-label">Baslik</label>
<input class="prop-input" type="text" :value="col.title"
@change="(e) => updateColumn(col.id, { title: (e.target as HTMLInputElement).value })" />
</div>
<div class="prop-row">
<label class="prop-label">Alan</label>
<select v-if="tableItemFields.length > 0" class="prop-input prop-select" :value="col.field"
@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.title }} ({{ f.key }})</option>
</select>
<input v-else class="prop-input" type="text" :value="col.field"
@change="(e) => updateColumn(col.id, { field: (e.target as HTMLInputElement).value })" />
</div>
<div class="prop-row">
<label class="prop-label">Hizalama</label>
<select class="prop-input prop-select" :value="col.align"
@change="(e) => updateColumn(col.id, { align: (e.target as HTMLSelectElement).value as 'left'|'center'|'right' })">
<option value="left">Sol</option>
<option value="center">Orta</option>
<option value="right">Sag</option>
</select>
</div>
<div class="prop-row">
<label class="prop-label">Format</label>
<select class="prop-input prop-select" :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="prop-row">
<label class="prop-label">Genislik</label>
<select class="prop-input prop-select"
: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>
</div>
<div v-if="col.width.type === 'fixed'" class="prop-row">
<label class="prop-label">mm</label>
<input class="prop-input" type="number" step="1" min="5"
:value="(col.width as any).value"
@change="(e) => updateColumn(col.id, { width: { type: 'fixed', value: parseFloat((e.target as HTMLInputElement).value) || 30 } })" />
</div>
</div>
</div>
<!-- Table style -->
<div class="prop-section">
<div class="prop-section__title">Tablo Stili</div>
<div class="prop-row">
<label class="prop-label">Yazi boyutu</label>
<input class="prop-input" type="number" step="1" min="6"
:value="element.style.fontSize ?? 10"
@input="(e) => updateTableStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)" />
</div>
<div class="prop-row">
<label class="prop-label">Header bg</label>
<input class="prop-input prop-color" type="color"
:value="element.style.headerBg ?? '#f0f0f0'"
@input="(e) => updateTableStyle('headerBg', (e.target as HTMLInputElement).value)" />
</div>
<div class="prop-row">
<label class="prop-label">Header renk</label>
<input class="prop-input prop-color" type="color"
:value="element.style.headerColor ?? '#000000'"
@input="(e) => updateTableStyle('headerColor', (e.target as HTMLInputElement).value)" />
</div>
<div class="prop-row">
<label class="prop-label">Zebra tek</label>
<div class="prop-row-inline">
<input class="prop-input prop-color" type="color"
:value="element.style.zebraOdd ?? '#fafafa'"
@input="(e) => updateTableStyle('zebraOdd', (e.target as HTMLInputElement).value)" />
<button v-if="element.style.zebraOdd" class="prop-clear" @click="updateTableStyle('zebraOdd', undefined)">x</button>
</div>
</div>
<div class="prop-row">
<label class="prop-label">Kenarlik rengi</label>
<div class="prop-row-inline">
<input class="prop-input prop-color" type="color"
:value="element.style.borderColor ?? '#cccccc'"
@input="(e) => updateTableStyle('borderColor', (e.target as HTMLInputElement).value)" />
<button v-if="element.style.borderColor" class="prop-clear" @click="updateTableStyle('borderColor', undefined)">x</button>
</div>
</div>
<div class="prop-row">
<label class="prop-label">Kenarlik (mm)</label>
<input class="prop-input" type="number" step="0.1" min="0"
:value="element.style.borderWidth ?? 0.5"
@input="(e) => updateTableStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)" />
</div>
</div>
</template>

View File

@@ -0,0 +1,67 @@
<script setup lang="ts">
import { useTemplateStore } from '../../stores/template'
import type { TemplateElement, SizeValue } from '../../core/types'
import '../../styles/properties.css'
const props = defineProps<{ element: TemplateElement }>()
const templateStore = useTemplateStore()
function updateSize(axis: 'width' | 'height', sv: SizeValue) {
templateStore.updateElementSize(props.element.id, { [axis]: sv })
}
</script>
<template>
<div class="prop-section">
<div class="prop-section__title">Boyut</div>
<div class="prop-row">
<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 })
}">
<option value="auto">Otomatik</option>
<option value="fixed">Sabit (mm)</option>
<option value="fr">Oran (fr)</option>
</select>
</div>
<div v-if="element.size.width.type === 'fixed'" class="prop-row">
<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 v-if="element.size.width.type === 'fr'" class="prop-row">
<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>
<div class="prop-row">
<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 })
}">
<option value="auto">Otomatik</option>
<option value="fixed">Sabit (mm)</option>
<option value="fr">Oran (fr)</option>
</select>
</div>
<div v-if="element.size.height.type === 'fixed'" class="prop-row">
<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>
</template>

View File

@@ -0,0 +1,65 @@
<script setup lang="ts">
import { useTemplateStore } from '../../stores/template'
import { useEditorStore } from '../../stores/editor'
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>)
}
</script>
<template>
<div class="prop-section">
<div class="prop-section__title">Metin Stili</div>
<div v-if="element.type === 'static_text'" class="prop-row">
<label class="prop-label">Metin</label>
<input class="prop-input" type="text"
:value="(element as StaticTextElement).content"
@input="(e) => update({ content: (e.target as HTMLInputElement).value } as any)" />
</div>
<div class="prop-row">
<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">
<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">
<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">
<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>
</template>