format
Some checks failed
CI / frontend (push) Successful in 1m55s
CI / publish-crates (push) Successful in 23s
CI / rust (push) Successful in 49s
CI / wasm (push) Successful in 1m44s
CI / publish-npm (push) Failing after 1m50s

This commit is contained in:
2026-04-07 01:56:40 +03:00
parent 33f7556b03
commit 5ffc6d866c
42 changed files with 2996 additions and 780 deletions

View File

@@ -94,9 +94,13 @@ if (savedSchema) {
currentSchema.value = savedSchema
}
watch(currentSchema, (val) => {
localStorage.setItem(SCHEMA_STORAGE_KEY, JSON.stringify(val))
}, { deep: true })
watch(
currentSchema,
(val) => {
localStorage.setItem(SCHEMA_STORAGE_KEY, JSON.stringify(val))
},
{ deep: true },
)
// --- Sample Invoice Data ---
@@ -125,10 +129,38 @@ const sampleData: Record<string, unknown> = {
telefon: '+90 216 444 0018',
},
kalemler: [
{ siraNo: 1, adi: 'Web Uygulama Gelistirme', miktar: 1, birim: 'Adet', birimFiyat: 45000, tutar: 45000 },
{ siraNo: 2, adi: 'Mobil Uygulama Gelistirme', miktar: 1, birim: 'Adet', birimFiyat: 35000, tutar: 35000 },
{ siraNo: 3, adi: 'UI/UX Tasarim Hizmeti', miktar: 40, birim: 'Saat', birimFiyat: 750, tutar: 30000 },
{ siraNo: 4, adi: 'Sunucu Bakim Sozlesmesi (Yillik)', miktar: 1, birim: 'Adet', birimFiyat: 12000, tutar: 12000 },
{
siraNo: 1,
adi: 'Web Uygulama Gelistirme',
miktar: 1,
birim: 'Adet',
birimFiyat: 45000,
tutar: 45000,
},
{
siraNo: 2,
adi: 'Mobil Uygulama Gelistirme',
miktar: 1,
birim: 'Adet',
birimFiyat: 35000,
tutar: 35000,
},
{
siraNo: 3,
adi: 'UI/UX Tasarim Hizmeti',
miktar: 40,
birim: 'Saat',
birimFiyat: 750,
tutar: 30000,
},
{
siraNo: 4,
adi: 'Sunucu Bakim Sozlesmesi (Yillik)',
miktar: 1,
birim: 'Adet',
birimFiyat: 12000,
tutar: 12000,
},
{ siraNo: 5, adi: 'SSL Sertifikasi', miktar: 3, birim: 'Adet', birimFiyat: 500, tutar: 1500 },
],
toplamlar: {
@@ -370,10 +402,30 @@ const defaultInvoiceTemplate: Template = {
columns: [
{ id: 'col_sira', field: 'siraNo', title: '#', width: sz.fixed(10), align: 'center' },
{ id: 'col_adi', field: 'adi', title: 'Urun / Hizmet', width: sz.fr(), align: 'left' },
{ id: 'col_miktar', field: 'miktar', title: 'Miktar', width: sz.fixed(18), align: 'right' },
{
id: 'col_miktar',
field: 'miktar',
title: 'Miktar',
width: sz.fixed(18),
align: 'right',
},
{ id: 'col_birim', field: 'birim', title: 'Birim', width: sz.fixed(18), align: 'center' },
{ id: 'col_fiyat', field: 'birimFiyat', title: 'Birim Fiyat', width: sz.fixed(28), align: 'right', format: 'currency' as const },
{ id: 'col_tutar', field: 'tutar', title: 'Tutar', width: sz.fixed(28), align: 'right', format: 'currency' as const },
{
id: 'col_fiyat',
field: 'birimFiyat',
title: 'Birim Fiyat',
width: sz.fixed(28),
align: 'right',
format: 'currency' as const,
},
{
id: 'col_tutar',
field: 'tutar',
title: 'Tutar',
width: sz.fixed(28),
align: 'right',
format: 'currency' as const,
},
],
style: {
fontSize: 9,
@@ -486,12 +538,16 @@ function loadFromLocalStorage(): Template | null {
const template = ref<Template>(loadFromLocalStorage() ?? structuredClone(defaultInvoiceTemplate))
let saveTimeout: ReturnType<typeof setTimeout> | null = null
watch(template, (val) => {
if (saveTimeout) clearTimeout(saveTimeout)
saveTimeout = setTimeout(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(val))
}, 500)
}, { deep: true })
watch(
template,
(val) => {
if (saveTimeout) clearTimeout(saveTimeout)
saveTimeout = setTimeout(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(val))
}, 500)
},
{ deep: true },
)
// --- Editor ref ---
@@ -626,36 +682,120 @@ function resetTemplate() {
<h1>dreport</h1>
<span class="app-header__subtitle">Belge Tasarim Araci</span>
<div style="flex: 1"></div>
<input ref="fileInputRef" type="file" accept=".json" style="display: none" @change="onImportFile" />
<input ref="schemaFileInputRef" type="file" accept=".json" style="display: none" @change="onSchemaImportFile" />
<input
ref="fileInputRef"
type="file"
accept=".json"
style="display: none"
@change="onImportFile"
/>
<input
ref="schemaFileInputRef"
type="file"
accept=".json"
style="display: none"
@change="onSchemaImportFile"
/>
<!-- Template operations -->
<button class="header-btn header-btn--secondary" @click="resetTemplate" title="Sifirla">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8a6 6 0 0 1 10.2-4.3L14 2v4h-4l1.7-1.7A4.5 4.5 0 1 0 12.5 8" /><path d="M12.5 8a4.5 4.5 0 0 1-8.2 2.5" /></svg>
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2 8a6 6 0 0 1 10.2-4.3L14 2v4h-4l1.7-1.7A4.5 4.5 0 1 0 12.5 8" />
<path d="M12.5 8a4.5 4.5 0 0 1-8.2 2.5" />
</svg>
Sifirla
</button>
<button class="header-btn header-btn--secondary" @click="triggerImport" title="Sablon Yukle">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 10V2m0 0L5 5m3-3 3 3" /><path d="M2 10v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2" /></svg>
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 10V2m0 0L5 5m3-3 3 3" />
<path d="M2 10v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2" />
</svg>
Yukle
</button>
<button class="header-btn header-btn--secondary" @click="exportTemplate" title="Sablon Kaydet">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v8m0 0 3-3m-3 3L5 7" /><path d="M2 10v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2" /></svg>
<button
class="header-btn header-btn--secondary"
@click="exportTemplate"
title="Sablon Kaydet"
>
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 2v8m0 0 3-3m-3 3L5 7" />
<path d="M2 10v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2" />
</svg>
Kaydet
</button>
<button class="header-btn header-btn--secondary" @click="exportBundle" title="Sablon + Schema Birlikte Kaydet">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="1" width="12" height="14" rx="1.5" /><path d="M5 4h6M5 7h6M5 10h4" /></svg>
<button
class="header-btn header-btn--secondary"
@click="exportBundle"
title="Sablon + Schema Birlikte Kaydet"
>
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="2" y="1" width="12" height="14" rx="1.5" />
<path d="M5 4h6M5 7h6M5 10h4" />
</svg>
Paket
</button>
<div class="header-divider"></div>
<!-- Schema operations -->
<button class="header-btn header-btn--secondary" @click="triggerSchemaImport" title="Schema Yukle">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 10V2m0 0L5 5m3-3 3 3" /><path d="M2 10v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2" /></svg>
<button
class="header-btn header-btn--secondary"
@click="triggerSchemaImport"
title="Schema Yukle"
>
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 10V2m0 0L5 5m3-3 3 3" />
<path d="M2 10v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2" />
</svg>
Schema
</button>
<button class="header-btn header-btn--secondary" @click="exportSchema" title="Schema Kaydet">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v8m0 0 3-3m-3 3L5 7" /><path d="M2 10v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2" /></svg>
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 2v8m0 0 3-3m-3 3L5 7" />
<path d="M2 10v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2" />
</svg>
Schema
</button>
@@ -663,7 +803,17 @@ function resetTemplate() {
<!-- Output -->
<button class="header-btn" :disabled="pdfLoading" @click="downloadPdf">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="1" width="10" height="14" rx="1.5" /><path d="M6 5h4M6 8h4M6 11h2" /></svg>
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="1" width="10" height="14" rx="1.5" />
<path d="M6 5h4M6 8h4M6 11h2" />
</svg>
{{ pdfLoading ? 'Hazirlaniyor...' : 'PDF Onizle' }}
</button>
</header>

View File

@@ -90,7 +90,7 @@ function schemaToLanguageInfo(): DexprLanguageInfo {
const tree = schemaStore.schemaTree
for (const child of tree.children) {
if (child.type === 'object') {
const fields = child.children.map(f => ({
const fields = child.children.map((f) => ({
name: f.key,
type: schemaToDexprType(f),
}))
@@ -112,7 +112,9 @@ function schemaToLanguageInfo(): DexprLanguageInfo {
return info
}
function schemaToDexprType(node: SchemaNode): 'String' | 'Number' | 'Boolean' | 'Object' | 'NumberList' | 'StringList' {
function schemaToDexprType(
node: SchemaNode,
): 'String' | 'Number' | 'Boolean' | 'Object' | 'NumberList' | 'StringList' {
switch (node.type) {
case 'number':
case 'integer':
@@ -134,7 +136,7 @@ function createState(doc: string): EditorState {
return EditorState.create({
doc,
extensions: [
EditorView.updateListener.of(update => {
EditorView.updateListener.of((update) => {
if (update.docChanged) {
const val = update.state.doc.toString()
if (val !== props.modelValue) {
@@ -207,22 +209,29 @@ onBeforeUnmount(() => {
})
// Disaridan gelen deger degisikligi (undo/redo vs.)
watch(() => props.modelValue, (newVal) => {
if (!view) return
const current = view.state.doc.toString()
if (current !== newVal) {
view.dispatch({
changes: { from: 0, to: current.length, insert: newVal ?? '' },
})
}
})
watch(
() => props.modelValue,
(newVal) => {
if (!view) return
const current = view.state.doc.toString()
if (current !== newVal) {
view.dispatch({
changes: { from: 0, to: current.length, insert: newVal ?? '' },
})
}
},
)
// Schema degisince editor'u yeniden olustur (autocomplete guncellenmeli)
watch(langInfo, () => {
if (!view) return
const doc = view.state.doc.toString()
view.setState(createState(doc))
}, { deep: true })
watch(
langInfo,
() => {
if (!view) return
const doc = view.state.doc.toString()
view.setState(createState(doc))
},
{ deep: true },
)
</script>
<template>

View File

@@ -8,11 +8,14 @@ import LayoutRenderer from './LayoutRenderer.vue'
import InteractionOverlay from './InteractionOverlay.vue'
import RulerBar from './RulerBar.vue'
const props = withDefaults(defineProps<{
handleErrors?: boolean
}>(), {
handleErrors: true,
})
const props = withDefaults(
defineProps<{
handleErrors?: boolean
}>(),
{
handleErrors: true,
},
)
const templateStore = useTemplateStore()
const editorStore = useEditorStore()
@@ -26,7 +29,14 @@ const emit = defineEmits<{
}>()
// Layout engine — template + data'yı worker'a gönderir, WASM ile layout hesaplar
const { layout, layoutMap, error, computing: compiling, generateBarcode, dispose } = useLayoutEngine(template, mockData, layoutVersion)
const {
layout,
layoutMap,
error,
computing: compiling,
generateBarcode,
dispose,
} = useLayoutEngine(template, mockData, layoutVersion)
// LayoutRenderer'ın barcode üretmek için kullanabileceği fonksiyon
provide('generateBarcode', generateBarcode)
@@ -86,7 +96,7 @@ let resizeObserver: ResizeObserver | null = null
onMounted(() => {
if (containerRef.value) {
resizeObserver = new ResizeObserver(entries => {
resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0]
if (entry) containerWidth.value = entry.contentRect.width
})
@@ -132,10 +142,7 @@ function onWheel(e: WheelEvent) {
} else {
// İki parmak pan (touchpad) veya normal scroll
e.preventDefault()
editorStore.setPan(
editorStore.panX - e.deltaX,
editorStore.panY - e.deltaY,
)
editorStore.setPan(editorStore.panX - e.deltaX, editorStore.panY - e.deltaY)
}
}
@@ -169,7 +176,16 @@ function applyZoom(delta: number, clientX: number, clientY: number) {
}
function onKeyDown(e: KeyboardEvent) {
if (e.code === 'Space' && !e.repeat && !(e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement || e.target instanceof HTMLTextAreaElement || (e.target as HTMLElement)?.isContentEditable)) {
if (
e.code === 'Space' &&
!e.repeat &&
!(
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLSelectElement ||
e.target instanceof HTMLTextAreaElement ||
(e.target as HTMLElement)?.isContentEditable
)
) {
e.preventDefault()
spaceHeld.value = true
}
@@ -225,9 +241,18 @@ function onPointerUp(e: PointerEvent) {
@pointerup="onPointerUp"
>
<!-- Sayfalar -->
<div ref="pageRef" class="editor-canvas__pages" :style="[pagesContainerStyle, panTransform ? { transform: panTransform } : {}]">
<div
ref="pageRef"
class="editor-canvas__pages"
:style="[pagesContainerStyle, panTransform ? { transform: panTransform } : {}]"
>
<LayoutRenderer :layout="layout" :scale="scale" />
<InteractionOverlay :scale="scale" :layout-map="layoutMap" :page-count="layoutPages.length" :page-height-px="pageHeightPx" />
<InteractionOverlay
:scale="scale"
:layout-map="layoutMap"
:page-count="layoutPages.length"
:page-height-px="pageHeightPx"
/>
</div>
</div>
@@ -235,12 +260,8 @@ function onPointerUp(e: PointerEvent) {
<div v-if="props.handleErrors && error" class="editor-canvas__error">
{{ error }}
</div>
<div v-if="compiling" class="editor-canvas__compiling">
Derleniyor...
</div>
<div class="editor-canvas__zoom">
%{{ editorStore.zoomPercent }}
</div>
<div v-if="compiling" class="editor-canvas__compiling">Derleniyor...</div>
<div class="editor-canvas__zoom">%{{ editorStore.zoomPercent }}</div>
</div>
</template>

View File

@@ -59,7 +59,12 @@ const layoutStyle = computed(() => {
}
// justify (main-axis)
const justifyMap = { start: 'flex-start', center: 'center', end: 'flex-end', 'space-between': 'space-between' }
const justifyMap = {
start: 'flex-start',
center: 'center',
end: 'flex-end',
'space-between': 'space-between',
}
style.justifyContent = justifyMap[c.justify] || 'flex-start'
}

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,8 @@ const props = defineProps<{
const templateStore = useTemplateStore()
const editorStore = useEditorStore()
const { activeGuides, collectEdges, calculateSnap, calculateResizeSnap, clearGuides } = useSnapGuides()
const { activeGuides, collectEdges, calculateSnap, calculateResizeSnap, clearGuides } =
useSnapGuides()
// Tüm elemanları flat olarak topla (root hariç)
const flatElements = computed(() => {
@@ -69,7 +70,7 @@ const allContainers = computed(() => {
/** Sayfa index'ine göre y offset hesapla (sayfalar arası gap dahil) */
function pageYOffset(pageIndex: number): number {
if (pageIndex <= 0) return 0
const pageH = props.pageHeightPx ?? (templateStore.template.page.height * props.scale)
const pageH = props.pageHeightPx ?? templateStore.template.page.height * props.scale
return pageIndex * (pageH + PAGE_GAP_PX)
}
@@ -118,7 +119,11 @@ const dropVisualIndex = ref<number | null>(null)
const dropLogicalIndex = ref<number | null>(null)
/** Mouse pozisyonuna göre en derin container'ı bul */
function findDeepestContainer(mouseX: number, mouseY: number, excludeId?: string): ContainerElement {
function findDeepestContainer(
mouseX: number,
mouseY: number,
excludeId?: string,
): ContainerElement {
const s = props.scale
let best: ContainerElement = templateStore.template.root
@@ -135,7 +140,7 @@ function findDeepestContainer(mouseX: number, mouseY: number, excludeId?: string
if (mouseX >= cx && mouseX <= cx + cw && mouseY >= cy && mouseY <= cy + ch) {
// Daha küçük (daha derin) container'ı tercih et
const bestL = props.layoutMap[best.id]
if (!bestL || (cw * ch < bestL.width_mm * s * bestL.height_mm * s)) {
if (!bestL || cw * ch < bestL.width_mm * s * bestL.height_mm * s) {
best = c
}
}
@@ -144,9 +149,16 @@ function findDeepestContainer(mouseX: number, mouseY: number, excludeId?: string
}
/** Container içinde drop index hesapla */
function computeDropIndex(container: ContainerElement, mouseX: number, mouseY: number, excludeId?: string) {
function computeDropIndex(
container: ContainerElement,
mouseX: number,
mouseY: number,
excludeId?: string,
) {
const s = props.scale
const flowChildren = container.children.filter(c => c.type !== 'page_break' && c.position.type !== 'absolute' && c.id !== excludeId)
const flowChildren = container.children.filter(
(c) => c.type !== 'page_break' && c.position.type !== 'absolute' && c.id !== excludeId,
)
const isRow = container.direction === 'row'
let visualIdx = flowChildren.length
@@ -156,18 +168,26 @@ function computeDropIndex(container: ContainerElement, mouseX: number, mouseY: n
if (!l) continue
if (isRow) {
const centerX = l.x_mm * s + (l.width_mm * s) / 2
if (mouseX < centerX) { visualIdx = i; break }
if (mouseX < centerX) {
visualIdx = i
break
}
} else {
const centerY = l.y_mm * s + pageYOffset(l.pageIndex) + (l.height_mm * s) / 2
if (mouseY < centerY) { visualIdx = i; break }
if (mouseY < centerY) {
visualIdx = i
break
}
}
}
// Mantıksal index: excludeId aynı container'daysa offset hesapla
let logicalIdx = visualIdx
if (excludeId) {
const allFlow = container.children.filter(c => c.type !== 'page_break' && c.position.type !== 'absolute')
const currentIdx = allFlow.findIndex(c => c.id === excludeId)
const allFlow = container.children.filter(
(c) => c.type !== 'page_break' && c.position.type !== 'absolute',
)
const currentIdx = allFlow.findIndex((c) => c.id === excludeId)
if (currentIdx >= 0) {
// visualIdx, excludeId çıkarılmış listede. Gerçek listedeki pozisyona çevir.
// flowChildren zaten excludeId hariç, dolayısıyla visualIdx doğrudan gerçek insert indexi
@@ -177,7 +197,10 @@ function computeDropIndex(container: ContainerElement, mouseX: number, mouseY: n
let count = 0
for (let i = 0; i < allFlow.length; i++) {
if (allFlow[i].id === excludeId) continue
if (count === visualIdx) { realIdx = i; break }
if (count === visualIdx) {
realIdx = i
break
}
count++
realIdx = i + 1
}
@@ -219,7 +242,9 @@ const dropIndicatorStyle = computed(() => {
// Sürüklenen elemanı çıkar
const dragId = dragElementId.value
const flowChildren = container.children.filter(c => c.type !== 'page_break' && c.position.type !== 'absolute' && c.id !== dragId)
const flowChildren = container.children.filter(
(c) => c.type !== 'page_break' && c.position.type !== 'absolute' && c.id !== dragId,
)
const cl = props.layoutMap[container.id]
if (!cl) return { display: 'none' }
@@ -380,13 +405,18 @@ function onDragEnd() {
window.removeEventListener('pointermove', onDragMove)
window.removeEventListener('pointerup', onDragEnd)
if (isDragging.value && dragElementId.value && dropTargetContainerId.value !== null && dropLogicalIndex.value !== null) {
if (
isDragging.value &&
dragElementId.value &&
dropTargetContainerId.value !== null &&
dropLogicalIndex.value !== null
) {
const currentParent = templateStore.getParent(dragElementId.value)
const targetContainerId = dropTargetContainerId.value
if (currentParent && currentParent.id === targetContainerId) {
// Aynı container içinde reorder
const currentIdx = currentParent.children.findIndex(c => c.id === dragElementId.value)
const currentIdx = currentParent.children.findIndex((c) => c.id === dragElementId.value)
if (currentIdx !== -1 && currentIdx !== dropLogicalIndex.value) {
templateStore.reorderChild(currentParent.id, currentIdx, dropLogicalIndex.value)
}
@@ -400,7 +430,9 @@ function onDragEnd() {
dragElementId.value = null
editorStore.setDragging(false)
clearDropTarget()
setTimeout(() => { didDrag.value = false }, 50)
setTimeout(() => {
didDrag.value = false
}, 50)
}
// --- Absolute eleman drag ---
@@ -420,7 +452,12 @@ function onAbsoluteDragStart(e: PointerEvent, el: TemplateElement) {
elY: el.position.y,
}
collectEdges(props.layoutMap, el.id, templateStore.template.page.width, templateStore.template.page.height)
collectEdges(
props.layoutMap,
el.id,
templateStore.template.page.width,
templateStore.template.page.height,
)
window.addEventListener('pointermove', onAbsoluteDragMove)
window.addEventListener('pointerup', onAbsoluteDragEnd)
@@ -466,7 +503,9 @@ function onAbsoluteDragEnd() {
absoluteDragId.value = null
editorStore.setDragging(false)
clearGuides()
setTimeout(() => { didDrag.value = false }, 50)
setTimeout(() => {
didDrag.value = false
}, 50)
}
// --- Resize ---
@@ -493,18 +532,34 @@ function onResizeStart(e: PointerEvent, elId: string, handle: string) {
const s = props.scale
// Barkod ve görsel elemanları için aspect ratio'yu kaydet
const el = flatElements.value.find(e => e.id === elId)
resizeAspectRatio.value = ((el?.type === 'barcode' || el?.type === 'image') && l.height_mm > 0) ? l.width_mm / l.height_mm : 0
const el = flatElements.value.find((e) => e.id === elId)
resizeAspectRatio.value =
(el?.type === 'barcode' || el?.type === 'image') && l.height_mm > 0
? l.width_mm / l.height_mm
: 0
resizeStart.value = {
mouseX: e.clientX, mouseY: e.clientY,
x: l.x_mm * s, y: l.y_mm * s,
width: l.width_mm * s, height: l.height_mm * s,
mouseX: e.clientX,
mouseY: e.clientY,
x: l.x_mm * s,
y: l.y_mm * s,
width: l.width_mm * s,
height: l.height_mm * s,
}
resizeGhost.value = {
x: l.x_mm * s,
y: l.y_mm * s,
width: l.width_mm * s,
height: l.height_mm * s,
}
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)
collectEdges(
props.layoutMap,
elId,
templateStore.template.page.width,
templateStore.template.page.height,
)
window.addEventListener('pointermove', onResizeMove)
window.addEventListener('pointerup', onResizeEnd)
@@ -519,13 +574,21 @@ function onResizeMove(e: PointerEvent) {
const pxToMm = 1 / props.scale
const ar = resizeAspectRatio.value
let gx = resizeStart.value.x, gy = resizeStart.value.y
let gw = resizeStart.value.width, gh = resizeStart.value.height
let gx = resizeStart.value.x,
gy = resizeStart.value.y
let gw = resizeStart.value.width,
gh = resizeStart.value.height
if (handle.includes('e')) gw = Math.max(20, resizeStart.value.width + dx)
if (handle.includes('w')) { gw = Math.max(20, resizeStart.value.width - dx); gx = resizeStart.value.x + dx }
if (handle.includes('w')) {
gw = Math.max(20, resizeStart.value.width - dx)
gx = resizeStart.value.x + dx
}
if (handle.includes('s')) gh = Math.max(10, resizeStart.value.height + dy)
if (handle.includes('n')) { gh = Math.max(10, resizeStart.value.height - dy); gy = resizeStart.value.y + dy }
if (handle.includes('n')) {
gh = Math.max(10, resizeStart.value.height - dy)
gy = resizeStart.value.y + dy
}
// Aspect ratio koruma (barkod)
if (ar > 0) {
@@ -538,7 +601,8 @@ function onResizeMove(e: PointerEvent) {
const startHMm = resizeStart.value.height * pxToMm
const startXMm = resizeStart.value.x * pxToMm
const startYMm = resizeStart.value.y * pxToMm
let wMm = startWMm, hMm = startHMm
let wMm = startWMm,
hMm = startHMm
if (handle.includes('e')) {
const rightEdge = calculateResizeSnap('right', startXMm + startWMm + dx * pxToMm)
wMm = Math.max(5, rightEdge - startXMm)
@@ -571,8 +635,10 @@ function onResizeEnd() {
const handle = resizeHandle.value
const ar = resizeAspectRatio.value
const sizeUpdate: { width?: SizeValue; height?: SizeValue } = {}
if (handle.includes('e') || handle.includes('w')) sizeUpdate.width = sz.fixed(resizeFinalMm.value.width)
if (handle.includes('s') || handle.includes('n')) sizeUpdate.height = sz.fixed(resizeFinalMm.value.height)
if (handle.includes('e') || handle.includes('w'))
sizeUpdate.width = sz.fixed(resizeFinalMm.value.width)
if (handle.includes('s') || handle.includes('n'))
sizeUpdate.height = sz.fixed(resizeFinalMm.value.height)
// Aspect ratio aktifken her zaman hem width hem height güncelle
if (ar > 0) {
sizeUpdate.width = sz.fixed(resizeFinalMm.value.width)
@@ -621,8 +687,8 @@ function onToolboxDrop(_e: DragEvent) {
}
// Aktif sürükleme var mı (eleman veya toolbox)
const isAnyDragActive = computed(() =>
(isDragging.value && dragElementId.value !== null) || !!editorStore.draggedNewElement
const isAnyDragActive = computed(
() => (isDragging.value && dragElementId.value !== null) || !!editorStore.draggedNewElement,
)
</script>
@@ -644,26 +710,57 @@ const isAnyDragActive = computed(() =>
'element-handle--selected': editorStore.isSelected(el.id),
'element-handle--container': isContainer(el),
'element-handle--dragging': isDragging && dragElementId === el.id,
'element-handle--drop-target': isContainer(el) && dropTargetContainerId === el.id && isAnyDragActive,
'element-handle--drop-target':
isContainer(el) && dropTargetContainerId === el.id && isAnyDragActive,
}"
:style="getElementStyle(el)"
@pointerdown="(e: PointerEvent) => { onElementClick(e, el.id); onDragStart(e, el) }"
@pointerdown="
(e: PointerEvent) => {
onElementClick(e, el.id)
onDragStart(e, el)
}
"
>
<!-- Selection border -->
<div v-if="editorStore.isSelected(el.id)" class="selection-border" />
<!-- Resize handles (sadece tek seçimde) -->
<template v-if="editorStore.isSelected(el.id) && editorStore.selectedElementIds.size === 1 && !isResizing && el.type !== 'page_break'">
<template
v-if="
editorStore.isSelected(el.id) &&
editorStore.selectedElementIds.size === 1 &&
!isResizing &&
el.type !== 'page_break'
"
>
<template v-if="el.type === 'barcode' || el.type === 'image'">
<!-- Barkod/Görsel: sadece yatay resize (aspect ratio korunur) -->
<div class="resize-handle resize-handle--e" @pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'e')" />
<div class="resize-handle resize-handle--w" @pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'w')" />
<div
class="resize-handle resize-handle--e"
@pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'e')"
/>
<div
class="resize-handle resize-handle--w"
@pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'w')"
/>
</template>
<template v-else>
<div class="resize-handle resize-handle--se" @pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'se')" />
<div class="resize-handle resize-handle--sw" @pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'sw')" />
<div class="resize-handle resize-handle--ne" @pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'ne')" />
<div class="resize-handle resize-handle--nw" @pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'nw')" />
<div
class="resize-handle resize-handle--se"
@pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'se')"
/>
<div
class="resize-handle resize-handle--sw"
@pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'sw')"
/>
<div
class="resize-handle resize-handle--ne"
@pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'ne')"
/>
<div
class="resize-handle resize-handle--nw"
@pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'nw')"
/>
</template>
</template>
</div>
@@ -777,12 +874,36 @@ const isAnyDragActive = computed(() =>
z-index: 10;
}
.resize-handle--se { right: -3px; bottom: -3px; cursor: se-resize; }
.resize-handle--sw { left: -3px; bottom: -3px; cursor: sw-resize; }
.resize-handle--ne { right: -3px; top: -3px; cursor: ne-resize; }
.resize-handle--nw { left: -3px; top: -3px; cursor: nw-resize; }
.resize-handle--e { right: -3px; top: calc(50% - 3px); cursor: e-resize; }
.resize-handle--w { left: -3px; top: calc(50% - 3px); cursor: w-resize; }
.resize-handle--se {
right: -3px;
bottom: -3px;
cursor: se-resize;
}
.resize-handle--sw {
left: -3px;
bottom: -3px;
cursor: sw-resize;
}
.resize-handle--ne {
right: -3px;
top: -3px;
cursor: ne-resize;
}
.resize-handle--nw {
left: -3px;
top: -3px;
cursor: nw-resize;
}
.resize-handle--e {
right: -3px;
top: calc(50% - 3px);
cursor: e-resize;
}
.resize-handle--w {
left: -3px;
top: calc(50% - 3px);
cursor: w-resize;
}
/* Drag ghost */
.drag-ghost {

View File

@@ -8,7 +8,16 @@ const props = defineProps<{
}>()
// WASM barcode üretme fonksiyonu (EditorCanvas'tan provide edilir)
const generateBarcode = inject<(format: string, value: string, width: number, height: number, includeText: boolean) => 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')
function pageContainerStyle(page: PageLayout): Record<string, string> {
const s = props.scale
@@ -92,7 +101,12 @@ function lineStyle(el: ElementLayout): Record<string, string> {
// --- Barcode rendering (WASM ile) ---
async function renderBarcodeToCanvas(canvas: HTMLCanvasElement, format: string, value: string, includeText: boolean = false) {
async function renderBarcodeToCanvas(
canvas: HTMLCanvasElement,
format: string,
value: string,
includeText: boolean = false,
) {
if (!value || !generateBarcode) return
try {
@@ -106,7 +120,13 @@ async function renderBarcodeToCanvas(canvas: HTMLCanvasElement, format: string,
const hPt = elHmm * MM_TO_PT
const size = Math.max(1, Math.round(wPt * 4))
const barcodeHeight = Math.max(1, Math.round(hPt * 4))
const result = await generateBarcode(format, value, size, barcodeHeight, isQr ? false : includeText)
const result = await generateBarcode(
format,
value,
size,
barcodeHeight,
isQr ? false : includeText,
)
if (!result) return
// Canvas boyutlarını WASM çıktısına ayarla (crisp rendering)
@@ -116,11 +136,7 @@ async function renderBarcodeToCanvas(canvas: HTMLCanvasElement, format: string,
const ctx = canvas.getContext('2d')
if (!ctx) return
const imageData = new ImageData(
new Uint8ClampedArray(result.rgba),
result.width,
result.height,
)
const imageData = new ImageData(new Uint8ClampedArray(result.rgba), result.width, result.height)
ctx.putImageData(imageData, 0, 0)
} catch (e) {
console.warn(`[dreport] WASM barcode render hatası (${format}):`, e)
@@ -159,7 +175,7 @@ watch(
await nextTick()
await nextTick()
const canvases = document.querySelectorAll<HTMLCanvasElement>('canvas[data-barcode]')
canvases.forEach(canvas => {
canvases.forEach((canvas) => {
const format = canvas.dataset.format
const value = canvas.dataset.value
const includeText = canvas.dataset.includeText === 'true'
@@ -168,7 +184,7 @@ watch(
}
})
},
{ deep: true }
{ deep: true },
)
</script>
@@ -187,7 +203,7 @@ watch(
class="layout-el layout-el--page-break"
:style="elStyle(el)"
>
<div style="border-top: 1px dashed #9ca3af; width: 100%; height: 0;" />
<div style="border-top: 1px dashed #9ca3af; width: 100%; height: 0" />
</div>
<!-- Container -->
@@ -200,13 +216,27 @@ watch(
}"
:style="{ ...elStyle(el), ...containerStyle(el) }"
>
<span v-if="el.id === 'header' || el.id.startsWith('header_p')" class="layout-el__section-label">Üst Bilgi</span>
<span v-else-if="el.id === 'footer' || el.id.startsWith('footer_p')" class="layout-el__section-label">Alt Bilgi</span>
<span
v-if="el.id === 'header' || el.id.startsWith('header_p')"
class="layout-el__section-label"
>Üst Bilgi</span
>
<span
v-else-if="el.id === 'footer' || el.id.startsWith('footer_p')"
class="layout-el__section-label"
>Alt Bilgi</span
>
</div>
<!-- Static text / Text / Page number -->
<div
v-else-if="el.element_type === 'static_text' || el.element_type === 'text' || el.element_type === 'page_number' || el.element_type === 'current_date' || el.element_type === 'calculated_text'"
v-else-if="
el.element_type === 'static_text' ||
el.element_type === 'text' ||
el.element_type === 'page_number' ||
el.element_type === 'current_date' ||
el.element_type === 'calculated_text'
"
class="layout-el layout-el--text"
:style="{ ...elStyle(el), ...textStyle(el) }"
>
@@ -231,7 +261,11 @@ watch(
<img
v-if="el.content?.type === 'image' && el.content.src"
:src="el.content.src"
:style="{ width: '100%', height: '100%', objectFit: (el.style.objectFit || 'fill') as CSSProperties['objectFit'] }"
:style="{
width: '100%',
height: '100%',
objectFit: (el.style.objectFit || 'fill') as CSSProperties['objectFit'],
}"
/>
<div v-else class="layout-el__placeholder">Görsel</div>
</div>
@@ -248,7 +282,10 @@ watch(
data-barcode
:data-format="el.content.format"
:data-value="el.content.value"
:data-include-text="el.style.barcodeIncludeText ?? (el.content.format === 'ean13' || el.content.format === 'ean8')"
:data-include-text="
el.style.barcodeIncludeText ??
(el.content.format === 'ean13' || el.content.format === 'ean8')
"
:data-el-w="el.width_mm"
:data-el-h="el.height_mm"
:style="{ width: '100%', height: '100%', display: 'block' }"
@@ -264,16 +301,24 @@ watch(
:style="elStyle(el)"
>
<svg viewBox="0 0 20 20" :style="{ width: '100%', height: '100%' }">
<rect x="1" y="1" width="18" height="18" fill="none"
<rect
x="1"
y="1"
width="18"
height="18"
fill="none"
:stroke="el.style.borderColor ?? '#333'"
:stroke-width="el.style.borderWidth ? el.style.borderWidth * 3 : 1.5" />
<path v-if="el.content?.type === 'checkbox' && el.content.checked"
:stroke-width="el.style.borderWidth ? el.style.borderWidth * 3 : 1.5"
/>
<path
v-if="el.content?.type === 'checkbox' && el.content.checked"
d="M4 10 L8 15 L16 5"
fill="none"
:stroke="el.style.color ?? '#000'"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round" />
stroke-linejoin="round"
/>
</svg>
</div>
@@ -293,7 +338,8 @@ watch(
fontFamily: span.fontFamily || undefined,
color: span.color || undefined,
}"
>{{ span.text }}</span>
>{{ span.text }}</span
>
</template>
</div>
@@ -313,13 +359,24 @@ watch(
<div
v-if="el.content?.type === 'chart' && el.content.svg"
v-html="el.content.svg"
style="width: 100%; height: 100%;"
style="width: 100%; height: 100%"
/>
<div v-else class="layout-el__placeholder" :style="{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%', color: '#94a3b8', fontSize: '12px' }">
<div
v-else
class="layout-el__placeholder"
:style="{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
color: '#94a3b8',
fontSize: '12px',
}"
>
Grafik
</div>
</div>
</template>
</div>
</div>

View File

@@ -21,10 +21,7 @@ const RULER_SIZE = computed(() => props.rulerSize ?? 20)
const hCanvas = ref<HTMLCanvasElement | null>(null)
const vCanvas = ref<HTMLCanvasElement | null>(null)
function drawRuler(
canvas: HTMLCanvasElement | null,
direction: 'horizontal' | 'vertical',
) {
function drawRuler(canvas: HTMLCanvasElement | null, direction: 'horizontal' | 'vertical') {
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
@@ -79,9 +76,10 @@ function drawTicks(
// EditorCanvas sayfayı ortalar, ruler da buna uymalı
// Yatay: canvas ortası - sayfa genişliği/2
// Sayfanın canvas üzerindeki orijin px'i
const canvasCenter = direction === 'horizontal'
? (length / 2) // flex centering approximation
: 40 // EditorCanvas padding-top: 40px
const canvasCenter =
direction === 'horizontal'
? length / 2 // flex centering approximation
: 40 // EditorCanvas padding-top: 40px
const pageStartPx = canvasCenter - (pageMm * s) / 2 + pan
@@ -188,16 +186,8 @@ onBeforeUnmount(() => {
<template>
<div class="ruler-corner" :style="{ width: `${RULER_SIZE}px`, height: `${RULER_SIZE}px` }" />
<canvas
ref="hCanvas"
class="ruler-h"
:style="{ height: `${RULER_SIZE}px` }"
/>
<canvas
ref="vCanvas"
class="ruler-v"
:style="{ width: `${RULER_SIZE}px` }"
/>
<canvas ref="hCanvas" class="ruler-h" :style="{ height: `${RULER_SIZE}px` }" />
<canvas ref="vCanvas" class="ruler-v" :style="{ width: `${RULER_SIZE}px` }" />
</template>
<style scoped>

View File

@@ -55,21 +55,36 @@ const elementTypeLabel = computed(() => {
if (el.id === 'header') return 'Üst Bilgi'
if (el.id === 'footer') return 'Alt Bilgi'
return 'Container'
case 'static_text': return 'Metin'
case 'text': return 'Metin'
case 'line': return 'Cizgi'
case 'repeating_table': return 'Tablo'
case 'image': return 'Gorsel'
case 'page_number': return 'Sayfa No'
case 'barcode': return 'Barkod'
case 'checkbox': return 'Onay Kutusu'
case 'shape': return 'Sekil'
case 'current_date': return 'Tarih'
case 'calculated_text': return 'Hesaplanan Metin'
case 'rich_text': return 'Zengin Metin'
case 'page_break': return 'Sayfa Sonu'
case 'chart': return 'Grafik'
default: return 'Eleman'
case 'static_text':
return 'Metin'
case 'text':
return 'Metin'
case 'line':
return 'Cizgi'
case 'repeating_table':
return 'Tablo'
case 'image':
return 'Gorsel'
case 'page_number':
return 'Sayfa No'
case 'barcode':
return 'Barkod'
case 'checkbox':
return 'Onay Kutusu'
case 'shape':
return 'Sekil'
case 'current_date':
return 'Tarih'
case 'calculated_text':
return 'Hesaplanan Metin'
case 'rich_text':
return 'Zengin Metin'
case 'page_break':
return 'Sayfa Sonu'
case 'chart':
return 'Grafik'
default:
return 'Eleman'
}
})
@@ -105,12 +120,12 @@ function deleteSelected() {
<div class="properties-panel">
<div v-if="multipleSelected" class="properties-panel__empty">
{{ editorStore.selectedElementIds.size }} eleman secili
<button class="prop-delete-btn" style="margin-top: 12px" @click="deleteSelected">Secilenleri Sil</button>
<button class="prop-delete-btn" style="margin-top: 12px" @click="deleteSelected">
Secilenleri Sil
</button>
</div>
<div v-else-if="!selectedElement" class="properties-panel__empty">
Bir eleman secin
</div>
<div v-else-if="!selectedElement" class="properties-panel__empty">Bir eleman secin</div>
<template v-else>
<!-- Header -->
@@ -134,68 +149,87 @@ function deleteSelected() {
<TextProperties
v-if="selectedElement.type === 'static_text' || selectedElement.type === 'text'"
:element="selectedElement" />
:element="selectedElement"
/>
<LineProperties
v-if="selectedElement.type === 'line'"
:element="(selectedElement as LineElement)" />
:element="selectedElement as LineElement"
/>
<ImageProperties
v-if="selectedElement.type === 'image'"
:element="(selectedElement as ImageElement)" />
:element="selectedElement as ImageElement"
/>
<PageNumberProperties
v-if="selectedElement.type === 'page_number'"
:element="(selectedElement as PageNumberElement)" />
:element="selectedElement as PageNumberElement"
/>
<BarcodeProperties
v-if="selectedElement.type === 'barcode'"
:element="(selectedElement as BarcodeElement)" />
:element="selectedElement as BarcodeElement"
/>
<CurrentDateProperties
v-if="selectedElement.type === 'current_date'"
:element="(selectedElement as CurrentDateElement)" />
:element="selectedElement as CurrentDateElement"
/>
<CheckboxProperties
v-if="selectedElement.type === 'checkbox'"
:element="(selectedElement as CheckboxElement)" />
:element="selectedElement as CheckboxElement"
/>
<CalculatedTextProperties
v-if="selectedElement.type === 'calculated_text'"
:element="(selectedElement as CalculatedTextElement)" />
:element="selectedElement as CalculatedTextElement"
/>
<RichTextProperties
v-if="selectedElement.type === 'rich_text'"
:element="(selectedElement as RichTextElement)" />
:element="selectedElement as RichTextElement"
/>
<ShapeProperties
v-if="selectedElement.type === 'shape'"
:element="(selectedElement as ShapeElement)" />
:element="selectedElement as ShapeElement"
/>
<ContainerProperties
v-if="isContainer(selectedElement)"
:element="(selectedElement as ContainerElement)" />
:element="selectedElement as ContainerElement"
/>
<RepeatingTableProperties
v-if="selectedElement.type === 'repeating_table'"
:element="(selectedElement as RepeatingTableElement)" />
:element="selectedElement as RepeatingTableElement"
/>
<ChartProperties
v-if="selectedElement.type === 'chart'"
:element="(selectedElement as ChartElement)" />
:element="selectedElement as ChartElement"
/>
<!-- Header/Footer toggles for root element -->
<div v-if="selectedElement.id === 'root'" class="prop-section">
<div class="prop-section__title">Sayfa Ust/Alt Bilgi</div>
<div class="prop-row">
<label class="prop-label">Ust Bilgi (Header)</label>
<input type="checkbox" :checked="!!templateStore.template.header"
@change="toggleHeader" />
<input
type="checkbox"
:checked="!!templateStore.template.header"
@change="toggleHeader"
/>
</div>
<div class="prop-row">
<label class="prop-label">Alt Bilgi (Footer)</label>
<input type="checkbox" :checked="!!templateStore.template.footer"
@change="toggleFooter" />
<input
type="checkbox"
:checked="!!templateStore.template.footer"
@change="toggleFooter"
/>
</div>
</div>

View File

@@ -7,12 +7,15 @@ 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 props = withDefaults(
defineProps<{
node: SchemaNode
depth?: number
}>(),
{
depth: 0,
},
)
const editorStore = useEditorStore()
const schemaStore = useSchemaStore()
@@ -68,7 +71,7 @@ function createBoundTextElement(node: SchemaNode): TemplateElement {
function createBoundTableElement(node: SchemaNode): RepeatingTableElement {
const itemFields = schemaStore.getArrayItemFields(node.path)
const columns: TableColumn[] = itemFields.map(field => ({
const columns: TableColumn[] = itemFields.map((field) => ({
id: `col_${(++colIdCounter).toString(36)}`,
field: field.key,
title: field.title,
@@ -108,9 +111,7 @@ function onDragEnd() {
editorStore.endDragNewElement()
}
const displayChildren = isArray
? (props.node.itemProperties ?? [])
: props.node.children
const displayChildren = isArray ? (props.node.itemProperties ?? []) : props.node.children
</script>
<template>
@@ -131,7 +132,11 @@ const displayChildren = isArray
@dragstart="onDragStart"
@dragend="onDragEnd"
>
<span v-if="hasChildren" class="schema-node__arrow" :class="{ 'schema-node__arrow--expanded': expanded }">
<span
v-if="hasChildren"
class="schema-node__arrow"
:class="{ 'schema-node__arrow--expanded': expanded }"
>
&#9654;
</span>
<span v-else class="schema-node__arrow-placeholder" />

View File

@@ -1,7 +1,21 @@
<script setup lang="ts">
import { useEditorStore } from '../../stores/editor'
import { useSchemaStore } from '../../stores/schema'
import type { TemplateElement, RepeatingTableElement, TableColumn, ImageElement, PageNumberElement, BarcodeElement, PageBreakElement, CurrentDateElement, ShapeElement, CheckboxElement, CalculatedTextElement, RichTextElement, ChartElement } from '../../core/types'
import type {
TemplateElement,
RepeatingTableElement,
TableColumn,
ImageElement,
PageNumberElement,
BarcodeElement,
PageBreakElement,
CurrentDateElement,
ShapeElement,
CheckboxElement,
CalculatedTextElement,
RichTextElement,
ChartElement,
} from '../../core/types'
import { sz } from '../../core/types'
import { schemaFormatToFormatType, defaultAlignForSchema } from '../../core/schema-parser'
@@ -88,7 +102,7 @@ const tools: ToolItem[] = [
if (firstArray) {
dataPath = firstArray.path
const itemFields = schemaStore.getArrayItemFields(firstArray.path)
columns = itemFields.map(field => ({
columns = itemFields.map((field) => ({
id: nextId('col'),
field: field.key,
title: field.title,
@@ -212,8 +226,8 @@ const tools: ToolItem[] = [
if (firstArray) {
dataPath = firstArray.path
const itemFields = schemaStore.getArrayItemFields(firstArray.path)
const stringField = itemFields.find(f => f.type === 'string')
const numberField = itemFields.find(f => f.type === 'number' || f.type === 'integer')
const stringField = itemFields.find((f) => f.type === 'string')
const numberField = itemFields.find((f) => f.type === 'number' || f.type === 'integer')
categoryField = stringField?.key ?? itemFields[0]?.key ?? ''
valueField = numberField?.key ?? itemFields[1]?.key ?? ''
}

View File

@@ -50,7 +50,7 @@ function validateBarcode(format: BarcodeFormat, value: string): boolean {
case 'code39':
return /^[A-Z0-9\-. $/+%]+$/i.test(value)
case 'code128':
return value.length > 0 && [...value].every(c => c.charCodeAt(0) < 128)
return value.length > 0 && [...value].every((c) => c.charCodeAt(0) < 128)
case 'qr':
return value.length > 0
default:
@@ -61,10 +61,14 @@ function validateBarcode(format: BarcodeFormat, value: string): boolean {
const barcodeInputValue = ref('')
const barcodeInputInvalid = ref(false)
watch(() => props.element.value ?? '', (val) => {
barcodeInputValue.value = val
barcodeInputInvalid.value = false
}, { immediate: true })
watch(
() => props.element.value ?? '',
(val) => {
barcodeInputValue.value = val
barcodeInputInvalid.value = false
},
{ immediate: true },
)
function onBarcodeValueInput(e: Event) {
const val = (e.target as HTMLInputElement).value
@@ -96,9 +100,13 @@ function onBarcodeFormatChange(newFormat: BarcodeFormat) {
<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"
<select
class="prop-input prop-select"
:value="element.format"
@change="(e) => onBarcodeFormatChange((e.target as HTMLSelectElement).value as BarcodeFormat)">
@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>
@@ -108,44 +116,70 @@ function onBarcodeFormatChange(newFormat: BarcodeFormat) {
</div>
<div class="prop-row" data-tip="Barkod icerigi formata uygun olmali">
<label class="prop-label">Deger</label>
<input class="prop-input" type="text"
<input
class="prop-input"
type="text"
:class="{ 'prop-input--invalid': barcodeInputInvalid }"
:value="barcodeInputValue"
@input="onBarcodeValueInput" />
@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"
<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>
@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" data-tip="Barkod altinda degeri metin olarak goster">
<div
v-if="element.format !== 'qr'"
class="prop-row"
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)" />
<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" data-tip="Schema'dan dinamik veri baglama">
<div
v-if="schemaStore.scalarFields.length > 0"
class="prop-row"
data-tip="Schema'dan dinamik veri baglama"
>
<label class="prop-label">Veri Baglama</label>
<select class="prop-input prop-select"
<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)
@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>
<option v-for="field in schemaStore.scalarFields" :key="field.path" :value="field.path">
{{ field.title }} ({{ field.path }})
</option>
</select>
</div>
</div>

View File

@@ -27,18 +27,26 @@ function onExpressionChange(value: string) {
<template>
<div class="prop-section">
<div class="prop-section__title">Hesaplanan Metin</div>
<div class="prop-row-stack" data-tip="Hesaplama ifadesi (orn: toplamlar.kdv + toplamlar.araToplam)">
<div
class="prop-row-stack"
data-tip="Hesaplama ifadesi (orn: toplamlar.kdv + toplamlar.araToplam)"
>
<label class="prop-label">Ifade</label>
<DexprEditor
:model-value="element.expression"
@update:model-value="onExpressionChange"
placeholder="toplamlar.kdv + toplamlar.araToplam" />
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"
<select
class="prop-input prop-select"
:value="element.format ?? ''"
@change="(e) => update({ format: (e.target as HTMLSelectElement).value || undefined } as any)">
@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>
@@ -47,30 +55,44 @@ function onExpressionChange(value: string) {
</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"
<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)" />
@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"
<input
class="prop-input prop-color"
type="color"
:value="(element.style as TextStyle).color ?? '#000000'"
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
@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"
<select
class="prop-input prop-select"
:value="(element.style as TextStyle).fontWeight ?? 'normal'"
@change="(e) => updateStyle('fontWeight', (e.target as HTMLSelectElement).value)">
@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"
<select
class="prop-input prop-select"
:value="(element.style as TextStyle).align ?? 'left'"
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)">
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)"
>
<option value="left">Sol</option>
<option value="center">Orta</option>
<option value="right">Sag</option>

View File

@@ -33,13 +33,15 @@ const itemFields = computed(() => {
return schemaStore.getArrayItemFields(path)
})
const stringFields = computed(() => itemFields.value.filter(f => f.type === 'string'))
const numberFields = computed(() => itemFields.value.filter(f => f.type === 'number' || f.type === 'integer'))
const stringFields = computed(() => itemFields.value.filter((f) => f.type === 'string'))
const numberFields = computed(() =>
itemFields.value.filter((f) => f.type === 'number' || f.type === 'integer'),
)
function updateDataSource(path: string) {
const fields = schemaStore.getArrayItemFields(path)
const strField = fields.find(f => f.type === 'string')
const numField = fields.find(f => f.type === 'number' || f.type === 'integer')
const strField = fields.find((f) => f.type === 'string')
const numField = fields.find((f) => f.type === 'number' || f.type === 'integer')
update({
dataSource: { type: 'array', path },
categoryField: strField?.key ?? fields[0]?.key ?? '',
@@ -73,7 +75,9 @@ 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']
return (
props.element.style.colors ?? ['#4F46E5', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899']
)
})
function updateColor(index: number, value: string) {
@@ -99,7 +103,11 @@ function removeColor(index: number) {
<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 })">
<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>
@@ -112,33 +120,61 @@ function removeColor(index: number) {
<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)">
<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>
<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
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
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 })">
<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>
<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 })">
<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>
@@ -150,19 +186,40 @@ function removeColor(index: number) {
<div class="prop-section__title">Baslik</div>
<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)" placeholder="Grafik basligi">
<input
class="prop-input"
type="text"
:value="element.title?.text ?? ''"
@change="updateTitle('text', ($event.target as HTMLInputElement).value)"
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))">
<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))"
/>
</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)">
<input
class="prop-color"
type="color"
:value="element.title?.color ?? '#333333'"
@input="updateTitle('color', ($event.target as HTMLInputElement).value)"
/>
</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)">
<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>
@@ -175,12 +232,20 @@ function removeColor(index: number) {
<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)">
<input
type="checkbox"
:checked="element.legend?.show ?? false"
@change="updateLegend('show', ($event.target as HTMLInputElement).checked)"
/>
</div>
<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)">
<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>
@@ -188,7 +253,15 @@ function removeColor(index: number) {
</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))">
<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>
</template>
</div>
@@ -198,16 +271,33 @@ function removeColor(index: number) {
<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)">
<input
type="checkbox"
:checked="element.labels?.show ?? false"
@change="updateLabels('show', ($event.target as HTMLInputElement).checked)"
/>
</div>
<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))">
<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)">
<input
class="prop-color"
type="color"
:value="element.labels?.color ?? '#333333'"
@input="updateLabels('color', ($event.target as HTMLInputElement).value)"
/>
</div>
</template>
</div>
@@ -217,19 +307,40 @@ function removeColor(index: number) {
<div class="prop-section__title">Eksenler</div>
<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)" placeholder="X ekseni">
<input
class="prop-input"
type="text"
:value="element.axis?.xLabel ?? ''"
@change="updateAxis('xLabel', ($event.target as HTMLInputElement).value || undefined)"
placeholder="X ekseni"
/>
</div>
<div class="prop-row">
<label class="prop-label">Y Etiketi</label>
<input class="prop-input" type="text" :value="element.axis?.yLabel ?? ''" @change="updateAxis('yLabel', ($event.target as HTMLInputElement).value || undefined)" placeholder="Y ekseni">
<input
class="prop-input"
type="text"
:value="element.axis?.yLabel ?? ''"
@change="updateAxis('yLabel', ($event.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)">
<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)">
<input
class="prop-color"
type="color"
:value="element.axis?.gridColor ?? '#E5E7EB'"
@input="updateAxis('gridColor', ($event.target as HTMLInputElement).value)"
/>
</div>
</div>
@@ -238,14 +349,26 @@ function removeColor(index: number) {
<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)">
<input
class="prop-color"
type="color"
:value="element.style.backgroundColor ?? '#FFFFFF'"
@input="updateStyle('backgroundColor', ($event.target as HTMLInputElement).value)"
/>
</div>
<!-- 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)">
<button class="prop-btn-sm prop-btn-sm--danger" @click="removeColor(i)" title="Kaldir">×</button>
<input
class="prop-color"
type="color"
:value="color"
@input="updateColor(i, ($event.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>
@@ -255,7 +378,15 @@ function removeColor(index: number) {
<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))">
<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>
@@ -263,11 +394,22 @@ function removeColor(index: number) {
<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))">
<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)">
<input
type="checkbox"
:checked="element.style.showPoints ?? true"
@change="updateStyle('showPoints', ($event.target as HTMLInputElement).checked)"
/>
</div>
</div>
@@ -275,11 +417,19 @@ function removeColor(index: number) {
<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>
<div class="prop-row" style="font-size: 11px; color: #94a3b8;">
0 = Pie, &gt;0 = Donut
<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>
<div class="prop-row" style="font-size: 11px; color: #94a3b8">0 = Pie, &gt;0 = Donut</div>
</div>
</div>
</template>

View File

@@ -24,27 +24,40 @@ function updateStyle(key: string, value: unknown) {
<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"
<input
type="checkbox"
:checked="element.checked ?? false"
@change="(e) => update({ checked: (e.target as HTMLInputElement).checked } as any)" />
@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"
<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)" />
@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"
<input
class="prop-input prop-color"
type="color"
:value="element.style.checkColor ?? '#000000'"
@input="(e) => updateStyle('checkColor', (e.target as HTMLInputElement).value)" />
@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"
<input
class="prop-input prop-color"
type="color"
:value="element.style.borderColor ?? '#333333'"
@input="(e) => updateStyle('borderColor', (e.target as HTMLInputElement).value)" />
@input="(e) => updateStyle('borderColor', (e.target as HTMLInputElement).value)"
/>
</div>
</div>
</template>

View File

@@ -25,24 +25,37 @@ function updateStyle(key: string, value: unknown) {
<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"
<select
class="prop-input prop-select"
:value="element.direction"
@change="(e) => update({ direction: (e.target as HTMLSelectElement).value } as any)">
@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"
<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)" />
@input="
(e) => update({ gap: parseFloat((e.target as HTMLInputElement).value) || 0 } as any)
"
/>
</div>
<div class="prop-row" data-tip="Cocuklarin cross-axis hizalamasi">
<label class="prop-label">{{ element.direction === 'column' ? 'Yatay Hizalama' : 'Dikey Hizalama' }}</label>
<select class="prop-input prop-select"
<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)">
@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>
@@ -50,10 +63,14 @@ function updateStyle(key: string, value: unknown) {
</select>
</div>
<div class="prop-row" data-tip="Cocuklarin main-axis dagilimi">
<label class="prop-label">{{ element.direction === 'column' ? 'Dikey Dagilim' : 'Yatay Dagilim' }}</label>
<select class="prop-input prop-select"
<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)">
@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>
@@ -72,9 +89,11 @@ function updateStyle(key: string, value: unknown) {
<div class="prop-row" data-tip="Sayfa sonunda bolunmeyi kontrol eder">
<label class="prop-label">Sayfa Bolme</label>
<select class="prop-input prop-select"
<select
class="prop-input prop-select"
:value="element.breakInside ?? 'auto'"
@change="(e) => update({ breakInside: (e.target as HTMLSelectElement).value } as any)">
@change="(e) => update({ breakInside: (e.target as HTMLSelectElement).value } as any)"
>
<option value="auto">Izin Ver</option>
<option value="avoid">Bolme</option>
</select>
@@ -84,32 +103,59 @@ function updateStyle(key: string, value: unknown) {
<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"
<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>
@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"
<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)" />
@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"
<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>
@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"
<select
class="prop-input prop-select"
:value="element.style.borderStyle ?? 'solid'"
@change="(e) => updateStyle('borderStyle', (e.target as HTMLSelectElement).value)">
@change="(e) => updateStyle('borderStyle', (e.target as HTMLSelectElement).value)"
>
<option value="solid">Duz</option>
<option value="dashed">Kesikli</option>
<option value="dotted">Noktali</option>
@@ -117,9 +163,16 @@ function updateStyle(key: string, value: unknown) {
</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"
<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)" />
@input="
(e) => updateStyle('borderRadius', parseFloat((e.target as HTMLInputElement).value) || 0)
"
/>
</div>
</div>
</template>

View File

@@ -24,9 +24,11 @@ function updateStyle(key: string, value: unknown) {
<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"
<select
class="prop-input prop-select"
:value="element.format ?? 'DD.MM.YYYY'"
@change="(e) => update({ format: (e.target as HTMLSelectElement).value } as any)">
@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>
@@ -35,21 +37,33 @@ function updateStyle(key: string, value: unknown) {
</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"
<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)" />
@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"
<input
class="prop-input prop-color"
type="color"
:value="(element.style as TextStyle).color ?? '#666666'"
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
@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"
<select
class="prop-input prop-select"
:value="(element.style as TextStyle).align ?? 'left'"
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)">
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)"
>
<option value="left">Sol</option>
<option value="center">Orta</option>
<option value="right">Sag</option>

View File

@@ -40,7 +40,9 @@ function setMode(mode: 'static' | 'dynamic') {
update({ binding: undefined } as Partial<TemplateElement>)
} 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 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>)
}
@@ -52,7 +54,7 @@ function setBindingPath(path: string) {
/** Schema'dan görsel olabilecek alanlar (format: image veya string) */
const imageScalarFields = computed(() => {
return schemaStore.scalarFields.filter(f => f.format === 'image' || f.type === 'string')
return schemaStore.scalarFields.filter((f) => f.format === 'image' || f.type === 'string')
})
</script>
@@ -64,8 +66,20 @@ const imageScalarFields = computed(() => {
<div class="prop-row" data-tip="Gorsel kaynagi: dosya veya veri alanından">
<label class="prop-label">Mod</label>
<div class="prop-toggle-group">
<button class="prop-toggle-btn" :class="{ 'prop-toggle-btn--active': !isDynamic }" @click="setMode('static')">Statik</button>
<button class="prop-toggle-btn" :class="{ 'prop-toggle-btn--active': isDynamic }" @click="setMode('dynamic')">Dinamik</button>
<button
class="prop-toggle-btn"
:class="{ 'prop-toggle-btn--active': !isDynamic }"
@click="setMode('static')"
>
Statik
</button>
<button
class="prop-toggle-btn"
:class="{ 'prop-toggle-btn--active': isDynamic }"
@click="setMode('dynamic')"
>
Dinamik
</button>
</div>
</div>
@@ -84,7 +98,9 @@ const imageScalarFields = computed(() => {
</div>
<div v-if="element.src" class="prop-row" data-tip="Gorseli kaldirmak icin tiklayin">
<label class="prop-label"></label>
<button class="prop-clear" @click="update({ src: undefined } as any)">Gorseli kaldir</button>
<button class="prop-clear" @click="update({ src: undefined } as any)">
Gorseli kaldir
</button>
</div>
</template>
@@ -92,15 +108,15 @@ const imageScalarFields = computed(() => {
<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"
<select
class="prop-input prop-select"
:value="element.binding?.path ?? ''"
@change="(e) => setBindingPath((e.target as HTMLSelectElement).value)">
@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>
<option v-for="field in imageScalarFields" :key="field.path" :value="field.path">
{{ field.title }} ({{ field.path }})
</option>
</select>
</div>
<div v-if="element.binding?.path" class="prop-row">
@@ -112,9 +128,11 @@ const imageScalarFields = computed(() => {
<!-- 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"
<select
class="prop-input prop-select"
:value="element.style.objectFit ?? 'contain'"
@change="(e) => updateStyle('objectFit', (e.target as HTMLSelectElement).value)">
@change="(e) => updateStyle('objectFit', (e.target as HTMLSelectElement).value)"
>
<option value="contain">Sigdir</option>
<option value="cover">Kap</option>
<option value="stretch">Esnet</option>
@@ -137,7 +155,9 @@ const imageScalarFields = computed(() => {
color: #64748b;
font-size: 11px;
cursor: pointer;
transition: background 0.1s, color 0.1s;
transition:
background 0.1s,
color 0.1s;
}
.prop-toggle-btn:first-child {

View File

@@ -11,7 +11,9 @@ 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>)
templateStore.updateElement(id, {
style: { ...props.element.style, [key]: value },
} as Partial<TemplateElement>)
}
</script>
@@ -20,15 +22,25 @@ function updateStyle(key: string, value: unknown) {
<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"
<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)" />
@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"
<input
class="prop-input prop-color"
type="color"
:value="element.style.strokeColor ?? '#000000'"
@input="(e) => updateStyle('strokeColor', (e.target as HTMLInputElement).value)" />
@input="(e) => updateStyle('strokeColor', (e.target as HTMLInputElement).value)"
/>
</div>
</div>
</template>

View File

@@ -22,10 +22,42 @@ function onInput(side: 'top' | 'right' | 'bottom' | 'left', e: Event) {
<div class="pb">
<span class="pb__label">Padding</span>
<div class="pb__box">
<input class="pb__in pb__in--t" type="number" step="1" min="0" :value="props.top" @input="(e) => onInput('top', e)" data-tip="Ust bosluk (mm)" />
<input class="pb__in pb__in--r" type="number" step="1" min="0" :value="props.right" @input="(e) => onInput('right', e)" data-tip="Sag bosluk (mm)" />
<input class="pb__in pb__in--b" type="number" step="1" min="0" :value="props.bottom" @input="(e) => onInput('bottom', e)" data-tip="Alt bosluk (mm)" />
<input class="pb__in pb__in--l" type="number" step="1" min="0" :value="props.left" @input="(e) => onInput('left', e)" data-tip="Sol bosluk (mm)" />
<input
class="pb__in pb__in--t"
type="number"
step="1"
min="0"
:value="props.top"
@input="(e) => onInput('top', e)"
data-tip="Ust bosluk (mm)"
/>
<input
class="pb__in pb__in--r"
type="number"
step="1"
min="0"
:value="props.right"
@input="(e) => onInput('right', e)"
data-tip="Sag bosluk (mm)"
/>
<input
class="pb__in pb__in--b"
type="number"
step="1"
min="0"
:value="props.bottom"
@input="(e) => onInput('bottom', e)"
data-tip="Alt bosluk (mm)"
/>
<input
class="pb__in pb__in--l"
type="number"
step="1"
min="0"
:value="props.left"
@input="(e) => onInput('left', e)"
data-tip="Sol bosluk (mm)"
/>
<div class="pb__center" />
</div>
</div>
@@ -87,11 +119,32 @@ function onInput(side: 'top' | 'right' | 'bottom' | 'left', e: Event) {
margin: 0;
}
.pb__in:hover { background: #f1f5f9; }
.pb__in:focus { background: white; box-shadow: 0 0 0 1px #93c5fd; }
.pb__in:hover {
background: #f1f5f9;
}
.pb__in:focus {
background: white;
box-shadow: 0 0 0 1px #93c5fd;
}
.pb__in--t { top: 1px; left: 50%; transform: translateX(-50%); }
.pb__in--b { bottom: 1px; left: 50%; transform: translateX(-50%); }
.pb__in--l { left: 2px; top: 50%; transform: translateY(-50%); }
.pb__in--r { right: 2px; top: 50%; transform: translateY(-50%); }
.pb__in--t {
top: 1px;
left: 50%;
transform: translateX(-50%);
}
.pb__in--b {
bottom: 1px;
left: 50%;
transform: translateX(-50%);
}
.pb__in--l {
left: 2px;
top: 50%;
transform: translateY(-50%);
}
.pb__in--r {
right: 2px;
top: 50%;
transform: translateY(-50%);
}
</style>

View File

@@ -24,9 +24,11 @@ function updateStyle(key: string, value: unknown) {
<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"
<select
class="prop-input prop-select"
:value="element.format ?? '{current} / {total}'"
@change="(e) => update({ format: (e.target as HTMLSelectElement).value } as any)">
@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>
@@ -35,21 +37,33 @@ function updateStyle(key: string, value: unknown) {
</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"
<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)" />
@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"
<input
class="prop-input prop-color"
type="color"
:value="(element.style as TextStyle).color ?? '#666666'"
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
@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"
<select
class="prop-input prop-select"
:value="(element.style as TextStyle).align ?? 'center'"
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)">
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)"
>
<option value="left">Sol</option>
<option value="center">Orta</option>
<option value="right">Sag</option>

View File

@@ -20,7 +20,11 @@ function togglePositioning() {
<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">
<select
class="prop-input prop-select"
:value="element.position.type"
@change="togglePositioning"
>
<option value="flow">Flow</option>
<option value="absolute">Absolute</option>
</select>
@@ -28,15 +32,37 @@ function 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"
<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 })" />
@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"
<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 })" />
@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>

View File

@@ -5,7 +5,12 @@ 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 type {
RepeatingTableElement,
TableColumn,
FormatType,
TemplateElement,
} from '../../core/types'
import '../../styles/properties.css'
const props = defineProps<{ element: RepeatingTableElement }>()
@@ -27,7 +32,7 @@ function nextColId() {
function updateTableDataSource(path: string) {
const itemFields = schemaStore.getArrayItemFields(path)
if (itemFields.length > 0) {
const columns: TableColumn[] = itemFields.map(field => ({
const columns: TableColumn[] = itemFields.map((field) => ({
id: nextColId(),
field: field.key,
title: field.title,
@@ -51,7 +56,7 @@ function updateTableStyle(key: string, value: unknown) {
}
function updateColumn(colId: string, updates: Partial<TableColumn>) {
const columns = props.element.columns.map(c => c.id === colId ? { ...c, ...updates } : c)
const columns = props.element.columns.map((c) => (c.id === colId ? { ...c, ...updates } : c))
update({ columns } as Partial<TemplateElement>)
}
@@ -67,12 +72,14 @@ function addColumn() {
}
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 Partial<TemplateElement>)
}
function moveColumn(colId: string, direction: -1 | 1) {
const cols = [...props.element.columns]
const idx = cols.findIndex(c => c.id === colId)
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]]
@@ -90,15 +97,15 @@ const tableItemFields = computed(() => {
<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"
<select
class="prop-input prop-select"
:value="element.dataSource.path"
@change="(e) => updateTableDataSource((e.target as HTMLSelectElement).value)">
@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>
<option v-for="arr in schemaStore.arrayFields" :key="arr.path" :value="arr.path">
{{ arr.title }} ({{ arr.path }})
</option>
</select>
</div>
</div>
@@ -109,26 +116,41 @@ const tableItemFields = computed(() => {
Sutunlar
<button class="prop-add-btn" @click="addColumn">+</button>
</div>
<div
v-for="col in element.columns"
:key="col.id"
class="tbl-col"
>
<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"
<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" />
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>
<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>
<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
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>
@@ -136,37 +158,148 @@ const tableItemFields = computed(() => {
<!-- 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 })
<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"
<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" />
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
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
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
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>
@@ -174,8 +307,18 @@ const tableItemFields = computed(() => {
<!-- 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 })">
<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>
@@ -185,22 +328,45 @@ const tableItemFields = computed(() => {
</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 } })
}">
<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"
<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 })" />
@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>
@@ -216,15 +382,36 @@ const tableItemFields = computed(() => {
<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"
<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)" />
@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"
<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)" />
@input="
(e) =>
updateTableStyle(
'headerFontSize',
parseFloat((e.target as HTMLInputElement).value) || 10,
)
"
/>
</span>
</div>
@@ -232,23 +419,38 @@ const tableItemFields = computed(() => {
<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"
<input
class="ts-swatch"
type="color"
:value="element.style.headerBg ?? '#f0f0f0'"
@input="(e) => updateTableStyle('headerBg', (e.target as HTMLInputElement).value)" />
@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"
<input
class="ts-swatch"
type="color"
:value="element.style.headerColor ?? '#000000'"
@input="(e) => updateTableStyle('headerColor', (e.target as HTMLInputElement).value)" />
@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"
<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)">&times;</button>
@input="(e) => updateTableStyle('zebraOdd', (e.target as HTMLInputElement).value)"
/>
<button
v-if="element.style.zebraOdd"
class="ts-swatch-clr"
@click="updateTableStyle('zebraOdd', undefined)"
>
&times;
</button>
</div>
<span class="ts-clbl">Zebra</span>
</div>
@@ -258,15 +460,36 @@ const tableItemFields = computed(() => {
<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"
<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)">&times;</button>
@input="(e) => updateTableStyle('borderColor', (e.target as HTMLInputElement).value)"
/>
<button
v-if="element.style.borderColor"
class="ts-swatch-clr"
@click="updateTableStyle('borderColor', undefined)"
>
&times;
</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"
<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)" />
@input="
(e) =>
updateTableStyle(
'borderWidth',
parseFloat((e.target as HTMLInputElement).value) || 0,
)
"
/>
</span>
<span class="ts-unit">mm</span>
</div>
@@ -276,42 +499,96 @@ const tableItemFields = computed(() => {
<div class="ts-val ts-val--pair">
<span class="ts-pad-icon" data-tip="Yatay bosluk (mm)">&#8596;</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"
<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)" />
@input="
(e) =>
updateTableStyle(
'cellPaddingH',
parseFloat((e.target as HTMLInputElement).value) || 0,
)
"
/>
</span>
<span class="ts-pad-icon" data-tip="Dikey bosluk (mm)">&#8597;</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"
<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)" />
@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>
<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)">&#8596;</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"
<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)" />
@input="
(e) =>
updateTableStyle(
'headerPaddingH',
parseFloat((e.target as HTMLInputElement).value) || 0,
)
"
/>
</span>
<span class="ts-pad-icon" data-tip="Dikey bosluk (mm)">&#8597;</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"
<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)" />
@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>
<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"
<input
type="checkbox"
:checked="element.repeatHeader !== false"
@change="(e) => update({ repeatHeader: (e.target as HTMLInputElement).checked } as any)" />
@change="(e) => update({ repeatHeader: (e.target as HTMLInputElement).checked } as any)"
/>
<span class="ts-toggle__track"></span>
</label>
</div>
@@ -666,7 +943,7 @@ const tableItemFields = computed(() => {
background: white;
border-radius: 50%;
transition: transform 0.15s;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.ts-toggle input:checked + .ts-toggle__track {

View File

@@ -46,21 +46,33 @@ function removeSpan(index: number) {
<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"
<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)" />
@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"
<input
class="prop-input prop-color"
type="color"
:value="element.style.color ?? '#000000'"
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
@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"
<select
class="prop-input prop-select"
:value="element.style.align ?? 'left'"
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)">
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)"
>
<option value="left">Sol</option>
<option value="center">Orta</option>
<option value="right">Sag</option>
@@ -82,33 +94,49 @@ function removeSpan(index: number) {
class="prop-span-card__remove"
@click="removeSpan(idx)"
title="Sil"
>&times;</button>
>
&times;
</button>
</div>
<div class="prop-row" data-tip="Span metin icerigi">
<label class="prop-label">Metin</label>
<input class="prop-input" type="text"
<input
class="prop-input"
type="text"
:value="span.text ?? ''"
@input="(e) => updateSpan(idx, { text: (e.target as HTMLInputElement).value })" />
@input="(e) => updateSpan(idx, { text: (e.target as HTMLInputElement).value })"
/>
</div>
<div class="prop-row" data-tip="Span yazi boyutu — bos birakilirsa varsayilan kullanilir">
<label class="prop-label">Boyut</label>
<input class="prop-input" type="number" step="1" min="1"
<input
class="prop-input"
type="number"
step="1"
min="1"
:value="(span.style as TextStyle).fontSize ?? ''"
placeholder="varsayilan"
@input="(e) => {
const v = (e.target as HTMLInputElement).value
updateSpanStyle(idx, 'fontSize', v ? parseFloat(v) : undefined)
}" />
@input="
(e) => {
const v = (e.target as HTMLInputElement).value
updateSpanStyle(idx, 'fontSize', v ? parseFloat(v) : undefined)
}
"
/>
</div>
<div class="prop-row" data-tip="Span yazi kalinligi">
<label class="prop-label">Kalinlik</label>
<select class="prop-input prop-select"
<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)
}">
@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>
@@ -116,9 +144,12 @@ function removeSpan(index: number) {
</div>
<div class="prop-row" data-tip="Span metin rengi">
<label class="prop-label">Renk</label>
<input class="prop-input prop-color" type="color"
<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)" />
@input="(e) => updateSpanStyle(idx, 'color', (e.target as HTMLInputElement).value)"
/>
</div>
</div>
</div>

View File

@@ -24,9 +24,11 @@ function updateStyle(key: string, value: unknown) {
<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"
<select
class="prop-input prop-select"
:value="element.shapeType"
@change="(e) => update({ shapeType: (e.target as HTMLSelectElement).value } as any)">
@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>
@@ -34,27 +36,51 @@ function updateStyle(key: string, value: unknown) {
</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"
<input
class="prop-input prop-color"
type="color"
:value="element.style.backgroundColor ?? '#f0f0f0'"
@input="(e) => updateStyle('backgroundColor', (e.target as HTMLInputElement).value)" />
@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"
<input
class="prop-input prop-color"
type="color"
:value="element.style.borderColor ?? '#333333'"
@input="(e) => updateStyle('borderColor', (e.target as HTMLInputElement).value)" />
@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"
<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)" />
@input="
(e) => updateStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)
"
/>
</div>
<div v-if="element.shapeType === 'rounded_rectangle'" class="prop-row" data-tip="Kose yuvarlakligi (mm)">
<div
v-if="element.shapeType === 'rounded_rectangle'"
class="prop-row"
data-tip="Kose yuvarlakligi (mm)"
>
<label class="prop-label">Kose Yuvarlakligi</label>
<input class="prop-input" type="number" step="0.5" min="0"
<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)" />
@input="
(e) => updateStyle('borderRadius', parseFloat((e.target as HTMLInputElement).value) || 0)
"
/>
</div>
</div>
</template>

View File

@@ -16,52 +16,105 @@ function updateSize(axis: 'width' | 'height', sv: SizeValue) {
<div class="prop-section__title">Boyut</div>
<div class="prop-row" data-tip="Genislik boyutlandirma modu">
<label class="prop-label">Genislik</label>
<select class="prop-input prop-select"
<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) => {
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" data-tip="Sabit genislik degeri (mm)">
<div
v-if="element.size.width.type === 'fixed'"
class="prop-row"
data-tip="Sabit genislik degeri (mm)"
>
<label class="prop-label">mm</label>
<input class="prop-input" type="number" step="1" min="1"
<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 })" />
@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" data-tip="Kalan alani oransal doldurma degeri">
<div
v-if="element.size.width.type === 'fr'"
class="prop-row"
data-tip="Kalan alani oransal doldurma degeri"
>
<label class="prop-label">fr</label>
<input class="prop-input" type="number" step="1" min="1"
<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 })" />
@input="
(e) =>
updateSize('width', {
type: 'fr',
value: parseFloat((e.target as HTMLInputElement).value) || 1,
})
"
/>
</div>
<div class="prop-row" data-tip="Yukseklik boyutlandirma modu">
<label class="prop-label">Yukseklik</label>
<select class="prop-input prop-select"
<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) => {
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" data-tip="Sabit yukseklik degeri (mm)">
<div
v-if="element.size.height.type === 'fixed'"
class="prop-row"
data-tip="Sabit yukseklik degeri (mm)"
>
<label class="prop-label">mm</label>
<input class="prop-input" type="number" step="1" min="1"
<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 })" />
@input="
(e) =>
updateSize('height', {
type: 'fixed',
value: parseFloat((e.target as HTMLInputElement).value) || 10,
})
"
/>
</div>
</div>
</template>

View File

@@ -25,37 +25,54 @@ function updateStyle(key: string, value: unknown) {
<div v-if="element.type === 'static_text'" class="prop-row" data-tip="Sabit metin icerigi">
<label class="prop-label">Metin</label>
<input class="prop-input" type="text"
<input
class="prop-input"
type="text"
:value="(element as StaticTextElement).content"
@input="(e) => update({ content: (e.target as HTMLInputElement).value } as any)" />
@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"
<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)" />
@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"
<select
class="prop-input prop-select"
:value="(element.style as TextStyle).fontWeight ?? 'normal'"
@change="(e) => updateStyle('fontWeight', (e.target as HTMLSelectElement).value)">
@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"
<input
class="prop-input prop-color"
type="color"
:value="(element.style as TextStyle).color ?? '#000000'"
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
@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"
<select
class="prop-input prop-select"
:value="(element.style as TextStyle).align ?? 'left'"
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)">
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)"
>
<option value="left">Sol</option>
<option value="center">Orta</option>
<option value="right">Sag</option>

View File

@@ -121,11 +121,20 @@ export function useLayoutEngine(
// --- Barcode üretimi (WASM üzerinden) ---
let barcodeReqId = 0
const barcodeCallbacks = new Map<number, (result: { width: number; height: number; rgba: ArrayBuffer } | null) => void>()
const barcodeCallbacks = new Map<
number,
(result: { width: number; height: number; rgba: ArrayBuffer } | null) => void
>()
function generateBarcode(format: string, value: string, width: number, height: number, includeText: boolean = false): Promise<{ width: number; height: number; rgba: ArrayBuffer } | null> {
function generateBarcode(
format: string,
value: string,
width: number,
height: number,
includeText: boolean = false,
): Promise<{ width: number; height: number; rgba: ArrayBuffer } | null> {
if (!worker) initWorker()
return new Promise(resolve => {
return new Promise((resolve) => {
barcodeReqId++
const id = barcodeReqId
const timeout = setTimeout(() => {
@@ -140,11 +149,17 @@ export function useLayoutEngine(
})
}
function handleBarcodeResponse(msg: Extract<WorkerResponse, { type: 'barcode-result' } | { type: 'barcode-error' }>) {
function handleBarcodeResponse(
msg: Extract<WorkerResponse, { type: 'barcode-result' } | { type: 'barcode-error' }>,
) {
const cb = barcodeCallbacks.get(msg.id)
if (cb) {
barcodeCallbacks.delete(msg.id)
cb(msg.type === 'barcode-result' ? { width: msg.width, height: msg.height, rgba: msg.rgba } : null)
cb(
msg.type === 'barcode-result'
? { width: msg.width, height: msg.height, rgba: msg.rgba }
: null,
)
}
}

View File

@@ -13,7 +13,7 @@ export interface SnapResult {
}
interface EdgeSet {
verticals: number[] // x positions in mm (left, right, center of elements + page)
verticals: number[] // x positions in mm (left, right, center of elements + page)
horizontals: number[] // y positions in mm (top, bottom, center of elements + page)
}
@@ -27,9 +27,9 @@ export function useSnapGuides() {
layoutMap: Record<string, ElementLayout>,
excludeId: string,
pageWidth: number,
pageHeight: number
pageHeight: number,
) {
const verticals: number[] = [0, pageWidth / 2, pageWidth] // page edges + center
const verticals: number[] = [0, pageWidth / 2, pageWidth] // page edges + center
const horizontals: number[] = [0, pageHeight / 2, pageHeight]
for (const [id, el] of Object.entries(layoutMap)) {
@@ -48,7 +48,7 @@ export function useSnapGuides() {
proposedX_mm: number,
proposedY_mm: number,
width_mm: number,
height_mm: number
height_mm: number,
): SnapResult {
if (!cachedEdges) {
return { snappedX_mm: proposedX_mm, snappedY_mm: proposedY_mm, guides: [] }
@@ -132,13 +132,12 @@ export function useSnapGuides() {
/** Calculate snap for resize edge */
function calculateResizeSnap(
edge: 'left' | 'right' | 'top' | 'bottom',
proposedValue_mm: number
proposedValue_mm: number,
): number {
if (!cachedEdges) return proposedValue_mm
const targets = (edge === 'left' || edge === 'right')
? cachedEdges.verticals
: cachedEdges.horizontals
const targets =
edge === 'left' || edge === 'right' ? cachedEdges.verticals : cachedEdges.horizontals
const guides: SnapGuide[] = []
let snapped = proposedValue_mm
@@ -154,7 +153,7 @@ export function useSnapGuides() {
if (snapped !== proposedValue_mm) {
guides.push({
type: (edge === 'left' || edge === 'right') ? 'vertical' : 'horizontal',
type: edge === 'left' || edge === 'right' ? 'vertical' : 'horizontal',
position_mm: snapped,
})
}

View File

@@ -29,7 +29,7 @@ export function useUndoRedo<T>(source: Ref<T>, maxHistory = 50) {
redoStack.value = []
}, 300)
},
{ deep: true }
{ deep: true },
)
function undo() {

View File

@@ -148,7 +148,7 @@ describe('findScalarFields', () => {
// firma.unvan, firma.vergiNo, fatura.no, fatura.tutar, fatura.tarih = 5
expect(scalars).toHaveLength(5)
const paths = scalars.map(s => s.path)
const paths = scalars.map((s) => s.path)
expect(paths).toContain('firma.unvan')
expect(paths).toContain('firma.vergiNo')
expect(paths).toContain('fatura.no')
@@ -159,7 +159,7 @@ describe('findScalarFields', () => {
it('does not include object or array nodes', () => {
const tree = parseSchema(testSchema)
const scalars = findScalarFields(tree)
const types = scalars.map(s => s.type)
const types = scalars.map((s) => s.type)
expect(types).not.toContain('object')
expect(types).not.toContain('array')
@@ -195,17 +195,38 @@ describe('defaultAlignForSchema', () => {
})
it('returns right for currency format', () => {
const node: SchemaNode = { path: 'x', key: 'x', title: 'X', type: 'string', format: 'currency', children: [] }
const node: SchemaNode = {
path: 'x',
key: 'x',
title: 'X',
type: 'string',
format: 'currency',
children: [],
}
expect(defaultAlignForSchema(node)).toBe('right')
})
it('returns right for percentage format', () => {
const node: SchemaNode = { path: 'x', key: 'x', title: 'X', type: 'string', format: 'percentage', children: [] }
const node: SchemaNode = {
path: 'x',
key: 'x',
title: 'X',
type: 'string',
format: 'percentage',
children: [],
}
expect(defaultAlignForSchema(node)).toBe('right')
})
it('returns center for date format', () => {
const node: SchemaNode = { path: 'x', key: 'x', title: 'X', type: 'string', format: 'date', children: [] }
const node: SchemaNode = {
path: 'x',
key: 'x',
title: 'X',
type: 'string',
format: 'date',
children: [],
}
expect(defaultAlignForSchema(node)).toBe('center')
})

View File

@@ -57,10 +57,13 @@ function mockColumnValue(field: string, format: string | undefined, index: numbe
const lower = field.toLowerCase()
if (lower.includes('sira') || lower.includes('no') || lower === 'id') return index + 1
if (lower.includes('miktar') || lower.includes('adet')) return [2, 1, 5][index % 3]
if (lower.includes('fiyat') || lower.includes('tutar') || lower.includes('toplam')) return [1500, 2750, 500][index % 3]
if (lower.includes('fiyat') || lower.includes('tutar') || lower.includes('toplam'))
return [1500, 2750, 500][index % 3]
if (lower.includes('birim')) return ['Adet', 'Saat', 'Adet'][index % 3]
if (lower.includes('tarih') || lower.includes('date')) return ['2026-01-15', '2026-02-20', '2026-03-10'][index % 3]
if (lower.includes('ad') || lower.includes('isim') || lower.includes('name')) return ['Kalem A', 'Kalem B', 'Kalem C'][index % 3]
if (lower.includes('tarih') || lower.includes('date'))
return ['2026-01-15', '2026-02-20', '2026-03-10'][index % 3]
if (lower.includes('ad') || lower.includes('isim') || lower.includes('name'))
return ['Kalem A', 'Kalem B', 'Kalem C'][index % 3]
return `Ornek ${index + 1}`
}

View File

@@ -4,11 +4,11 @@
*/
export interface SchemaNode {
path: string // Noktalı yol — ör: "firma.unvan"
key: string // Son segment — ör: "unvan"
title: string // Görüntüleme adı — schema'daki "title" veya key
path: string // Noktalı yol — ör: "firma.unvan"
key: string // Son segment — ör: "unvan"
title: string // Görüntüleme adı — schema'daki "title" veya key
type: 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean'
format?: string // "currency", "date", "percentage", "image" vs.
format?: string // "currency", "date", "percentage", "image" vs.
children: SchemaNode[]
/** Sadece array tipi için: array item'larının alt alanları */
itemProperties?: SchemaNode[]
@@ -73,7 +73,12 @@ export function findArrayFields(node: SchemaNode): SchemaNode[] {
/** Schema ağacından tüm scalar alanları bulur (metin binding için) */
export function findScalarFields(node: SchemaNode): SchemaNode[] {
const result: SchemaNode[] = []
if (node.type === 'string' || node.type === 'number' || node.type === 'integer' || node.type === 'boolean') {
if (
node.type === 'string' ||
node.type === 'number' ||
node.type === 'integer' ||
node.type === 'boolean'
) {
result.push(node)
}
for (const child of node.children) {
@@ -83,13 +88,19 @@ export function findScalarFields(node: SchemaNode): SchemaNode[] {
}
/** Format tipinden FormatType'a dönüşüm */
export function schemaFormatToFormatType(format?: string): 'currency' | 'date' | 'percentage' | 'number' | undefined {
export function schemaFormatToFormatType(
format?: string,
): 'currency' | 'date' | 'percentage' | 'number' | undefined {
if (!format) return undefined
switch (format) {
case 'currency': return 'currency'
case 'date': return 'date'
case 'percentage': return 'percentage'
default: return undefined
case 'currency':
return 'currency'
case 'date':
return 'date'
case 'percentage':
return 'percentage'
default:
return undefined
}
}

View File

@@ -180,7 +180,7 @@ export interface ShapeElement extends BaseElement {
}
export interface CheckboxStyle {
size?: number // mm — kare boyutu
size?: number // mm — kare boyutu
checkColor?: string // checkmark rengi
borderColor?: string
borderWidth?: number
@@ -251,11 +251,11 @@ export interface ChartAxis {
export interface ChartStyle {
colors?: string[]
backgroundColor?: string
barGap?: number // 0.0-1.0
lineWidth?: number // mm
barGap?: number // 0.0-1.0
lineWidth?: number // mm
showPoints?: boolean
curveType?: 'linear' | 'smooth'
innerRadius?: number // 0=pie, >0=donut (0-0.9)
innerRadius?: number // 0=pie, >0=donut (0-0.9)
}
export interface ChartElement extends BaseElement {
@@ -293,7 +293,21 @@ export interface RepeatingTableElement extends BaseElement {
repeatHeader?: boolean
}
export type LeafElement = StaticTextElement | TextElement | LineElement | RepeatingTableElement | ImageElement | PageNumberElement | BarcodeElement | PageBreakElement | CurrentDateElement | ShapeElement | CheckboxElement | CalculatedTextElement | RichTextElement | ChartElement
export type LeafElement =
| StaticTextElement
| TextElement
| LineElement
| RepeatingTableElement
| ImageElement
| PageNumberElement
| BarcodeElement
| PageBreakElement
| CurrentDateElement
| ShapeElement
| CheckboxElement
| CalculatedTextElement
| RichTextElement
| ChartElement
export type TemplateElement = LeafElement | ContainerElement
// --- Template ---
@@ -330,10 +344,7 @@ export function isLeaf(el: TemplateElement): el is LeafElement {
}
/** Ağaçta bir element'i ID ile bulur */
export function findElementById(
root: ContainerElement,
id: string
): TemplateElement | undefined {
export function findElementById(root: ContainerElement, id: string): TemplateElement | undefined {
if (root.id === id) return root
for (const child of root.children) {
if (child.id === id) return child
@@ -346,10 +357,7 @@ export function findElementById(
}
/** Bir element'in parent container'ını bulur */
export function findParent(
root: ContainerElement,
id: string
): ContainerElement | undefined {
export function findParent(root: ContainerElement, id: string): ContainerElement | undefined {
for (const child of root.children) {
if (child.id === id) return root
if (isContainer(child)) {

View File

@@ -58,23 +58,39 @@ let installed = false
export function setupTooltips() {
if (installed) return
installed = true
document.addEventListener('pointerenter', (e) => {
const target = closest(e.target)
if (target) show(target)
}, true)
document.addEventListener(
'pointerenter',
(e) => {
const target = closest(e.target)
if (target) show(target)
},
true,
)
document.addEventListener('pointerleave', (e) => {
const target = closest(e.target)
if (target && target === currentTarget) hide()
}, true)
document.addEventListener(
'pointerleave',
(e) => {
const target = closest(e.target)
if (target && target === currentTarget) hide()
},
true,
)
document.addEventListener('focusin', (e) => {
const target = closest(e.target)
if (target) show(target)
}, true)
document.addEventListener(
'focusin',
(e) => {
const target = closest(e.target)
if (target) show(target)
},
true,
)
document.addEventListener('focusout', (e) => {
const target = closest(e.target)
if (target && target === currentTarget) hide()
}, true)
document.addEventListener(
'focusout',
(e) => {
const target = closest(e.target)
if (target && target === currentTarget) hide()
},
true,
)
}

View File

@@ -15,15 +15,18 @@ export interface DreportEditorConfig {
apiBaseUrl?: string
}
const props = withDefaults(defineProps<{
schema: JsonSchema
modelValue: Template
data?: Record<string, unknown>
config?: DreportEditorConfig
handleErrors?: boolean
}>(), {
handleErrors: true,
})
const props = withDefaults(
defineProps<{
schema: JsonSchema
modelValue: Template
data?: Record<string, unknown>
config?: DreportEditorConfig
handleErrors?: boolean
}>(),
{
handleErrors: true,
},
)
const emit = defineEmits<{
'update:modelValue': [value: Template]
@@ -45,34 +48,55 @@ onMounted(() => {
schemaStore.setSchema(props.schema)
syncing = true
templateStore.template = JSON.parse(JSON.stringify(props.modelValue))
nextTick(() => { syncing = false })
nextTick(() => {
syncing = false
})
templateStore.setOverrideData(props.data ?? null)
setupTooltips()
})
watch(() => props.schema, (val) => {
schemaStore.setSchema(val)
}, { deep: true })
watch(
() => props.schema,
(val) => {
schemaStore.setSchema(val)
},
{ deep: true },
)
watch(() => props.data, (val) => {
templateStore.setOverrideData(val ?? null)
}, { deep: true })
watch(
() => props.data,
(val) => {
templateStore.setOverrideData(val ?? null)
},
{ deep: true },
)
// Template: prop → store (only on reference change from parent)
watch(() => props.modelValue, (val) => {
if (syncing) return
syncing = true
templateStore.template = JSON.parse(JSON.stringify(val))
nextTick(() => { syncing = false })
})
watch(
() => props.modelValue,
(val) => {
if (syncing) return
syncing = true
templateStore.template = JSON.parse(JSON.stringify(val))
nextTick(() => {
syncing = false
})
},
)
// Template: store → emit
watch(() => templateStore.template, (val) => {
if (syncing) return
syncing = true
emit('update:modelValue', JSON.parse(JSON.stringify(val)))
nextTick(() => { syncing = false })
}, { deep: true })
watch(
() => templateStore.template,
(val) => {
if (syncing) return
syncing = true
emit('update:modelValue', JSON.parse(JSON.stringify(val)))
nextTick(() => {
syncing = false
})
},
{ deep: true },
)
// --- Error forwarding ---
@@ -85,7 +109,8 @@ function onCompileError(error: string | null) {
function onKeyDown(e: KeyboardEvent) {
const target = e.target as HTMLElement
const tag = target?.tagName
const isInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target?.isContentEditable
const isInput =
tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target?.isContentEditable
// Delete / Backspace — çoklu seçim desteği
if ((e.key === 'Delete' || e.key === 'Backspace') && editorStore.selectedElementIds.size > 0) {
@@ -116,7 +141,11 @@ function onKeyDown(e: KeyboardEvent) {
}
// Z-Order kısayolları
if ((e.ctrlKey || e.metaKey) && editorStore.selectedElementId && editorStore.selectedElementId !== 'root') {
if (
(e.ctrlKey || e.metaKey) &&
editorStore.selectedElementId &&
editorStore.selectedElementId !== 'root'
) {
if (e.key === ']' && e.shiftKey) {
e.preventDefault()
templateStore.bringToFront(editorStore.selectedElementId)
@@ -202,8 +231,20 @@ defineExpose({
<div class="dreport-editor">
<aside class="dreport-editor__sidebar dreport-editor__sidebar--left">
<div class="sidebar-tabs">
<button class="sidebar-tab" :class="{ 'sidebar-tab--active': leftTab === 'tools' }" @click="leftTab = 'tools'">Araclar</button>
<button class="sidebar-tab" :class="{ 'sidebar-tab--active': leftTab === 'schema' }" @click="leftTab = 'schema'">Schema</button>
<button
class="sidebar-tab"
:class="{ 'sidebar-tab--active': leftTab === 'tools' }"
@click="leftTab = 'tools'"
>
Araclar
</button>
<button
class="sidebar-tab"
:class="{ 'sidebar-tab--active': leftTab === 'schema' }"
@click="leftTab = 'schema'"
>
Schema
</button>
</div>
<ToolboxPanel v-if="leftTab === 'tools'" />
<SchemaTreePanel v-else />

View File

@@ -24,13 +24,17 @@ const template = ref<Template>({
page: { width: 210, height: 297 },
fonts: ['Noto Sans'],
root: {
id: 'root', type: 'container',
id: 'root',
type: 'container',
position: { type: 'flow' },
size: { width: { type: 'auto' }, height: { type: 'auto' } },
direction: 'column', gap: 0,
direction: 'column',
gap: 0,
padding: { top: 0, right: 0, bottom: 0, left: 0 },
align: 'stretch', justify: 'start',
style: {}, children: [],
align: 'stretch',
justify: 'start',
style: {},
children: [],
},
})
const data = ref<Record<string, unknown>>({})
@@ -81,11 +85,7 @@ onMounted(() => {
:data-render-ready="ready || undefined"
:data-render-error="errorMsg || undefined"
>
<LayoutRenderer
v-if="layout"
:layout="layout"
:scale="SCALE"
/>
<LayoutRenderer v-if="layout" :layout="layout" :scale="SCALE" />
<div v-else-if="errorMsg" class="error">{{ errorMsg }}</div>
<div v-else class="loading">Computing layout...</div>
</div>

View File

@@ -12,7 +12,13 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useTemplateStore } from '../template'
import { useEditorStore } from '../editor'
import type { Template, StaticTextElement, ContainerElement, ImageElement, TemplateElement } from '../../core/types'
import type {
Template,
StaticTextElement,
ContainerElement,
ImageElement,
TemplateElement,
} from '../../core/types'
import { sz } from '../../core/types'
function createTestTemplate(): Template {
@@ -392,52 +398,52 @@ describe('3.2 Z-Order controls', () => {
const store = setupThreeElements()
// Sıra: [a, b, c] → bringForward(a) → [b, a, c]
store.bringForward('a')
expect(store.template.root.children.map(c => c.id)).toEqual(['b', 'a', 'c'])
expect(store.template.root.children.map((c) => c.id)).toEqual(['b', 'a', 'c'])
})
it('sendBackward moves element one step down', () => {
const store = setupThreeElements()
// Sıra: [a, b, c] → sendBackward(c) → [a, c, b]
store.sendBackward('c')
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'c', 'b'])
expect(store.template.root.children.map((c) => c.id)).toEqual(['a', 'c', 'b'])
})
it('bringToFront moves element to end', () => {
const store = setupThreeElements()
// Sıra: [a, b, c] → bringToFront(a) → [b, c, a]
store.bringToFront('a')
expect(store.template.root.children.map(c => c.id)).toEqual(['b', 'c', 'a'])
expect(store.template.root.children.map((c) => c.id)).toEqual(['b', 'c', 'a'])
})
it('sendToBack moves element to beginning', () => {
const store = setupThreeElements()
// Sıra: [a, b, c] → sendToBack(c) → [c, a, b]
store.sendToBack('c')
expect(store.template.root.children.map(c => c.id)).toEqual(['c', 'a', 'b'])
expect(store.template.root.children.map((c) => c.id)).toEqual(['c', 'a', 'b'])
})
it('bringForward on last element is no-op', () => {
const store = setupThreeElements()
store.bringForward('c')
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'b', 'c'])
expect(store.template.root.children.map((c) => c.id)).toEqual(['a', 'b', 'c'])
})
it('sendBackward on first element is no-op', () => {
const store = setupThreeElements()
store.sendBackward('a')
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'b', 'c'])
expect(store.template.root.children.map((c) => c.id)).toEqual(['a', 'b', 'c'])
})
it('bringToFront on last element is no-op', () => {
const store = setupThreeElements()
store.bringToFront('c')
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'b', 'c'])
expect(store.template.root.children.map((c) => c.id)).toEqual(['a', 'b', 'c'])
})
it('sendToBack on first element is no-op', () => {
const store = setupThreeElements()
store.sendToBack('a')
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'b', 'c'])
expect(store.template.root.children.map((c) => c.id)).toEqual(['a', 'b', 'c'])
})
})

View File

@@ -78,7 +78,7 @@ describe('useTemplateStore', () => {
store.addChild('root', createTextElement('b', 'B'))
store.addChild('root', createTextElement('c', 'C'), 1)
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'c', 'b'])
expect(store.template.root.children.map((c) => c.id)).toEqual(['a', 'c', 'b'])
})
it('removeElement removes element', () => {
@@ -133,7 +133,7 @@ describe('useTemplateStore', () => {
store.reorderChild('root', 0, 2)
expect(store.template.root.children.map(c => c.id)).toEqual(['b', 'c', 'a'])
expect(store.template.root.children.map((c) => c.id)).toEqual(['b', 'c', 'a'])
})
it('exportTemplate returns valid JSON', () => {

View File

@@ -1,6 +1,12 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Template, TemplateElement, ContainerElement, SizeConstraint, PositionMode } from '../core/types'
import type {
Template,
TemplateElement,
ContainerElement,
SizeConstraint,
PositionMode,
} from '../core/types'
import { findElementById, findParent, isContainer, sz } from '../core/types'
import { generateMockData } from '../core/mock-data-generator'
import { useUndoRedo } from '../composables/useUndoRedo'
@@ -132,7 +138,7 @@ export const useTemplateStore = defineStore('template', () => {
function removeElement(elementId: string) {
const parent = getParent(elementId)
if (!parent) return
const idx = parent.children.findIndex(c => c.id === elementId)
const idx = parent.children.findIndex((c) => c.id === elementId)
if (idx !== -1) {
parent.children.splice(idx, 1)
bumpLayoutVersion()
@@ -146,7 +152,7 @@ export const useTemplateStore = defineStore('template', () => {
// Ağaçtan kaldır (bump'sız)
const parent = getParent(elementId)
if (parent) {
const idx = parent.children.findIndex(c => c.id === elementId)
const idx = parent.children.findIndex((c) => c.id === elementId)
if (idx !== -1) parent.children.splice(idx, 1)
}
// Hedef container'a ekle (bump'sız)
@@ -202,7 +208,7 @@ export const useTemplateStore = defineStore('template', () => {
function bringForward(elementId: string) {
const parent = getParent(elementId)
if (!parent) return
const idx = parent.children.findIndex(c => c.id === elementId)
const idx = parent.children.findIndex((c) => c.id === elementId)
if (idx < 0 || idx >= parent.children.length - 1) return
reorderChild(parent.id, idx, idx + 1)
}
@@ -211,7 +217,7 @@ export const useTemplateStore = defineStore('template', () => {
function sendBackward(elementId: string) {
const parent = getParent(elementId)
if (!parent) return
const idx = parent.children.findIndex(c => c.id === elementId)
const idx = parent.children.findIndex((c) => c.id === elementId)
if (idx <= 0) return
reorderChild(parent.id, idx, idx - 1)
}
@@ -220,7 +226,7 @@ export const useTemplateStore = defineStore('template', () => {
function bringToFront(elementId: string) {
const parent = getParent(elementId)
if (!parent) return
const idx = parent.children.findIndex(c => c.id === elementId)
const idx = parent.children.findIndex((c) => c.id === elementId)
if (idx < 0 || idx >= parent.children.length - 1) return
reorderChild(parent.id, idx, parent.children.length - 1)
}
@@ -229,7 +235,7 @@ export const useTemplateStore = defineStore('template', () => {
function sendToBack(elementId: string) {
const parent = getParent(elementId)
if (!parent) return
const idx = parent.children.findIndex(c => c.id === elementId)
const idx = parent.children.findIndex((c) => c.id === elementId)
if (idx <= 0) return
reorderChild(parent.id, idx, 0)
}
@@ -251,7 +257,11 @@ export const useTemplateStore = defineStore('template', () => {
if (!parsed.root || parsed.root.type !== 'container') {
throw new Error('Geçersiz şablon: root alanı eksik veya container değil')
}
if (!parsed.page || typeof parsed.page.width !== 'number' || typeof parsed.page.height !== 'number') {
if (
!parsed.page ||
typeof parsed.page.width !== 'number' ||
typeof parsed.page.height !== 'number'
) {
throw new Error('Geçersiz şablon: page alanı eksik veya geçersiz')
}
template.value = parsed

View File

@@ -44,7 +44,8 @@
box-sizing: border-box;
}
html, body {
html,
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

View File

@@ -2,7 +2,12 @@
/// Template JSON + Data JSON → Layout WASM → LayoutResult
/// Font loading is dynamic — fetches from backend API based on template needs.
import init, { loadFonts, addFonts, computeLayout, generateBarcode } from '../core/wasm-layout/dreport_layout.js'
import init, {
loadFonts,
addFonts,
computeLayout,
generateBarcode,
} from '../core/wasm-layout/dreport_layout.js'
import type { LayoutResult } from '../core/layout-types'
let initPromise: Promise<void> | null = null
@@ -35,7 +40,9 @@ async function doInit() {
fontCatalog = await res.json()
console.log(`[layout-worker] Font kataloğu yüklendi (${fontCatalog.length} aile)`)
} else {
console.warn(`[layout-worker] Font kataloğu alınamadı (HTTP ${res.status}), static fallback deneniyor`)
console.warn(
`[layout-worker] Font kataloğu alınamadı (HTTP ${res.status}), static fallback deneniyor`,
)
await loadStaticFallback()
return
}
@@ -68,7 +75,7 @@ async function loadStaticFallback() {
if (res.ok) {
buffers.push(new Uint8Array(await res.arrayBuffer()))
}
})
}),
)
if (buffers.length > 0) {
@@ -81,13 +88,13 @@ async function loadStaticFallback() {
/** Load all variants of given families from the API into WASM */
async function ensureFamiliesLoaded(families: string[]): Promise<void> {
const toLoad = families.filter(f => !loadedFamilies.has(f.toLowerCase()))
const toLoad = families.filter((f) => !loadedFamilies.has(f.toLowerCase()))
if (toLoad.length === 0) return
const buffers: Uint8Array[] = []
for (const family of toLoad) {
const info = fontCatalog.find(f => f.family.toLowerCase() === family.toLowerCase())
const info = fontCatalog.find((f) => f.family.toLowerCase() === family.toLowerCase())
if (!info) {
console.warn(`[layout-worker] Font ailesi bulunamadı: ${family}`)
continue
@@ -132,7 +139,15 @@ function ensureInit(): Promise<void> {
type WorkerMessage =
| { type: 'compile'; templateJson: string; dataJson: string; id: number }
| { type: 'barcode'; format: string; value: string; width: number; height: number; includeText: boolean; id: number }
| {
type: 'barcode'
format: string
value: string
width: number
height: number
includeText: boolean
id: number
}
| { type: 'configure'; fontApiBase?: string }
self.onmessage = async (e: MessageEvent<WorkerMessage>) => {