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 currentSchema.value = savedSchema
} }
watch(currentSchema, (val) => { watch(
localStorage.setItem(SCHEMA_STORAGE_KEY, JSON.stringify(val)) currentSchema,
}, { deep: true }) (val) => {
localStorage.setItem(SCHEMA_STORAGE_KEY, JSON.stringify(val))
},
{ deep: true },
)
// --- Sample Invoice Data --- // --- Sample Invoice Data ---
@@ -125,10 +129,38 @@ const sampleData: Record<string, unknown> = {
telefon: '+90 216 444 0018', telefon: '+90 216 444 0018',
}, },
kalemler: [ 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: 1,
{ siraNo: 3, adi: 'UI/UX Tasarim Hizmeti', miktar: 40, birim: 'Saat', birimFiyat: 750, tutar: 30000 }, adi: 'Web Uygulama Gelistirme',
{ siraNo: 4, adi: 'Sunucu Bakim Sozlesmesi (Yillik)', miktar: 1, birim: 'Adet', birimFiyat: 12000, tutar: 12000 }, 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 }, { siraNo: 5, adi: 'SSL Sertifikasi', miktar: 3, birim: 'Adet', birimFiyat: 500, tutar: 1500 },
], ],
toplamlar: { toplamlar: {
@@ -370,10 +402,30 @@ const defaultInvoiceTemplate: Template = {
columns: [ columns: [
{ id: 'col_sira', field: 'siraNo', title: '#', width: sz.fixed(10), align: 'center' }, { 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_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_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: { style: {
fontSize: 9, fontSize: 9,
@@ -486,12 +538,16 @@ function loadFromLocalStorage(): Template | null {
const template = ref<Template>(loadFromLocalStorage() ?? structuredClone(defaultInvoiceTemplate)) const template = ref<Template>(loadFromLocalStorage() ?? structuredClone(defaultInvoiceTemplate))
let saveTimeout: ReturnType<typeof setTimeout> | null = null let saveTimeout: ReturnType<typeof setTimeout> | null = null
watch(template, (val) => { watch(
if (saveTimeout) clearTimeout(saveTimeout) template,
saveTimeout = setTimeout(() => { (val) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(val)) if (saveTimeout) clearTimeout(saveTimeout)
}, 500) saveTimeout = setTimeout(() => {
}, { deep: true }) localStorage.setItem(STORAGE_KEY, JSON.stringify(val))
}, 500)
},
{ deep: true },
)
// --- Editor ref --- // --- Editor ref ---
@@ -626,36 +682,120 @@ function resetTemplate() {
<h1>dreport</h1> <h1>dreport</h1>
<span class="app-header__subtitle">Belge Tasarim Araci</span> <span class="app-header__subtitle">Belge Tasarim Araci</span>
<div style="flex: 1"></div> <div style="flex: 1"></div>
<input ref="fileInputRef" type="file" accept=".json" style="display: none" @change="onImportFile" /> <input
<input ref="schemaFileInputRef" type="file" accept=".json" style="display: none" @change="onSchemaImportFile" /> 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 --> <!-- Template operations -->
<button class="header-btn header-btn--secondary" @click="resetTemplate" title="Sifirla"> <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 Sifirla
</button> </button>
<button class="header-btn header-btn--secondary" @click="triggerImport" title="Sablon Yukle"> <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 Yukle
</button> </button>
<button class="header-btn header-btn--secondary" @click="exportTemplate" title="Sablon Kaydet"> <button
<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> 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 Kaydet
</button> </button>
<button class="header-btn header-btn--secondary" @click="exportBundle" title="Sablon + Schema Birlikte Kaydet"> <button
<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> 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 Paket
</button> </button>
<div class="header-divider"></div> <div class="header-divider"></div>
<!-- Schema operations --> <!-- Schema operations -->
<button class="header-btn header-btn--secondary" @click="triggerSchemaImport" title="Schema Yukle"> <button
<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> 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 Schema
</button> </button>
<button class="header-btn header-btn--secondary" @click="exportSchema" title="Schema Kaydet"> <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 Schema
</button> </button>
@@ -663,7 +803,17 @@ function resetTemplate() {
<!-- Output --> <!-- Output -->
<button class="header-btn" :disabled="pdfLoading" @click="downloadPdf"> <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' }} {{ pdfLoading ? 'Hazirlaniyor...' : 'PDF Onizle' }}
</button> </button>
</header> </header>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,7 +50,7 @@ function validateBarcode(format: BarcodeFormat, value: string): boolean {
case 'code39': case 'code39':
return /^[A-Z0-9\-. $/+%]+$/i.test(value) return /^[A-Z0-9\-. $/+%]+$/i.test(value)
case 'code128': 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': case 'qr':
return value.length > 0 return value.length > 0
default: default:
@@ -61,10 +61,14 @@ function validateBarcode(format: BarcodeFormat, value: string): boolean {
const barcodeInputValue = ref('') const barcodeInputValue = ref('')
const barcodeInputInvalid = ref(false) const barcodeInputInvalid = ref(false)
watch(() => props.element.value ?? '', (val) => { watch(
barcodeInputValue.value = val () => props.element.value ?? '',
barcodeInputInvalid.value = false (val) => {
}, { immediate: true }) barcodeInputValue.value = val
barcodeInputInvalid.value = false
},
{ immediate: true },
)
function onBarcodeValueInput(e: Event) { function onBarcodeValueInput(e: Event) {
const val = (e.target as HTMLInputElement).value 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-section__title">Barkod Ayarlari</div>
<div class="prop-row" data-tip="Barkod formati"> <div class="prop-row" data-tip="Barkod formati">
<label class="prop-label">Format</label> <label class="prop-label">Format</label>
<select class="prop-input prop-select" <select
class="prop-input prop-select"
:value="element.format" :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="qr">QR Kod</option>
<option value="ean13">EAN-13</option> <option value="ean13">EAN-13</option>
<option value="ean8">EAN-8</option> <option value="ean8">EAN-8</option>
@@ -108,44 +116,70 @@ function onBarcodeFormatChange(newFormat: BarcodeFormat) {
</div> </div>
<div class="prop-row" data-tip="Barkod icerigi formata uygun olmali"> <div class="prop-row" data-tip="Barkod icerigi formata uygun olmali">
<label class="prop-label">Deger</label> <label class="prop-label">Deger</label>
<input class="prop-input" type="text" <input
class="prop-input"
type="text"
:class="{ 'prop-input--invalid': barcodeInputInvalid }" :class="{ 'prop-input--invalid': barcodeInputInvalid }"
:value="barcodeInputValue" :value="barcodeInputValue"
@input="onBarcodeValueInput" /> @input="onBarcodeValueInput"
/>
</div> </div>
<div class="prop-row" data-tip="Barkod cizgi/modül rengi"> <div class="prop-row" data-tip="Barkod cizgi/modül rengi">
<label class="prop-label">Renk</label> <label class="prop-label">Renk</label>
<div class="prop-row-inline"> <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'" :value="element.style.color ?? '#000000'"
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" /> @input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)"
<button v-if="element.style.color" class="prop-clear" @click="updateStyle('color', undefined)">x</button> />
<button
v-if="element.style.color"
class="prop-clear"
@click="updateStyle('color', undefined)"
>
x
</button>
</div> </div>
</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> <label class="prop-label">Metin Goster</label>
<input type="checkbox" <input
:checked="element.style.includeText ?? (element.format === 'ean13' || element.format === 'ean8')" type="checkbox"
@change="(e) => updateStyle('includeText', (e.target as HTMLInputElement).checked)" /> :checked="
element.style.includeText ?? (element.format === 'ean13' || element.format === 'ean8')
"
@change="(e) => updateStyle('includeText', (e.target as HTMLInputElement).checked)"
/>
</div> </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> <label class="prop-label">Veri Baglama</label>
<select class="prop-input prop-select" <select
class="prop-input prop-select"
:value="element.binding?.path ?? ''" :value="element.binding?.path ?? ''"
@change="(e) => { @change="
const val = (e.target as HTMLSelectElement).value (e) => {
if (val) { const val = (e.target as HTMLSelectElement).value
update({ binding: { type: 'scalar', path: val } } as any) if (val) {
} else { update({ binding: { type: 'scalar', path: val } } as any)
update({ binding: undefined } as any) } else {
update({ binding: undefined } as any)
}
} }
}"> "
>
<option value="">Yok (statik deger)</option> <option value="">Yok (statik deger)</option>
<option <option v-for="field in schemaStore.scalarFields" :key="field.path" :value="field.path">
v-for="field in schemaStore.scalarFields" {{ field.title }} ({{ field.path }})
:key="field.path" </option>
:value="field.path"
>{{ field.title }} ({{ field.path }})</option>
</select> </select>
</div> </div>
</div> </div>

View File

@@ -27,18 +27,26 @@ function onExpressionChange(value: string) {
<template> <template>
<div class="prop-section"> <div class="prop-section">
<div class="prop-section__title">Hesaplanan Metin</div> <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> <label class="prop-label">Ifade</label>
<DexprEditor <DexprEditor
:model-value="element.expression" :model-value="element.expression"
@update:model-value="onExpressionChange" @update:model-value="onExpressionChange"
placeholder="toplamlar.kdv + toplamlar.araToplam" /> placeholder="toplamlar.kdv + toplamlar.araToplam"
/>
</div> </div>
<div class="prop-row" data-tip="Sonucun gosterim formati"> <div class="prop-row" data-tip="Sonucun gosterim formati">
<label class="prop-label">Format</label> <label class="prop-label">Format</label>
<select class="prop-input prop-select" <select
class="prop-input prop-select"
:value="element.format ?? ''" :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="">Yok</option>
<option value="currency">Para Birimi</option> <option value="currency">Para Birimi</option>
<option value="number">Sayi</option> <option value="number">Sayi</option>
@@ -47,30 +55,44 @@ function onExpressionChange(value: string) {
</div> </div>
<div class="prop-row" data-tip="Yazi tipi boyutu (point)"> <div class="prop-row" data-tip="Yazi tipi boyutu (point)">
<label class="prop-label">Boyut (pt)</label> <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" :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>
<div class="prop-row" data-tip="Metin rengi"> <div class="prop-row" data-tip="Metin rengi">
<label class="prop-label">Renk</label> <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'" :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>
<div class="prop-row" data-tip="Yazi tipi kalinligi"> <div class="prop-row" data-tip="Yazi tipi kalinligi">
<label class="prop-label">Kalinlik</label> <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'" :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="normal">Normal</option>
<option value="bold">Kalin</option> <option value="bold">Kalin</option>
</select> </select>
</div> </div>
<div class="prop-row" data-tip="Metnin yatay hizalamasi"> <div class="prop-row" data-tip="Metnin yatay hizalamasi">
<label class="prop-label">Hizalama</label> <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'" :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="left">Sol</option>
<option value="center">Orta</option> <option value="center">Orta</option>
<option value="right">Sag</option> <option value="right">Sag</option>

View File

@@ -33,13 +33,15 @@ const itemFields = computed(() => {
return schemaStore.getArrayItemFields(path) return schemaStore.getArrayItemFields(path)
}) })
const stringFields = computed(() => itemFields.value.filter(f => f.type === 'string')) const stringFields = computed(() => itemFields.value.filter((f) => f.type === 'string'))
const numberFields = computed(() => itemFields.value.filter(f => f.type === 'number' || f.type === 'integer')) const numberFields = computed(() =>
itemFields.value.filter((f) => f.type === 'number' || f.type === 'integer'),
)
function updateDataSource(path: string) { function updateDataSource(path: string) {
const fields = schemaStore.getArrayItemFields(path) const fields = schemaStore.getArrayItemFields(path)
const strField = fields.find(f => f.type === 'string') const strField = fields.find((f) => f.type === 'string')
const numField = fields.find(f => f.type === 'number' || f.type === 'integer') const numField = fields.find((f) => f.type === 'number' || f.type === 'integer')
update({ update({
dataSource: { type: 'array', path }, dataSource: { type: 'array', path },
categoryField: strField?.key ?? fields[0]?.key ?? '', categoryField: strField?.key ?? fields[0]?.key ?? '',
@@ -73,7 +75,9 @@ const hasGroup = computed(() => !!props.element.groupField)
// Renk paleti (default 6 renk) // Renk paleti (default 6 renk)
const colorList = computed(() => { 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) { function updateColor(index: number, value: string) {
@@ -99,7 +103,11 @@ function removeColor(index: number) {
<div class="prop-section"> <div class="prop-section">
<div class="prop-section__title">Grafik Tipi</div> <div class="prop-section__title">Grafik Tipi</div>
<div class="prop-row"> <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="bar">Bar</option>
<option value="line">Line</option> <option value="line">Line</option>
<option value="pie">Pie</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-section__title">Veri Kaynagi</div>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Array</label> <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 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> </select>
</div> </div>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Kategori</label> <label class="prop-label">Kategori</label>
<select class="prop-input prop-select" :value="element.categoryField" @change="update({ categoryField: ($event.target as HTMLSelectElement).value })"> <select
<option v-for="f in itemFields" :key="f.key" :value="f.key">{{ f.title || f.key }}</option> 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> </select>
</div> </div>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Deger</label> <label class="prop-label">Deger</label>
<select class="prop-input prop-select" :value="element.valueField" @change="update({ valueField: ($event.target as HTMLSelectElement).value })"> <select
<option v-for="f in numberFields" :key="f.key" :value="f.key">{{ f.title || f.key }}</option> 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> </select>
</div> </div>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Gruplama</label> <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 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> </select>
</div> </div>
<div v-if="hasGroup && !isPie" class="prop-row"> <div v-if="hasGroup && !isPie" class="prop-row">
<label class="prop-label">Grup Modu</label> <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="grouped">Yan Yana</option>
<option value="stacked">Yigin</option> <option value="stacked">Yigin</option>
</select> </select>
@@ -150,19 +186,40 @@ function removeColor(index: number) {
<div class="prop-section__title">Baslik</div> <div class="prop-section__title">Baslik</div>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Metin</label> <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>
<div class="prop-row" v-if="element.title?.text"> <div class="prop-row" v-if="element.title?.text">
<label class="prop-label">Boyut</label> <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>
<div class="prop-row" v-if="element.title?.text"> <div class="prop-row" v-if="element.title?.text">
<label class="prop-label">Renk</label> <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>
<div class="prop-row" v-if="element.title?.text"> <div class="prop-row" v-if="element.title?.text">
<label class="prop-label">Hiza</label> <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="left">Sol</option>
<option value="center">Orta</option> <option value="center">Orta</option>
<option value="right">Sag</option> <option value="right">Sag</option>
@@ -175,12 +232,20 @@ function removeColor(index: number) {
<div class="prop-section__title">Gosterge</div> <div class="prop-section__title">Gosterge</div>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Goster</label> <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> </div>
<template v-if="element.legend?.show"> <template v-if="element.legend?.show">
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Konum</label> <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="top">Ust</option>
<option value="bottom">Alt</option> <option value="bottom">Alt</option>
<option value="right">Sag</option> <option value="right">Sag</option>
@@ -188,7 +253,15 @@ function removeColor(index: number) {
</div> </div>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Boyut</label> <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> </div>
</template> </template>
</div> </div>
@@ -198,16 +271,33 @@ function removeColor(index: number) {
<div class="prop-section__title">Etiketler</div> <div class="prop-section__title">Etiketler</div>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Goster</label> <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> </div>
<template v-if="element.labels?.show"> <template v-if="element.labels?.show">
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Boyut</label> <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>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Renk</label> <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> </div>
</template> </template>
</div> </div>
@@ -217,19 +307,40 @@ function removeColor(index: number) {
<div class="prop-section__title">Eksenler</div> <div class="prop-section__title">Eksenler</div>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">X Etiketi</label> <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>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Y Etiketi</label> <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>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Izgara</label> <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>
<div class="prop-row" v-if="element.axis?.showGrid !== false"> <div class="prop-row" v-if="element.axis?.showGrid !== false">
<label class="prop-label">Izgara Renk</label> <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>
</div> </div>
@@ -238,14 +349,26 @@ function removeColor(index: number) {
<div class="prop-section__title">Stil</div> <div class="prop-section__title">Stil</div>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Arka Plan</label> <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> </div>
<!-- Renk Paleti --> <!-- Renk Paleti -->
<div class="prop-section__subtitle">Renk Paleti</div> <div class="prop-section__subtitle">Renk Paleti</div>
<div v-for="(color, i) in colorList" :key="i" class="prop-row"> <div v-for="(color, i) in colorList" :key="i" class="prop-row">
<input class="prop-color" type="color" :value="color" @input="updateColor(i, ($event.target as HTMLInputElement).value)"> <input
<button class="prop-btn-sm prop-btn-sm--danger" @click="removeColor(i)" title="Kaldir">×</button> 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> </div>
<button class="prop-btn-sm" @click="addColor">+ Renk Ekle</button> <button class="prop-btn-sm" @click="addColor">+ Renk Ekle</button>
</div> </div>
@@ -255,7 +378,15 @@ function removeColor(index: number) {
<div class="prop-section__title">Bar Ayarlari</div> <div class="prop-section__title">Bar Ayarlari</div>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Bar Boslugu</label> <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>
</div> </div>
@@ -263,11 +394,22 @@ function removeColor(index: number) {
<div class="prop-section__title">Line Ayarlari</div> <div class="prop-section__title">Line Ayarlari</div>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Cizgi Kalinligi</label> <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>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Noktalar</label> <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>
</div> </div>
@@ -275,11 +417,19 @@ function removeColor(index: number) {
<div class="prop-section__title">Pie Ayarlari</div> <div class="prop-section__title">Pie Ayarlari</div>
<div class="prop-row"> <div class="prop-row">
<label class="prop-label">Ic Yaricap</label> <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))"> <input
</div> class="prop-input prop-input--sm"
<div class="prop-row" style="font-size: 11px; color: #94a3b8;"> type="number"
0 = Pie, &gt;0 = Donut :value="element.style.innerRadius ?? 0"
step="0.05"
min="0"
max="0.9"
@change="
updateStyle('innerRadius', parseFloat(($event.target as HTMLInputElement).value))
"
/>
</div> </div>
<div class="prop-row" style="font-size: 11px; color: #94a3b8">0 = Pie, &gt;0 = Donut</div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -24,27 +24,40 @@ function updateStyle(key: string, value: unknown) {
<div class="prop-section__title">Onay Kutusu</div> <div class="prop-section__title">Onay Kutusu</div>
<div v-if="!element.binding" class="prop-row" data-tip="Onay kutusunun varsayilan durumu"> <div v-if="!element.binding" class="prop-row" data-tip="Onay kutusunun varsayilan durumu">
<label class="prop-label">Isaretli</label> <label class="prop-label">Isaretli</label>
<input type="checkbox" <input
type="checkbox"
:checked="element.checked ?? false" :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>
<div class="prop-row" data-tip="Onay kutusu boyutu (mm)"> <div class="prop-row" data-tip="Onay kutusu boyutu (mm)">
<label class="prop-label">Boyut (mm)</label> <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" :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>
<div class="prop-row" data-tip="Isaret (tik) rengi"> <div class="prop-row" data-tip="Isaret (tik) rengi">
<label class="prop-label">Isaret Rengi</label> <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'" :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>
<div class="prop-row" data-tip="Kutu kenarlik rengi"> <div class="prop-row" data-tip="Kutu kenarlik rengi">
<label class="prop-label">Kenar Rengi</label> <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'" :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>
</div> </div>
</template> </template>

View File

@@ -25,24 +25,37 @@ function updateStyle(key: string, value: unknown) {
<div class="prop-section__title">Container Ayarlari</div> <div class="prop-section__title">Container Ayarlari</div>
<div class="prop-row" data-tip="Cocuk elemanlarin dizilim yonu"> <div class="prop-row" data-tip="Cocuk elemanlarin dizilim yonu">
<label class="prop-label">Yon</label> <label class="prop-label">Yon</label>
<select class="prop-input prop-select" <select
class="prop-input prop-select"
:value="element.direction" :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="column">Dikey</option>
<option value="row">Yatay</option> <option value="row">Yatay</option>
</select> </select>
</div> </div>
<div class="prop-row" data-tip="Cocuk elemanlar arasi bosluk (mm)"> <div class="prop-row" data-tip="Cocuk elemanlar arasi bosluk (mm)">
<label class="prop-label">Bosluk (mm)</label> <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" :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>
<div class="prop-row" data-tip="Cocuklarin cross-axis hizalamasi"> <div class="prop-row" data-tip="Cocuklarin cross-axis hizalamasi">
<label class="prop-label">{{ element.direction === 'column' ? 'Yatay Hizalama' : 'Dikey Hizalama' }}</label> <label class="prop-label">{{
<select class="prop-input prop-select" element.direction === 'column' ? 'Yatay Hizalama' : 'Dikey Hizalama'
}}</label>
<select
class="prop-input prop-select"
:value="element.align" :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="start">{{ element.direction === 'column' ? 'Sol' : 'Ust' }}</option>
<option value="center">Orta</option> <option value="center">Orta</option>
<option value="end">{{ element.direction === 'column' ? 'Sag' : 'Alt' }}</option> <option value="end">{{ element.direction === 'column' ? 'Sag' : 'Alt' }}</option>
@@ -50,10 +63,14 @@ function updateStyle(key: string, value: unknown) {
</select> </select>
</div> </div>
<div class="prop-row" data-tip="Cocuklarin main-axis dagilimi"> <div class="prop-row" data-tip="Cocuklarin main-axis dagilimi">
<label class="prop-label">{{ element.direction === 'column' ? 'Dikey Dagilim' : 'Yatay Dagilim' }}</label> <label class="prop-label">{{
<select class="prop-input prop-select" element.direction === 'column' ? 'Dikey Dagilim' : 'Yatay Dagilim'
}}</label>
<select
class="prop-input prop-select"
:value="element.justify" :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="start">{{ element.direction === 'column' ? 'Ust' : 'Sol' }}</option>
<option value="center">Orta</option> <option value="center">Orta</option>
<option value="end">{{ element.direction === 'column' ? 'Alt' : 'Sag' }}</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"> <div class="prop-row" data-tip="Sayfa sonunda bolunmeyi kontrol eder">
<label class="prop-label">Sayfa Bolme</label> <label class="prop-label">Sayfa Bolme</label>
<select class="prop-input prop-select" <select
class="prop-input prop-select"
:value="element.breakInside ?? 'auto'" :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="auto">Izin Ver</option>
<option value="avoid">Bolme</option> <option value="avoid">Bolme</option>
</select> </select>
@@ -84,32 +103,59 @@ function updateStyle(key: string, value: unknown) {
<div class="prop-row" data-tip="Container arka plan rengi"> <div class="prop-row" data-tip="Container arka plan rengi">
<label class="prop-label">Arka plan</label> <label class="prop-label">Arka plan</label>
<div class="prop-row-inline"> <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'" :value="element.style.backgroundColor ?? '#ffffff'"
@input="(e) => updateStyle('backgroundColor', (e.target as HTMLInputElement).value)" /> @input="(e) => updateStyle('backgroundColor', (e.target as HTMLInputElement).value)"
<button v-if="element.style.backgroundColor" class="prop-clear" @click="updateStyle('backgroundColor', undefined)">x</button> />
<button
v-if="element.style.backgroundColor"
class="prop-clear"
@click="updateStyle('backgroundColor', undefined)"
>
x
</button>
</div> </div>
</div> </div>
<div class="prop-row" data-tip="Kenarlik kalinligi (mm)"> <div class="prop-row" data-tip="Kenarlik kalinligi (mm)">
<label class="prop-label">Kenarlik (mm)</label> <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" :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>
<div class="prop-row" data-tip="Kenarlik cizgisi rengi"> <div class="prop-row" data-tip="Kenarlik cizgisi rengi">
<label class="prop-label">Kenarlik rengi</label> <label class="prop-label">Kenarlik rengi</label>
<div class="prop-row-inline"> <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'" :value="element.style.borderColor ?? '#000000'"
@input="(e) => updateStyle('borderColor', (e.target as HTMLInputElement).value)" /> @input="(e) => updateStyle('borderColor', (e.target as HTMLInputElement).value)"
<button v-if="element.style.borderColor" class="prop-clear" @click="updateStyle('borderColor', undefined)">x</button> />
<button
v-if="element.style.borderColor"
class="prop-clear"
@click="updateStyle('borderColor', undefined)"
>
x
</button>
</div> </div>
</div> </div>
<div class="prop-row" data-tip="Kenarlik cizgi stili"> <div class="prop-row" data-tip="Kenarlik cizgi stili">
<label class="prop-label">Kenarlik stili</label> <label class="prop-label">Kenarlik stili</label>
<select class="prop-input prop-select" <select
class="prop-input prop-select"
:value="element.style.borderStyle ?? 'solid'" :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="solid">Duz</option>
<option value="dashed">Kesikli</option> <option value="dashed">Kesikli</option>
<option value="dotted">Noktali</option> <option value="dotted">Noktali</option>
@@ -117,9 +163,16 @@ function updateStyle(key: string, value: unknown) {
</div> </div>
<div class="prop-row" data-tip="Kose yuvarlakligi (mm)"> <div class="prop-row" data-tip="Kose yuvarlakligi (mm)">
<label class="prop-label">Radius (mm)</label> <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" :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>
</div> </div>
</template> </template>

View File

@@ -24,9 +24,11 @@ function updateStyle(key: string, value: unknown) {
<div class="prop-section__title">Tarih</div> <div class="prop-section__title">Tarih</div>
<div class="prop-row" data-tip="Tarih gosterim formati"> <div class="prop-row" data-tip="Tarih gosterim formati">
<label class="prop-label">Format</label> <label class="prop-label">Format</label>
<select class="prop-input prop-select" <select
class="prop-input prop-select"
:value="element.format ?? 'DD.MM.YYYY'" :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="DD/MM/YYYY">30/03/2026</option> <option value="DD/MM/YYYY">30/03/2026</option>
<option value="YYYY-MM-DD">2026-03-30</option> <option value="YYYY-MM-DD">2026-03-30</option>
@@ -35,21 +37,33 @@ function updateStyle(key: string, value: unknown) {
</div> </div>
<div class="prop-row" data-tip="Yazi tipi boyutu (point)"> <div class="prop-row" data-tip="Yazi tipi boyutu (point)">
<label class="prop-label">Boyut (pt)</label> <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" :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>
<div class="prop-row" data-tip="Metin rengi"> <div class="prop-row" data-tip="Metin rengi">
<label class="prop-label">Renk</label> <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'" :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>
<div class="prop-row" data-tip="Metnin yatay hizalamasi"> <div class="prop-row" data-tip="Metnin yatay hizalamasi">
<label class="prop-label">Hizalama</label> <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'" :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="left">Sol</option>
<option value="center">Orta</option> <option value="center">Orta</option>
<option value="right">Sag</option> <option value="right">Sag</option>

View File

@@ -40,7 +40,9 @@ function setMode(mode: 'static' | 'dynamic') {
update({ binding: undefined } as Partial<TemplateElement>) update({ binding: undefined } as Partial<TemplateElement>)
} else { } else {
// Dinamik moda geç — ilk uygun alanı seç veya boş bırak // 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 : '' const path = imageFields.length > 0 ? imageFields[0].path : ''
update({ src: undefined, binding: { type: 'scalar', path } } as Partial<TemplateElement>) 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) */ /** Schema'dan görsel olabilecek alanlar (format: image veya string) */
const imageScalarFields = computed(() => { 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> </script>
@@ -64,8 +66,20 @@ const imageScalarFields = computed(() => {
<div class="prop-row" data-tip="Gorsel kaynagi: dosya veya veri alanından"> <div class="prop-row" data-tip="Gorsel kaynagi: dosya veya veri alanından">
<label class="prop-label">Mod</label> <label class="prop-label">Mod</label>
<div class="prop-toggle-group"> <div class="prop-toggle-group">
<button class="prop-toggle-btn" :class="{ 'prop-toggle-btn--active': !isDynamic }" @click="setMode('static')">Statik</button> <button
<button class="prop-toggle-btn" :class="{ 'prop-toggle-btn--active': isDynamic }" @click="setMode('dynamic')">Dinamik</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>
</div> </div>
@@ -84,7 +98,9 @@ const imageScalarFields = computed(() => {
</div> </div>
<div v-if="element.src" class="prop-row" data-tip="Gorseli kaldirmak icin tiklayin"> <div v-if="element.src" class="prop-row" data-tip="Gorseli kaldirmak icin tiklayin">
<label class="prop-label"></label> <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> </div>
</template> </template>
@@ -92,15 +108,15 @@ const imageScalarFields = computed(() => {
<template v-else> <template v-else>
<div class="prop-row" data-tip="Gorsel URL'sinin gelecegi veri alani"> <div class="prop-row" data-tip="Gorsel URL'sinin gelecegi veri alani">
<label class="prop-label">Veri Alani</label> <label class="prop-label">Veri Alani</label>
<select class="prop-input prop-select" <select
class="prop-input prop-select"
:value="element.binding?.path ?? ''" :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 value="" disabled>Secin...</option>
<option <option v-for="field in imageScalarFields" :key="field.path" :value="field.path">
v-for="field in imageScalarFields" {{ field.title }} ({{ field.path }})
:key="field.path" </option>
:value="field.path"
>{{ field.title }} ({{ field.path }})</option>
</select> </select>
</div> </div>
<div v-if="element.binding?.path" class="prop-row"> <div v-if="element.binding?.path" class="prop-row">
@@ -112,9 +128,11 @@ const imageScalarFields = computed(() => {
<!-- Sığdırma modu (ortak) --> <!-- Sığdırma modu (ortak) -->
<div class="prop-row" data-tip="Gorselin alana sigdirma modu"> <div class="prop-row" data-tip="Gorselin alana sigdirma modu">
<label class="prop-label">Sigdirma</label> <label class="prop-label">Sigdirma</label>
<select class="prop-input prop-select" <select
class="prop-input prop-select"
:value="element.style.objectFit ?? 'contain'" :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="contain">Sigdir</option>
<option value="cover">Kap</option> <option value="cover">Kap</option>
<option value="stretch">Esnet</option> <option value="stretch">Esnet</option>
@@ -137,7 +155,9 @@ const imageScalarFields = computed(() => {
color: #64748b; color: #64748b;
font-size: 11px; font-size: 11px;
cursor: pointer; cursor: pointer;
transition: background 0.1s, color 0.1s; transition:
background 0.1s,
color 0.1s;
} }
.prop-toggle-btn:first-child { .prop-toggle-btn:first-child {

View File

@@ -11,7 +11,9 @@ const editorStore = useEditorStore()
function updateStyle(key: string, value: unknown) { function updateStyle(key: string, value: unknown) {
const id = editorStore.selectedElementId const id = editorStore.selectedElementId
if (!id) return 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> </script>
@@ -20,15 +22,25 @@ function updateStyle(key: string, value: unknown) {
<div class="prop-section__title">Cizgi Stili</div> <div class="prop-section__title">Cizgi Stili</div>
<div class="prop-row" data-tip="Cizgi kalinligi (mm)"> <div class="prop-row" data-tip="Cizgi kalinligi (mm)">
<label class="prop-label">Kalinlik (mm)</label> <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" :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>
<div class="prop-row" data-tip="Cizgi rengi"> <div class="prop-row" data-tip="Cizgi rengi">
<label class="prop-label">Renk</label> <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'" :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>
</div> </div>
</template> </template>

View File

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

View File

@@ -24,9 +24,11 @@ function updateStyle(key: string, value: unknown) {
<div class="prop-section__title">Sayfa Numarasi</div> <div class="prop-section__title">Sayfa Numarasi</div>
<div class="prop-row" data-tip="Sayfa numarasi gosterim formati"> <div class="prop-row" data-tip="Sayfa numarasi gosterim formati">
<label class="prop-label">Format</label> <label class="prop-label">Format</label>
<select class="prop-input prop-select" <select
class="prop-input prop-select"
:value="element.format ?? '{current} / {total}'" :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} / {total}">1 / 5</option>
<option value="{current}">1</option> <option value="{current}">1</option>
<option value="Sayfa {current}">Sayfa 1</option> <option value="Sayfa {current}">Sayfa 1</option>
@@ -35,21 +37,33 @@ function updateStyle(key: string, value: unknown) {
</div> </div>
<div class="prop-row" data-tip="Yazi tipi boyutu (point)"> <div class="prop-row" data-tip="Yazi tipi boyutu (point)">
<label class="prop-label">Boyut (pt)</label> <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" :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>
<div class="prop-row" data-tip="Metin rengi"> <div class="prop-row" data-tip="Metin rengi">
<label class="prop-label">Renk</label> <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'" :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>
<div class="prop-row" data-tip="Metnin yatay hizalamasi"> <div class="prop-row" data-tip="Metnin yatay hizalamasi">
<label class="prop-label">Hizalama</label> <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'" :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="left">Sol</option>
<option value="center">Orta</option> <option value="center">Orta</option>
<option value="right">Sag</option> <option value="right">Sag</option>

View File

@@ -20,7 +20,11 @@ function togglePositioning() {
<div class="prop-section__title">Pozisyon</div> <div class="prop-section__title">Pozisyon</div>
<div class="prop-row" data-tip="Flow: otomatik dizilim, Absolute: sabit konum"> <div class="prop-row" data-tip="Flow: otomatik dizilim, Absolute: sabit konum">
<label class="prop-label">Mod</label> <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="flow">Flow</option>
<option value="absolute">Absolute</option> <option value="absolute">Absolute</option>
</select> </select>
@@ -28,15 +32,37 @@ function togglePositioning() {
<template v-if="element.position.type === 'absolute'"> <template v-if="element.position.type === 'absolute'">
<div class="prop-row" data-tip="Yatay pozisyon parent sol kenardan uzaklik (mm)"> <div class="prop-row" data-tip="Yatay pozisyon parent sol kenardan uzaklik (mm)">
<label class="prop-label">X (mm)</label> <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" :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>
<div class="prop-row" data-tip="Dikey pozisyon parent ust kenardan uzaklik (mm)"> <div class="prop-row" data-tip="Dikey pozisyon parent ust kenardan uzaklik (mm)">
<label class="prop-label">Y (mm)</label> <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" :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> </div>
</template> </template>
</div> </div>

View File

@@ -5,7 +5,12 @@ import { useEditorStore } from '../../stores/editor'
import { useSchemaStore } from '../../stores/schema' import { useSchemaStore } from '../../stores/schema'
import { sz } from '../../core/types' import { sz } from '../../core/types'
import { schemaFormatToFormatType, defaultAlignForSchema } from '../../core/schema-parser' 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' import '../../styles/properties.css'
const props = defineProps<{ element: RepeatingTableElement }>() const props = defineProps<{ element: RepeatingTableElement }>()
@@ -27,7 +32,7 @@ function nextColId() {
function updateTableDataSource(path: string) { function updateTableDataSource(path: string) {
const itemFields = schemaStore.getArrayItemFields(path) const itemFields = schemaStore.getArrayItemFields(path)
if (itemFields.length > 0) { if (itemFields.length > 0) {
const columns: TableColumn[] = itemFields.map(field => ({ const columns: TableColumn[] = itemFields.map((field) => ({
id: nextColId(), id: nextColId(),
field: field.key, field: field.key,
title: field.title, title: field.title,
@@ -51,7 +56,7 @@ function updateTableStyle(key: string, value: unknown) {
} }
function updateColumn(colId: string, updates: Partial<TableColumn>) { 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>) update({ columns } as Partial<TemplateElement>)
} }
@@ -67,12 +72,14 @@ function addColumn() {
} }
function removeColumn(colId: string) { 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) { function moveColumn(colId: string, direction: -1 | 1) {
const cols = [...props.element.columns] 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 const newIdx = idx + direction
if (newIdx < 0 || newIdx >= cols.length) return if (newIdx < 0 || newIdx >= cols.length) return
;[cols[idx], cols[newIdx]] = [cols[newIdx], cols[idx]] ;[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-section__title">Veri Kaynagi</div>
<div class="prop-row" data-tip="Tablonun baglanacagi array veri kaynagi"> <div class="prop-row" data-tip="Tablonun baglanacagi array veri kaynagi">
<label class="prop-label">Kaynak</label> <label class="prop-label">Kaynak</label>
<select class="prop-input prop-select" <select
class="prop-input prop-select"
:value="element.dataSource.path" :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 value="" disabled>Secin...</option>
<option <option v-for="arr in schemaStore.arrayFields" :key="arr.path" :value="arr.path">
v-for="arr in schemaStore.arrayFields" {{ arr.title }} ({{ arr.path }})
:key="arr.path" </option>
:value="arr.path"
>{{ arr.title }} ({{ arr.path }})</option>
</select> </select>
</div> </div>
</div> </div>
@@ -109,26 +116,41 @@ const tableItemFields = computed(() => {
Sutunlar Sutunlar
<button class="prop-add-btn" @click="addColumn">+</button> <button class="prop-add-btn" @click="addColumn">+</button>
</div> </div>
<div <div v-for="col in element.columns" :key="col.id" class="tbl-col">
v-for="col in element.columns"
:key="col.id"
class="tbl-col"
>
<!-- Row 1: title + actions --> <!-- Row 1: title + actions -->
<div class="tbl-col__head"> <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 })" @change="(e) => updateColumn(col.id, { title: (e.target as HTMLInputElement).value })"
:placeholder="col.field" :placeholder="col.field"
data-tip="Sutun basligi" /> data-tip="Sutun basligi"
/>
<div class="tbl-col__actions"> <div class="tbl-col__actions">
<button class="tbl-col__act" @click="moveColumn(col.id, -1)" data-tip="Yukari tasi"> <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>
<button class="tbl-col__act" @click="moveColumn(col.id, 1)" data-tip="Asagi tasi"> <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>
<button class="tbl-col__act tbl-col__act--del" @click="removeColumn(col.id)" data-tip="Sutunu sil"> <button
<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> 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> </button>
</div> </div>
</div> </div>
@@ -136,37 +158,148 @@ const tableItemFields = computed(() => {
<!-- Row 2: field + align + format + width compact --> <!-- Row 2: field + align + format + width compact -->
<div class="tbl-col__controls"> <div class="tbl-col__controls">
<!-- Field --> <!-- Field -->
<select v-if="tableItemFields.length > 0" class="tbl-col__field" :value="col.field" data-tip="Veri alani" <select
@change="(e) => { v-if="tableItemFields.length > 0"
const field = (e.target as HTMLSelectElement).value class="tbl-col__field"
const node = tableItemFields.find(f => f.key === field) :value="col.field"
if (node) { data-tip="Veri alani"
updateColumn(col.id, { @change="
field, (e) => {
title: node.title, const field = (e.target as HTMLSelectElement).value
align: defaultAlignForSchema(node), const node = tableItemFields.find((f) => f.key === field)
format: schemaFormatToFormatType(node.format), if (node) {
}) updateColumn(col.id, {
} else { field,
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> <option v-for="f in tableItemFields" :key="f.key" :value="f.key">{{ f.key }}</option>
</select> </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 })" @change="(e) => updateColumn(col.id, { field: (e.target as HTMLInputElement).value })"
data-tip="Veri alani" /> data-tip="Veri alani"
/>
<!-- Alignment icons --> <!-- Alignment icons -->
<div class="tbl-col__align"> <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"> <button
<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> 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>
<button class="tbl-col__align-btn" :class="{ 'tbl-col__align-btn--on': col.align === 'center' }" @click="updateColumn(col.id, { align: 'center' })" data-tip="Ortala"> <button
<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> 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>
<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"> <button
<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> 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> </button>
</div> </div>
</div> </div>
@@ -174,8 +307,18 @@ const tableItemFields = computed(() => {
<!-- Row 3: format + width --> <!-- Row 3: format + width -->
<div class="tbl-col__extra" data-tip="Veri gosterim formati"> <div class="tbl-col__extra" data-tip="Veri gosterim formati">
<label class="tbl-col__elabel">Format</label> <label class="tbl-col__elabel">Format</label>
<select class="tbl-col__fmt" :value="col.format ?? ''" <select
@change="(e) => updateColumn(col.id, { format: ((e.target as HTMLSelectElement).value || undefined) as FormatType | undefined })"> 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="">Yok</option>
<option value="currency">Para birimi</option> <option value="currency">Para birimi</option>
<option value="number">Sayi</option> <option value="number">Sayi</option>
@@ -185,22 +328,45 @@ const tableItemFields = computed(() => {
</div> </div>
<div class="tbl-col__extra" data-tip="Sutun genislik modu"> <div class="tbl-col__extra" data-tip="Sutun genislik modu">
<label class="tbl-col__elabel">Genislik</label> <label class="tbl-col__elabel">Genislik</label>
<select class="tbl-col__wtype" :value="col.width.type" <select
@change="(e) => { class="tbl-col__wtype"
const t = (e.target as HTMLSelectElement).value :value="col.width.type"
if (t === 'auto') updateColumn(col.id, { width: { type: 'auto' } }) @change="
else if (t === 'fr') updateColumn(col.id, { width: { type: 'fr', value: 1 } }) (e) => {
else updateColumn(col.id, { width: { type: 'fixed', value: 30 } }) 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="auto">Otomatik</option>
<option value="fixed">Sabit (mm)</option> <option value="fixed">Sabit (mm)</option>
<option value="fr">Oran (fr)</option> <option value="fr">Oran (fr)</option>
</select> </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)'"> <span
<input class="tbl-col__wval" type="number" step="1" 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" :min="col.width.type === 'fixed' ? 5 : 1"
:value="(col.width as any).value" :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> </span>
</div> </div>
</div> </div>
@@ -216,15 +382,36 @@ const tableItemFields = computed(() => {
<div class="ts-val ts-val--pair"> <div class="ts-val ts-val--pair">
<span class="ts-sep">Icerik</span> <span class="ts-sep">Icerik</span>
<span class="ts-tip-wrap" data-tip="Icerik yazi boyutu (pt)"> <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" :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>
<span class="ts-sep">Header</span> <span class="ts-sep">Header</span>
<span class="ts-tip-wrap" data-tip="Header yazi boyutu (pt)"> <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" :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> </span>
</div> </div>
@@ -232,23 +419,38 @@ const tableItemFields = computed(() => {
<label class="ts-lbl" data-tip="Header, metin ve zebra satirlari renkleri">Renkler</label> <label class="ts-lbl" data-tip="Header, metin ve zebra satirlari renkleri">Renkler</label>
<div class="ts-val ts-val--colors"> <div class="ts-val ts-val--colors">
<div class="ts-color-item" data-tip="Header arkaplan rengi"> <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'" :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> <span class="ts-clbl">Arkaplan</span>
</div> </div>
<div class="ts-color-item" data-tip="Header metin rengi"> <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'" :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> <span class="ts-clbl">Metin</span>
</div> </div>
<div class="ts-color-item" data-tip="Zebra satir rengi tek satirlar"> <div class="ts-color-item" data-tip="Zebra satir rengi tek satirlar">
<div class="ts-swatch-wrap"> <div class="ts-swatch-wrap">
<input class="ts-swatch" type="color" <input
class="ts-swatch"
type="color"
:value="element.style.zebraOdd ?? '#fafafa'" :value="element.style.zebraOdd ?? '#fafafa'"
@input="(e) => updateTableStyle('zebraOdd', (e.target as HTMLInputElement).value)" /> @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> />
<button
v-if="element.style.zebraOdd"
class="ts-swatch-clr"
@click="updateTableStyle('zebraOdd', undefined)"
>
&times;
</button>
</div> </div>
<span class="ts-clbl">Zebra</span> <span class="ts-clbl">Zebra</span>
</div> </div>
@@ -258,15 +460,36 @@ const tableItemFields = computed(() => {
<label class="ts-lbl" data-tip="Tablo kenarlik rengi ve kalinligi">Kenarlik</label> <label class="ts-lbl" data-tip="Tablo kenarlik rengi ve kalinligi">Kenarlik</label>
<div class="ts-val ts-val--pair"> <div class="ts-val ts-val--pair">
<div class="ts-swatch-wrap" data-tip="Kenarlik rengi"> <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'" :value="element.style.borderColor ?? '#cccccc'"
@input="(e) => updateTableStyle('borderColor', (e.target as HTMLInputElement).value)" /> @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> />
<button
v-if="element.style.borderColor"
class="ts-swatch-clr"
@click="updateTableStyle('borderColor', undefined)"
>
&times;
</button>
</div> </div>
<span class="ts-tip-wrap" data-tip="Kenarlik kalinligi (mm)"> <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" :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>
<span class="ts-unit">mm</span> <span class="ts-unit">mm</span>
</div> </div>
@@ -276,42 +499,96 @@ const tableItemFields = computed(() => {
<div class="ts-val ts-val--pair"> <div class="ts-val ts-val--pair">
<span class="ts-pad-icon" data-tip="Yatay bosluk (mm)">&#8596;</span> <span class="ts-pad-icon" data-tip="Yatay bosluk (mm)">&#8596;</span>
<span class="ts-tip-wrap" data-tip="Yatay ic bosluk (mm)"> <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" :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>
<span class="ts-pad-icon" data-tip="Dikey bosluk (mm)">&#8597;</span> <span class="ts-pad-icon" data-tip="Dikey bosluk (mm)">&#8597;</span>
<span class="ts-tip-wrap" data-tip="Dikey ic bosluk (mm)"> <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" :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> </span>
</div> </div>
<!-- Header padding --> <!-- 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"> <div class="ts-val ts-val--pair">
<span class="ts-pad-icon" data-tip="Yatay bosluk (mm)">&#8596;</span> <span class="ts-pad-icon" data-tip="Yatay bosluk (mm)">&#8596;</span>
<span class="ts-tip-wrap" data-tip="Header yatay bosluk (mm)"> <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" :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>
<span class="ts-pad-icon" data-tip="Dikey bosluk (mm)">&#8597;</span> <span class="ts-pad-icon" data-tip="Dikey bosluk (mm)">&#8597;</span>
<span class="ts-tip-wrap" data-tip="Header dikey bosluk (mm)"> <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" :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> </span>
</div> </div>
<!-- Repeat header --> <!-- 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"> <div class="ts-val">
<label class="ts-toggle"> <label class="ts-toggle">
<input type="checkbox" <input
type="checkbox"
:checked="element.repeatHeader !== false" :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> <span class="ts-toggle__track"></span>
</label> </label>
</div> </div>
@@ -666,7 +943,7 @@ const tableItemFields = computed(() => {
background: white; background: white;
border-radius: 50%; border-radius: 50%;
transition: transform 0.15s; 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 { .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-section__title">Varsayilan Stil</div>
<div class="prop-row" data-tip="Varsayilan yazi tipi boyutu (point)"> <div class="prop-row" data-tip="Varsayilan yazi tipi boyutu (point)">
<label class="prop-label">Boyut (pt)</label> <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" :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>
<div class="prop-row" data-tip="Varsayilan metin rengi"> <div class="prop-row" data-tip="Varsayilan metin rengi">
<label class="prop-label">Renk</label> <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'" :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>
<div class="prop-row" data-tip="Metnin yatay hizalamasi"> <div class="prop-row" data-tip="Metnin yatay hizalamasi">
<label class="prop-label">Hizalama</label> <label class="prop-label">Hizalama</label>
<select class="prop-input prop-select" <select
class="prop-input prop-select"
:value="element.style.align ?? 'left'" :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="left">Sol</option>
<option value="center">Orta</option> <option value="center">Orta</option>
<option value="right">Sag</option> <option value="right">Sag</option>
@@ -82,33 +94,49 @@ function removeSpan(index: number) {
class="prop-span-card__remove" class="prop-span-card__remove"
@click="removeSpan(idx)" @click="removeSpan(idx)"
title="Sil" title="Sil"
>&times;</button> >
&times;
</button>
</div> </div>
<div class="prop-row" data-tip="Span metin icerigi"> <div class="prop-row" data-tip="Span metin icerigi">
<label class="prop-label">Metin</label> <label class="prop-label">Metin</label>
<input class="prop-input" type="text" <input
class="prop-input"
type="text"
:value="span.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>
<div class="prop-row" data-tip="Span yazi boyutu — bos birakilirsa varsayilan kullanilir"> <div class="prop-row" data-tip="Span yazi boyutu — bos birakilirsa varsayilan kullanilir">
<label class="prop-label">Boyut</label> <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 ?? ''" :value="(span.style as TextStyle).fontSize ?? ''"
placeholder="varsayilan" placeholder="varsayilan"
@input="(e) => { @input="
const v = (e.target as HTMLInputElement).value (e) => {
updateSpanStyle(idx, 'fontSize', v ? parseFloat(v) : undefined) const v = (e.target as HTMLInputElement).value
}" /> updateSpanStyle(idx, 'fontSize', v ? parseFloat(v) : undefined)
}
"
/>
</div> </div>
<div class="prop-row" data-tip="Span yazi kalinligi"> <div class="prop-row" data-tip="Span yazi kalinligi">
<label class="prop-label">Kalinlik</label> <label class="prop-label">Kalinlik</label>
<select class="prop-input prop-select" <select
class="prop-input prop-select"
:value="(span.style as TextStyle).fontWeight ?? ''" :value="(span.style as TextStyle).fontWeight ?? ''"
@change="(e) => { @change="
const v = (e.target as HTMLSelectElement).value (e) => {
updateSpanStyle(idx, 'fontWeight', v || undefined) const v = (e.target as HTMLSelectElement).value
}"> updateSpanStyle(idx, 'fontWeight', v || undefined)
}
"
>
<option value="">Varsayilan</option> <option value="">Varsayilan</option>
<option value="normal">Normal</option> <option value="normal">Normal</option>
<option value="bold">Kalin</option> <option value="bold">Kalin</option>
@@ -116,9 +144,12 @@ function removeSpan(index: number) {
</div> </div>
<div class="prop-row" data-tip="Span metin rengi"> <div class="prop-row" data-tip="Span metin rengi">
<label class="prop-label">Renk</label> <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'" :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> </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-section__title">Sekil</div>
<div class="prop-row" data-tip="Sekil tipi"> <div class="prop-row" data-tip="Sekil tipi">
<label class="prop-label">Tip</label> <label class="prop-label">Tip</label>
<select class="prop-input prop-select" <select
class="prop-input prop-select"
:value="element.shapeType" :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="rectangle">Dikdortgen</option>
<option value="rounded_rectangle">Yuvarlak Dikdortgen</option> <option value="rounded_rectangle">Yuvarlak Dikdortgen</option>
<option value="ellipse">Elips</option> <option value="ellipse">Elips</option>
@@ -34,27 +36,51 @@ function updateStyle(key: string, value: unknown) {
</div> </div>
<div class="prop-row" data-tip="Sekil arka plan rengi"> <div class="prop-row" data-tip="Sekil arka plan rengi">
<label class="prop-label">Arka Plan</label> <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'" :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>
<div class="prop-row" data-tip="Kenarlik cizgisi rengi"> <div class="prop-row" data-tip="Kenarlik cizgisi rengi">
<label class="prop-label">Kenar Rengi</label> <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'" :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>
<div class="prop-row" data-tip="Kenarlik cizgi kalinligi (mm)"> <div class="prop-row" data-tip="Kenarlik cizgi kalinligi (mm)">
<label class="prop-label">Kenar Kalinligi</label> <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" :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>
<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> <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" :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>
</div> </div>
</template> </template>

View File

@@ -16,52 +16,105 @@ function updateSize(axis: 'width' | 'height', sv: SizeValue) {
<div class="prop-section__title">Boyut</div> <div class="prop-section__title">Boyut</div>
<div class="prop-row" data-tip="Genislik boyutlandirma modu"> <div class="prop-row" data-tip="Genislik boyutlandirma modu">
<label class="prop-label">Genislik</label> <label class="prop-label">Genislik</label>
<select class="prop-input prop-select" <select
class="prop-input prop-select"
:value="element.size.width.type" :value="element.size.width.type"
@change="(e) => { @change="
const t = (e.target as HTMLSelectElement).value (e) => {
if (t === 'auto') updateSize('width', { type: 'auto' }) const t = (e.target as HTMLSelectElement).value
else if (t === 'fr') updateSize('width', { type: 'fr', value: 1 }) if (t === 'auto') updateSize('width', { type: 'auto' })
else updateSize('width', { type: 'fixed', value: 50 }) else if (t === 'fr') updateSize('width', { type: 'fr', value: 1 })
}"> else updateSize('width', { type: 'fixed', value: 50 })
}
"
>
<option value="auto">Otomatik</option> <option value="auto">Otomatik</option>
<option value="fixed">Sabit (mm)</option> <option value="fixed">Sabit (mm)</option>
<option value="fr">Oran (fr)</option> <option value="fr">Oran (fr)</option>
</select> </select>
</div> </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> <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" :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>
<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> <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" :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>
<div class="prop-row" data-tip="Yukseklik boyutlandirma modu"> <div class="prop-row" data-tip="Yukseklik boyutlandirma modu">
<label class="prop-label">Yukseklik</label> <label class="prop-label">Yukseklik</label>
<select class="prop-input prop-select" <select
class="prop-input prop-select"
:value="element.size.height.type" :value="element.size.height.type"
@change="(e) => { @change="
const t = (e.target as HTMLSelectElement).value (e) => {
if (t === 'auto') updateSize('height', { type: 'auto' }) const t = (e.target as HTMLSelectElement).value
else if (t === 'fr') updateSize('height', { type: 'fr', value: 1 }) if (t === 'auto') updateSize('height', { type: 'auto' })
else updateSize('height', { type: 'fixed', value: 20 }) else if (t === 'fr') updateSize('height', { type: 'fr', value: 1 })
}"> else updateSize('height', { type: 'fixed', value: 20 })
}
"
>
<option value="auto">Otomatik</option> <option value="auto">Otomatik</option>
<option value="fixed">Sabit (mm)</option> <option value="fixed">Sabit (mm)</option>
<option value="fr">Oran (fr)</option> <option value="fr">Oran (fr)</option>
</select> </select>
</div> </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> <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" :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>
</div> </div>
</template> </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"> <div v-if="element.type === 'static_text'" class="prop-row" data-tip="Sabit metin icerigi">
<label class="prop-label">Metin</label> <label class="prop-label">Metin</label>
<input class="prop-input" type="text" <input
class="prop-input"
type="text"
:value="(element as StaticTextElement).content" :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>
<div class="prop-row" data-tip="Yazi tipi boyutu (point)"> <div class="prop-row" data-tip="Yazi tipi boyutu (point)">
<label class="prop-label">Boyut (pt)</label> <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" :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>
<div class="prop-row" data-tip="Yazi tipi kalinligi"> <div class="prop-row" data-tip="Yazi tipi kalinligi">
<label class="prop-label">Kalinlik</label> <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'" :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="normal">Normal</option>
<option value="bold">Kalin</option> <option value="bold">Kalin</option>
</select> </select>
</div> </div>
<div class="prop-row" data-tip="Metin rengi"> <div class="prop-row" data-tip="Metin rengi">
<label class="prop-label">Renk</label> <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'" :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>
<div class="prop-row" data-tip="Metnin yatay hizalamasi"> <div class="prop-row" data-tip="Metnin yatay hizalamasi">
<label class="prop-label">Hizalama</label> <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'" :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="left">Sol</option>
<option value="center">Orta</option> <option value="center">Orta</option>
<option value="right">Sag</option> <option value="right">Sag</option>

View File

@@ -121,11 +121,20 @@ export function useLayoutEngine(
// --- Barcode üretimi (WASM üzerinden) --- // --- Barcode üretimi (WASM üzerinden) ---
let barcodeReqId = 0 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() if (!worker) initWorker()
return new Promise(resolve => { return new Promise((resolve) => {
barcodeReqId++ barcodeReqId++
const id = barcodeReqId const id = barcodeReqId
const timeout = setTimeout(() => { 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) const cb = barcodeCallbacks.get(msg.id)
if (cb) { if (cb) {
barcodeCallbacks.delete(msg.id) 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 { 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) horizontals: number[] // y positions in mm (top, bottom, center of elements + page)
} }
@@ -27,9 +27,9 @@ export function useSnapGuides() {
layoutMap: Record<string, ElementLayout>, layoutMap: Record<string, ElementLayout>,
excludeId: string, excludeId: string,
pageWidth: number, 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] const horizontals: number[] = [0, pageHeight / 2, pageHeight]
for (const [id, el] of Object.entries(layoutMap)) { for (const [id, el] of Object.entries(layoutMap)) {
@@ -48,7 +48,7 @@ export function useSnapGuides() {
proposedX_mm: number, proposedX_mm: number,
proposedY_mm: number, proposedY_mm: number,
width_mm: number, width_mm: number,
height_mm: number height_mm: number,
): SnapResult { ): SnapResult {
if (!cachedEdges) { if (!cachedEdges) {
return { snappedX_mm: proposedX_mm, snappedY_mm: proposedY_mm, guides: [] } return { snappedX_mm: proposedX_mm, snappedY_mm: proposedY_mm, guides: [] }
@@ -132,13 +132,12 @@ export function useSnapGuides() {
/** Calculate snap for resize edge */ /** Calculate snap for resize edge */
function calculateResizeSnap( function calculateResizeSnap(
edge: 'left' | 'right' | 'top' | 'bottom', edge: 'left' | 'right' | 'top' | 'bottom',
proposedValue_mm: number proposedValue_mm: number,
): number { ): number {
if (!cachedEdges) return proposedValue_mm if (!cachedEdges) return proposedValue_mm
const targets = (edge === 'left' || edge === 'right') const targets =
? cachedEdges.verticals edge === 'left' || edge === 'right' ? cachedEdges.verticals : cachedEdges.horizontals
: cachedEdges.horizontals
const guides: SnapGuide[] = [] const guides: SnapGuide[] = []
let snapped = proposedValue_mm let snapped = proposedValue_mm
@@ -154,7 +153,7 @@ export function useSnapGuides() {
if (snapped !== proposedValue_mm) { if (snapped !== proposedValue_mm) {
guides.push({ guides.push({
type: (edge === 'left' || edge === 'right') ? 'vertical' : 'horizontal', type: edge === 'left' || edge === 'right' ? 'vertical' : 'horizontal',
position_mm: snapped, position_mm: snapped,
}) })
} }

View File

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

View File

@@ -148,7 +148,7 @@ describe('findScalarFields', () => {
// firma.unvan, firma.vergiNo, fatura.no, fatura.tutar, fatura.tarih = 5 // firma.unvan, firma.vergiNo, fatura.no, fatura.tutar, fatura.tarih = 5
expect(scalars).toHaveLength(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.unvan')
expect(paths).toContain('firma.vergiNo') expect(paths).toContain('firma.vergiNo')
expect(paths).toContain('fatura.no') expect(paths).toContain('fatura.no')
@@ -159,7 +159,7 @@ describe('findScalarFields', () => {
it('does not include object or array nodes', () => { it('does not include object or array nodes', () => {
const tree = parseSchema(testSchema) const tree = parseSchema(testSchema)
const scalars = findScalarFields(tree) 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('object')
expect(types).not.toContain('array') expect(types).not.toContain('array')
@@ -195,17 +195,38 @@ describe('defaultAlignForSchema', () => {
}) })
it('returns right for currency format', () => { 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') expect(defaultAlignForSchema(node)).toBe('right')
}) })
it('returns right for percentage format', () => { 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') expect(defaultAlignForSchema(node)).toBe('right')
}) })
it('returns center for date format', () => { 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') expect(defaultAlignForSchema(node)).toBe('center')
}) })

View File

@@ -57,10 +57,13 @@ function mockColumnValue(field: string, format: string | undefined, index: numbe
const lower = field.toLowerCase() const lower = field.toLowerCase()
if (lower.includes('sira') || lower.includes('no') || lower === 'id') return index + 1 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('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('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('tarih') || lower.includes('date'))
if (lower.includes('ad') || lower.includes('isim') || lower.includes('name')) return ['Kalem A', 'Kalem B', 'Kalem C'][index % 3] 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}` return `Ornek ${index + 1}`
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,13 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia' import { setActivePinia, createPinia } from 'pinia'
import { useTemplateStore } from '../template' import { useTemplateStore } from '../template'
import { useEditorStore } from '../editor' 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' import { sz } from '../../core/types'
function createTestTemplate(): Template { function createTestTemplate(): Template {
@@ -392,52 +398,52 @@ describe('3.2 Z-Order controls', () => {
const store = setupThreeElements() const store = setupThreeElements()
// Sıra: [a, b, c] → bringForward(a) → [b, a, c] // Sıra: [a, b, c] → bringForward(a) → [b, a, c]
store.bringForward('a') 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', () => { it('sendBackward moves element one step down', () => {
const store = setupThreeElements() const store = setupThreeElements()
// Sıra: [a, b, c] → sendBackward(c) → [a, c, b] // Sıra: [a, b, c] → sendBackward(c) → [a, c, b]
store.sendBackward('c') 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', () => { it('bringToFront moves element to end', () => {
const store = setupThreeElements() const store = setupThreeElements()
// Sıra: [a, b, c] → bringToFront(a) → [b, c, a] // Sıra: [a, b, c] → bringToFront(a) → [b, c, a]
store.bringToFront('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', () => { it('sendToBack moves element to beginning', () => {
const store = setupThreeElements() const store = setupThreeElements()
// Sıra: [a, b, c] → sendToBack(c) → [c, a, b] // Sıra: [a, b, c] → sendToBack(c) → [c, a, b]
store.sendToBack('c') 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', () => { it('bringForward on last element is no-op', () => {
const store = setupThreeElements() const store = setupThreeElements()
store.bringForward('c') 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', () => { it('sendBackward on first element is no-op', () => {
const store = setupThreeElements() const store = setupThreeElements()
store.sendBackward('a') 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', () => { it('bringToFront on last element is no-op', () => {
const store = setupThreeElements() const store = setupThreeElements()
store.bringToFront('c') 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', () => { it('sendToBack on first element is no-op', () => {
const store = setupThreeElements() const store = setupThreeElements()
store.sendToBack('a') 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('b', 'B'))
store.addChild('root', createTextElement('c', 'C'), 1) 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', () => { it('removeElement removes element', () => {
@@ -133,7 +133,7 @@ describe('useTemplateStore', () => {
store.reorderChild('root', 0, 2) 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', () => { it('exportTemplate returns valid JSON', () => {

View File

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

View File

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

View File

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