This commit is contained in:
2026-04-05 15:10:29 +03:00
parent 7684a2a871
commit c346c604fe
37 changed files with 2460 additions and 625 deletions

View File

@@ -0,0 +1,237 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch, computed } from 'vue'
import { EditorView, lineNumbers } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import { dexpr } from 'codemirror-lang-dexpr'
import type { DexprLanguageInfo } from 'codemirror-lang-dexpr'
import { useSchemaStore } from '../../stores/schema'
import type { SchemaNode } from '../../core/schema-parser'
const props = defineProps<{
modelValue: string
placeholder?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const editorEl = ref<HTMLDivElement>()
let view: EditorView | null = null
let debounceTimer: ReturnType<typeof setTimeout> | null = null
function emitDebounced(val: string) {
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
emit('update:modelValue', val)
}, 300)
}
const schemaStore = useSchemaStore()
/** Schema tree'den dexpr LanguageInfo formatina donustur */
function schemaToLanguageInfo(): DexprLanguageInfo {
const info: DexprLanguageInfo = {
functions: [
{ name: 'log', signature: '(...args) -> Null', doc: 'Deger yazdir' },
{ name: 'rand', signature: '(min: Number, max: Number) -> Number', doc: 'Rastgele sayi' },
],
methods: {
String: [
{ name: 'upper', signature: '() -> String' },
{ name: 'lower', signature: '() -> String' },
{ name: 'trim', signature: '() -> String' },
{ name: 'length', signature: '() -> Number' },
{ name: 'contains', signature: '(substr: String) -> Boolean' },
{ name: 'replace', signature: '(old: String, new: String) -> String' },
{ name: 'split', signature: '(delim: String) -> StringList' },
{ name: 'substring', signature: '(start: Number, end?: Number) -> String' },
{ name: 'startsWith', signature: '(prefix: String) -> Boolean' },
{ name: 'endsWith', signature: '(suffix: String) -> Boolean' },
{ name: 'charAt', signature: '(index: Number) -> String' },
{ name: 'trimStart', signature: '() -> String' },
{ name: 'trimEnd', signature: '() -> String' },
],
Number: [],
Boolean: [],
NumberList: [
{ name: 'length', signature: '() -> Number' },
{ name: 'sum', signature: '() -> Number' },
{ name: 'avg', signature: '() -> Number' },
{ name: 'min', signature: '() -> Number' },
{ name: 'max', signature: '() -> Number' },
{ name: 'first', signature: '() -> Number' },
{ name: 'last', signature: '() -> Number' },
{ name: 'sort', signature: '() -> NumberList' },
{ name: 'reverse', signature: '() -> NumberList' },
{ name: 'contains', signature: '(value: Number) -> Boolean' },
],
StringList: [
{ name: 'length', signature: '() -> Number' },
{ name: 'join', signature: '(delim?: String) -> String' },
{ name: 'first', signature: '() -> String' },
{ name: 'last', signature: '() -> String' },
{ name: 'sort', signature: '() -> StringList' },
{ name: 'reverse', signature: '() -> StringList' },
{ name: 'contains', signature: '(value: String) -> Boolean' },
],
Object: [
{ name: 'keys', signature: '() -> StringList' },
{ name: 'values', signature: '() -> StringList | NumberList' },
{ name: 'length', signature: '() -> Number' },
{ name: 'contains', signature: '(key: String) -> Boolean' },
{ name: 'get', signature: '(key: String) -> any' },
],
},
variables: [],
}
// Schema tree'deki top-level object property'lerinden dexpr degiskenleri olustur
const tree = schemaStore.schemaTree
for (const child of tree.children) {
if (child.type === 'object') {
const fields = child.children.map(f => ({
name: f.key,
type: schemaToDexprType(f),
}))
info.variables!.push({
name: child.key,
type: 'Object',
doc: child.title,
fields,
})
} else {
info.variables!.push({
name: child.key,
type: schemaToDexprType(child),
doc: child.title,
})
}
}
return info
}
function schemaToDexprType(node: SchemaNode): 'String' | 'Number' | 'Boolean' | 'Object' | 'NumberList' | 'StringList' {
switch (node.type) {
case 'number':
case 'integer':
return 'Number'
case 'boolean':
return 'Boolean'
case 'object':
return 'Object'
case 'array':
return 'StringList'
default:
return 'String'
}
}
const langInfo = computed(() => schemaToLanguageInfo())
function createState(doc: string): EditorState {
return EditorState.create({
doc,
extensions: [
EditorView.updateListener.of(update => {
if (update.docChanged) {
const val = update.state.doc.toString()
if (val !== props.modelValue) {
emitDebounced(val)
}
}
}),
lineNumbers(),
dexpr(langInfo.value),
EditorView.lineWrapping,
EditorView.theme({
'&': {
fontSize: '11px',
border: '1px solid #e2e8f0',
borderRadius: '4px',
backgroundColor: '#fff',
maxHeight: '120px',
},
'&.cm-focused': {
outline: '2px solid #93c5fd',
outlineOffset: '-1px',
},
'.cm-scroller': {
overflow: 'auto',
},
'.cm-content': {
padding: '4px 6px',
fontFamily: '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace',
minHeight: '20px',
},
'.cm-line': {
padding: '0',
},
'.cm-gutters': {
backgroundColor: '#f8fafc',
borderRight: '1px solid #e2e8f0',
color: '#94a3b8',
fontSize: '10px',
minWidth: '20px',
paddingLeft: '2px',
paddingRight: '4px',
},
'.cm-activeLine': {
backgroundColor: 'transparent',
},
'.cm-tooltip.cm-tooltip-autocomplete': {
fontSize: '11px',
zIndex: '9999',
},
}),
EditorState.tabSize.of(2),
EditorView.contentAttributes.of({
'aria-label': 'dexpr expression editor',
}),
],
})
}
onMounted(() => {
if (!editorEl.value) return
view = new EditorView({
state: createState(props.modelValue ?? ''),
parent: editorEl.value,
})
})
onBeforeUnmount(() => {
view?.destroy()
view = null
})
// Disaridan gelen deger degisikligi (undo/redo vs.)
watch(() => props.modelValue, (newVal) => {
if (!view) return
const current = view.state.doc.toString()
if (current !== newVal) {
view.dispatch({
changes: { from: 0, to: current.length, insert: newVal ?? '' },
})
}
})
// Schema degisince editor'u yeniden olustur (autocomplete guncellenmeli)
watch(langInfo, () => {
if (!view) return
const doc = view.state.doc.toString()
view.setState(createState(doc))
}, { deep: true })
</script>
<template>
<div ref="editorEl" class="dexpr-editor" />
</template>
<style scoped>
.dexpr-editor {
width: 100%;
min-width: 0;
}
</style>

View File

@@ -168,7 +168,7 @@ function applyZoom(delta: number, clientX: number, clientY: number) {
}
function onKeyDown(e: KeyboardEvent) {
if (e.code === 'Space' && !e.repeat && !(e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement || e.target instanceof HTMLTextAreaElement)) {
if (e.code === 'Space' && !e.repeat && !(e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement || e.target instanceof HTMLTextAreaElement || (e.target as HTMLElement)?.isContentEditable)) {
e.preventDefault()
spaceHeld.value = true
}

View File

@@ -3,7 +3,7 @@ import { computed } from 'vue'
import { useTemplateStore } from '../../stores/template'
import { useEditorStore } from '../../stores/editor'
import { isContainer } from '../../core/types'
import type { ContainerElement, TextStyle } from '../../core/types'
import type { ContainerElement, TextStyle, RepeatingTableElement, TableStyle } from '../../core/types'
import type { ElementLayout } from '../../core/layout-types'
const props = defineProps<{
@@ -32,6 +32,10 @@ const isText = computed(() => {
const isLine = computed(() => selected.value?.type === 'line')
const isTable = computed(() => selected.value?.type === 'repeating_table')
const tableEl = computed(() => isTable.value ? selected.value as RepeatingTableElement : null)
const tableStyle = computed(() => tableEl.value?.style as TableStyle | undefined)
const toolbarStyle = computed(() => {
const el = selected.value
if (!el) return { display: 'none' }
@@ -66,6 +70,12 @@ function setGap(e: Event) { update({ gap: parseFloat((e.target as HTMLInputEleme
// Text
function setFontWeight(w: string) { updateStyle('fontWeight', w) }
function setTextAlign(a: string) { updateStyle('align', a) }
// Table
function updateTableStyle(key: string, value: unknown) {
if (!selected.value) return
update({ style: { ...selected.value.style, [key]: value } })
}
</script>
<template>
@@ -230,6 +240,66 @@ function setTextAlign(a: string) { updateStyle('align', a) }
</div>
</template>
<!-- ===== Repeating Table ===== -->
<template v-if="isTable && tableStyle">
<!-- Font size -->
<div class="et__group et__group--gap" data-tip="Yazi Boyutu">
<svg class="et__gap-icon" width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2 10L6 2l4 8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<line x1="3.5" y1="7" x2="8.5" y2="7" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
</svg>
<input type="number" class="et__num" step="1" min="6" :value="tableStyle.fontSize ?? 10" @input="(e) => updateTableStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)" />
</div>
<div class="et__sep" />
<!-- Header bg color -->
<div class="et__group">
<label class="et__color-wrap" data-tip="Header Rengi">
<input type="color" class="et__color" :value="tableStyle.headerBg ?? '#f0f0f0'" @input="(e) => updateTableStyle('headerBg', (e.target as HTMLInputElement).value)" />
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<rect x="2" y="2" width="10" height="4" rx="1" :fill="tableStyle.headerBg ?? '#f0f0f0'" stroke="#94a3b8" stroke-width="0.5"/>
<rect x="2" y="7" width="10" height="2" rx="0.5" fill="none" stroke="#94a3b8" stroke-width="0.5"/>
<rect x="2" y="10" width="10" height="2" rx="0.5" fill="none" stroke="#94a3b8" stroke-width="0.5"/>
</svg>
</label>
</div>
<!-- Zebra color -->
<div class="et__group">
<label class="et__color-wrap" data-tip="Zebra Rengi">
<input type="color" class="et__color" :value="tableStyle.zebraOdd ?? '#fafafa'" @input="(e) => updateTableStyle('zebraOdd', (e.target as HTMLInputElement).value)" />
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<rect x="2" y="2" width="10" height="2.5" rx="0.5" fill="none" stroke="#94a3b8" stroke-width="0.5"/>
<rect x="2" y="5.5" width="10" height="2.5" rx="0.5" :fill="tableStyle.zebraOdd ?? '#fafafa'" stroke="#94a3b8" stroke-width="0.5"/>
<rect x="2" y="9" width="10" height="2.5" rx="0.5" fill="none" stroke="#94a3b8" stroke-width="0.5"/>
</svg>
</label>
</div>
<div class="et__sep" />
<!-- Border color -->
<div class="et__group">
<label class="et__color-wrap" data-tip="Kenarlik Rengi">
<input type="color" class="et__color" :value="tableStyle.borderColor ?? '#cccccc'" @input="(e) => updateTableStyle('borderColor', (e.target as HTMLInputElement).value)" />
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<rect x="2" y="2" width="10" height="10" rx="1" fill="none" :stroke="tableStyle.borderColor ?? '#cccccc'" stroke-width="1.5"/>
<line x1="2" y1="6" x2="12" y2="6" :stroke="tableStyle.borderColor ?? '#cccccc'" stroke-width="0.8"/>
<line x1="7" y1="2" x2="7" y2="12" :stroke="tableStyle.borderColor ?? '#cccccc'" stroke-width="0.8"/>
</svg>
</label>
</div>
<!-- Border width -->
<div class="et__group et__group--gap" data-tip="Kenarlik (mm)">
<svg class="et__gap-icon" width="12" height="12" viewBox="0 0 12 12" fill="none">
<rect x="1" y="1" width="10" height="10" rx="1" fill="none" stroke="currentColor" stroke-width="1.5"/>
</svg>
<input type="number" class="et__num" step="0.1" min="0" :value="tableStyle.borderWidth ?? 0.5" @input="(e) => updateTableStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)" />
</div>
</template>
<!-- ===== Line ===== -->
<template v-if="isLine">
<!-- Stroke width -->
@@ -282,34 +352,6 @@ function setTextAlign(a: string) { updateStyle('align', a) }
flex-shrink: 0;
}
/* Tooltip */
[data-tip] {
position: relative;
}
[data-tip]::after {
content: attr(data-tip);
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
background: #0f172a;
color: #e2e8f0;
font-size: 10px;
padding: 3px 6px;
border-radius: 4px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
z-index: 10;
}
[data-tip]:hover::after,
[data-tip]:focus-within::after {
opacity: 1;
}
/* Button */
.et__btn {
display: flex;

View File

@@ -124,7 +124,7 @@ function findDeepestContainer(mouseX: number, mouseY: number, excludeId?: string
if (!l) continue
const cx = l.x_mm * s
const cy = l.y_mm * s
const cy = l.y_mm * s + pageYOffset(l.pageIndex)
const cw = l.width_mm * s
const ch = l.height_mm * s
@@ -154,7 +154,7 @@ function computeDropIndex(container: ContainerElement, mouseX: number, mouseY: n
const centerX = l.x_mm * s + (l.width_mm * s) / 2
if (mouseX < centerX) { visualIdx = i; break }
} else {
const centerY = l.y_mm * s + (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 }
}
}
@@ -246,7 +246,8 @@ const dropIndicatorStyle = computed(() => {
}
}
const top = cl.y_mm * s
const clPageOff = pageYOffset(cl.pageIndex)
const top = cl.y_mm * s + clPageOff
const height = cl.height_mm * s
return {
@@ -263,30 +264,31 @@ const dropIndicatorStyle = computed(() => {
}
// Column container: yatay gösterge çizgisi
const colPageOff = pageYOffset(cl.pageIndex)
let y = 0
if (idx === 0 && flowChildren.length > 0) {
const l = props.layoutMap[flowChildren[0].id]
if (l) {
y = (cl.y_mm * s + l.y_mm * s) / 2
y = (cl.y_mm * s + colPageOff + l.y_mm * s + pageYOffset(l.pageIndex)) / 2
} else {
y = cl.y_mm * s - 4
y = cl.y_mm * s + colPageOff - 4
}
} else if (idx < flowChildren.length && idx > 0) {
const above = props.layoutMap[flowChildren[idx - 1].id]
const below = props.layoutMap[flowChildren[idx].id]
if (above && below) {
const aboveBottom = (above.y_mm + above.height_mm) * s
const belowTop = below.y_mm * s
const aboveBottom = (above.y_mm + above.height_mm) * s + pageYOffset(above.pageIndex)
const belowTop = below.y_mm * s + pageYOffset(below.pageIndex)
y = (aboveBottom + belowTop) / 2
}
} else if (idx === 0 && flowChildren.length === 0) {
y = cl.y_mm * s + 8
y = cl.y_mm * s + colPageOff + 8
} else if (flowChildren.length > 0) {
const last = flowChildren[flowChildren.length - 1]
const l = props.layoutMap[last.id]
if (l) {
const gapPx = container.gap * props.scale
y = (l.y_mm + l.height_mm) * s + gapPx / 2
y = (l.y_mm + l.height_mm) * s + pageYOffset(l.pageIndex) + gapPx / 2
}
}

View File

@@ -205,6 +205,9 @@ const tools: ToolItem[] = [
create: (): PageBreakElement => ({
id: nextId('pb'),
type: 'page_break',
position: { type: 'flow' },
size: { width: sz.fr(1), height: sz.auto() },
style: {},
}),
},
]

View File

@@ -94,7 +94,7 @@ function onBarcodeFormatChange(newFormat: BarcodeFormat) {
<template>
<div class="prop-section">
<div class="prop-section__title">Barkod Ayarlari</div>
<div class="prop-row">
<div class="prop-row" data-tip="Barkod formati">
<label class="prop-label">Format</label>
<select class="prop-input prop-select"
:value="element.format"
@@ -106,14 +106,14 @@ function onBarcodeFormatChange(newFormat: BarcodeFormat) {
<option value="code39">Code 39</option>
</select>
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Barkod icerigi formata uygun olmali">
<label class="prop-label">Deger</label>
<input class="prop-input" type="text"
:class="{ 'prop-input--invalid': barcodeInputInvalid }"
:value="barcodeInputValue"
@input="onBarcodeValueInput" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Barkod cizgi/modül rengi">
<label class="prop-label">Renk</label>
<div class="prop-row-inline">
<input class="prop-input prop-color" type="color"
@@ -122,13 +122,13 @@ function onBarcodeFormatChange(newFormat: BarcodeFormat) {
<button v-if="element.style.color" class="prop-clear" @click="updateStyle('color', undefined)">x</button>
</div>
</div>
<div v-if="element.format !== 'qr'" class="prop-row">
<div v-if="element.format !== 'qr'" class="prop-row" data-tip="Barkod altinda degeri metin olarak goster">
<label class="prop-label">Metin Goster</label>
<input type="checkbox"
:checked="element.style.includeText ?? (element.format === 'ean13' || element.format === 'ean8')"
@change="(e) => updateStyle('includeText', (e.target as HTMLInputElement).checked)" />
</div>
<div v-if="schemaStore.scalarFields.length > 0" class="prop-row">
<div v-if="schemaStore.scalarFields.length > 0" class="prop-row" data-tip="Schema'dan dinamik veri baglama">
<label class="prop-label">Veri Baglama</label>
<select class="prop-input prop-select"
:value="element.binding?.path ?? ''"

View File

@@ -2,6 +2,7 @@
import { useTemplateStore } from '../../stores/template'
import { useEditorStore } from '../../stores/editor'
import type { CalculatedTextElement, TextStyle, TemplateElement } from '../../core/types'
import DexprEditor from '../common/DexprEditor.vue'
import '../../styles/properties.css'
const props = defineProps<{ element: CalculatedTextElement }>()
@@ -17,19 +18,23 @@ function update(updates: Partial<TemplateElement>) {
function updateStyle(key: string, value: unknown) {
update({ style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
}
function onExpressionChange(value: string) {
update({ expression: value } as any)
}
</script>
<template>
<div class="prop-section">
<div class="prop-section__title">Hesaplanan Metin</div>
<div class="prop-row">
<div class="prop-row-stack" data-tip="Hesaplama ifadesi (orn: toplamlar.kdv + toplamlar.araToplam)">
<label class="prop-label">Ifade</label>
<input class="prop-input" type="text"
:value="element.expression"
@change="(e) => update({ expression: (e.target as HTMLInputElement).value } as any)"
<DexprEditor
:model-value="element.expression"
@update:model-value="onExpressionChange"
placeholder="toplamlar.kdv + toplamlar.araToplam" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Sonucun gosterim formati">
<label class="prop-label">Format</label>
<select class="prop-input prop-select"
:value="element.format ?? ''"
@@ -40,19 +45,19 @@ function updateStyle(key: string, value: unknown) {
<option value="percentage">Yuzde</option>
</select>
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Yazi tipi boyutu (point)">
<label class="prop-label">Boyut (pt)</label>
<input class="prop-input" type="number" step="1" min="1"
:value="(element.style as TextStyle).fontSize ?? 11"
@input="(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 11)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Metin rengi">
<label class="prop-label">Renk</label>
<input class="prop-input prop-color" type="color"
:value="(element.style as TextStyle).color ?? '#000000'"
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Yazi tipi kalinligi">
<label class="prop-label">Kalinlik</label>
<select class="prop-input prop-select"
:value="(element.style as TextStyle).fontWeight ?? 'normal'"
@@ -61,7 +66,7 @@ function updateStyle(key: string, value: unknown) {
<option value="bold">Kalin</option>
</select>
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Metnin yatay hizalamasi">
<label class="prop-label">Hizalama</label>
<select class="prop-input prop-select"
:value="(element.style as TextStyle).align ?? 'left'"

View File

@@ -22,25 +22,25 @@ function updateStyle(key: string, value: unknown) {
<template>
<div class="prop-section">
<div class="prop-section__title">Onay Kutusu</div>
<div v-if="!element.binding" class="prop-row">
<div v-if="!element.binding" class="prop-row" data-tip="Onay kutusunun varsayilan durumu">
<label class="prop-label">Isaretli</label>
<input type="checkbox"
:checked="element.checked ?? false"
@change="(e) => update({ checked: (e.target as HTMLInputElement).checked } as any)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Onay kutusu boyutu (mm)">
<label class="prop-label">Boyut (mm)</label>
<input class="prop-input" type="number" step="0.5" min="1"
:value="element.style.size ?? 4"
@input="(e) => updateStyle('size', parseFloat((e.target as HTMLInputElement).value) || 4)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Isaret (tik) rengi">
<label class="prop-label">Isaret Rengi</label>
<input class="prop-input prop-color" type="color"
:value="element.style.checkColor ?? '#000000'"
@input="(e) => updateStyle('checkColor', (e.target as HTMLInputElement).value)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Kutu kenarlik rengi">
<label class="prop-label">Kenar Rengi</label>
<input class="prop-input prop-color" type="color"
:value="element.style.borderColor ?? '#333333'"

View File

@@ -23,7 +23,7 @@ function updateStyle(key: string, value: unknown) {
<template>
<div class="prop-section">
<div class="prop-section__title">Container Ayarlari</div>
<div class="prop-row">
<div class="prop-row" data-tip="Cocuk elemanlarin dizilim yonu">
<label class="prop-label">Yon</label>
<select class="prop-input prop-select"
:value="element.direction"
@@ -32,13 +32,13 @@ function updateStyle(key: string, value: unknown) {
<option value="row">Yatay</option>
</select>
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Cocuk elemanlar arasi bosluk (mm)">
<label class="prop-label">Bosluk (mm)</label>
<input class="prop-input" type="number" step="1" min="0"
:value="element.gap"
@input="(e) => update({ gap: parseFloat((e.target as HTMLInputElement).value) || 0 } as any)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Cocuklarin cross-axis hizalamasi">
<label class="prop-label">{{ element.direction === 'column' ? 'Yatay Hizalama' : 'Dikey Hizalama' }}</label>
<select class="prop-input prop-select"
:value="element.align"
@@ -49,7 +49,7 @@ function updateStyle(key: string, value: unknown) {
<option value="stretch">Esnet</option>
</select>
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Cocuklarin main-axis dagilimi">
<label class="prop-label">{{ element.direction === 'column' ? 'Dikey Dagilim' : 'Yatay Dagilim' }}</label>
<select class="prop-input prop-select"
:value="element.justify"
@@ -70,7 +70,7 @@ function updateStyle(key: string, value: unknown) {
@update="(side, value) => update({ padding: { ...element.padding, [side]: value } } as any)"
/>
<div class="prop-row">
<div class="prop-row" data-tip="Sayfa sonunda bolunmeyi kontrol eder">
<label class="prop-label">Sayfa Bolme</label>
<select class="prop-input prop-select"
:value="element.breakInside ?? 'auto'"
@@ -81,7 +81,7 @@ function updateStyle(key: string, value: unknown) {
</div>
<div class="prop-section__subtitle">Stil</div>
<div class="prop-row">
<div class="prop-row" data-tip="Container arka plan rengi">
<label class="prop-label">Arka plan</label>
<div class="prop-row-inline">
<input class="prop-input prop-color" type="color"
@@ -90,13 +90,13 @@ function updateStyle(key: string, value: unknown) {
<button v-if="element.style.backgroundColor" class="prop-clear" @click="updateStyle('backgroundColor', undefined)">x</button>
</div>
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Kenarlik kalinligi (mm)">
<label class="prop-label">Kenarlik (mm)</label>
<input class="prop-input" type="number" step="0.1" min="0"
:value="element.style.borderWidth ?? 0"
@input="(e) => updateStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Kenarlik cizgisi rengi">
<label class="prop-label">Kenarlik rengi</label>
<div class="prop-row-inline">
<input class="prop-input prop-color" type="color"
@@ -105,7 +105,7 @@ function updateStyle(key: string, value: unknown) {
<button v-if="element.style.borderColor" class="prop-clear" @click="updateStyle('borderColor', undefined)">x</button>
</div>
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Kenarlik cizgi stili">
<label class="prop-label">Kenarlik stili</label>
<select class="prop-input prop-select"
:value="element.style.borderStyle ?? 'solid'"
@@ -115,7 +115,7 @@ function updateStyle(key: string, value: unknown) {
<option value="dotted">Noktali</option>
</select>
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Kose yuvarlakligi (mm)">
<label class="prop-label">Radius (mm)</label>
<input class="prop-input" type="number" step="0.5" min="0"
:value="element.style.borderRadius ?? 0"

View File

@@ -22,7 +22,7 @@ function updateStyle(key: string, value: unknown) {
<template>
<div class="prop-section">
<div class="prop-section__title">Tarih</div>
<div class="prop-row">
<div class="prop-row" data-tip="Tarih gosterim formati">
<label class="prop-label">Format</label>
<select class="prop-input prop-select"
:value="element.format ?? 'DD.MM.YYYY'"
@@ -33,19 +33,19 @@ function updateStyle(key: string, value: unknown) {
<option value="DD.MM.YYYY HH:mm">30.03.2026 14:30</option>
</select>
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Yazi tipi boyutu (point)">
<label class="prop-label">Boyut (pt)</label>
<input class="prop-input" type="number" step="1" min="1"
:value="(element.style as TextStyle).fontSize ?? 10"
@input="(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Metin rengi">
<label class="prop-label">Renk</label>
<input class="prop-input prop-color" type="color"
:value="(element.style as TextStyle).color ?? '#666666'"
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Metnin yatay hizalamasi">
<label class="prop-label">Hizalama</label>
<select class="prop-input prop-select"
:value="(element.style as TextStyle).align ?? 'left'"

View File

@@ -33,22 +33,22 @@ function onImageFileSelect(e: Event) {
<template>
<div class="prop-section">
<div class="prop-section__title">Gorsel</div>
<div class="prop-row">
<div class="prop-row" data-tip="Gorsel dosyasi secin (PNG, JPG, SVG)">
<label class="prop-label">Kaynak</label>
<label class="prop-file-btn">
Dosya Sec
<input type="file" accept="image/*" style="display: none" @change="onImageFileSelect" />
</label>
</div>
<div v-if="element.src" class="prop-row">
<div v-if="element.src" class="prop-row" data-tip="Yuklenen gorsel onizlemesi">
<label class="prop-label">Onizleme</label>
<img :src="element.src" class="prop-image-preview" />
</div>
<div v-if="element.src" class="prop-row">
<div v-if="element.src" class="prop-row" data-tip="Gorseli kaldirmak icin tiklayin">
<label class="prop-label"></label>
<button class="prop-clear" @click="update({ src: undefined } as any)">Gorseli kaldir</button>
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Gorselin alana sigdirma modu">
<label class="prop-label">Sigdirma</label>
<select class="prop-input prop-select"
:value="element.style.objectFit ?? 'contain'"

View File

@@ -18,13 +18,13 @@ function updateStyle(key: string, value: unknown) {
<template>
<div class="prop-section">
<div class="prop-section__title">Cizgi Stili</div>
<div class="prop-row">
<div class="prop-row" data-tip="Cizgi kalinligi (mm)">
<label class="prop-label">Kalinlik (mm)</label>
<input class="prop-input" type="number" step="0.1" min="0.1"
:value="element.style.strokeWidth ?? 0.5"
@input="(e) => updateStyle('strokeWidth', parseFloat((e.target as HTMLInputElement).value) || 0.5)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Cizgi rengi">
<label class="prop-label">Renk</label>
<input class="prop-input prop-color" type="color"
:value="element.style.strokeColor ?? '#000000'"

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import '../../styles/properties.css'
const props = defineProps<{
top: number
right: number
@@ -20,10 +22,10 @@ function onInput(side: 'top' | 'right' | 'bottom' | 'left', e: Event) {
<div class="pb">
<span class="pb__label">Padding</span>
<div class="pb__box">
<input class="pb__in pb__in--t" type="number" step="1" min="0" :value="props.top" @input="(e) => onInput('top', e)" />
<input class="pb__in pb__in--r" type="number" step="1" min="0" :value="props.right" @input="(e) => onInput('right', e)" />
<input class="pb__in pb__in--b" type="number" step="1" min="0" :value="props.bottom" @input="(e) => onInput('bottom', e)" />
<input class="pb__in pb__in--l" type="number" step="1" min="0" :value="props.left" @input="(e) => onInput('left', e)" />
<input class="pb__in pb__in--t" type="number" step="1" min="0" :value="props.top" @input="(e) => onInput('top', e)" data-tip="Ust bosluk (mm)" />
<input class="pb__in pb__in--r" type="number" step="1" min="0" :value="props.right" @input="(e) => onInput('right', e)" data-tip="Sag bosluk (mm)" />
<input class="pb__in pb__in--b" type="number" step="1" min="0" :value="props.bottom" @input="(e) => onInput('bottom', e)" data-tip="Alt bosluk (mm)" />
<input class="pb__in pb__in--l" type="number" step="1" min="0" :value="props.left" @input="(e) => onInput('left', e)" data-tip="Sol bosluk (mm)" />
<div class="pb__center" />
</div>
</div>

View File

@@ -22,7 +22,7 @@ function updateStyle(key: string, value: unknown) {
<template>
<div class="prop-section">
<div class="prop-section__title">Sayfa Numarasi</div>
<div class="prop-row">
<div class="prop-row" data-tip="Sayfa numarasi gosterim formati">
<label class="prop-label">Format</label>
<select class="prop-input prop-select"
:value="element.format ?? '{current} / {total}'"
@@ -33,19 +33,19 @@ function updateStyle(key: string, value: unknown) {
<option value="Sayfa {current} / {total}">Sayfa 1 / 5</option>
</select>
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Yazi tipi boyutu (point)">
<label class="prop-label">Boyut (pt)</label>
<input class="prop-input" type="number" step="1" min="1"
:value="(element.style as TextStyle).fontSize ?? 10"
@input="(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Metin rengi">
<label class="prop-label">Renk</label>
<input class="prop-input prop-color" type="color"
:value="(element.style as TextStyle).color ?? '#666666'"
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Metnin yatay hizalamasi">
<label class="prop-label">Hizalama</label>
<select class="prop-input prop-select"
:value="(element.style as TextStyle).align ?? 'center'"

View File

@@ -18,7 +18,7 @@ function togglePositioning() {
<template>
<div class="prop-section">
<div class="prop-section__title">Pozisyon</div>
<div class="prop-row">
<div class="prop-row" data-tip="Flow: otomatik dizilim, Absolute: sabit konum">
<label class="prop-label">Mod</label>
<select class="prop-input prop-select" :value="element.position.type" @change="togglePositioning">
<option value="flow">Flow</option>
@@ -26,13 +26,13 @@ function togglePositioning() {
</select>
</div>
<template v-if="element.position.type === 'absolute'">
<div class="prop-row">
<div class="prop-row" data-tip="Yatay pozisyon parent sol kenardan uzaklik (mm)">
<label class="prop-label">X (mm)</label>
<input class="prop-input" type="number" step="0.5"
:value="element.position.x"
@input="(e) => templateStore.updateElementPosition(element.id, { type: 'absolute', x: parseFloat((e.target as HTMLInputElement).value) || 0, y: (element.position as any).y ?? 0 })" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Dikey pozisyon parent ust kenardan uzaklik (mm)">
<label class="prop-label">Y (mm)</label>
<input class="prop-input" type="number" step="0.5"
:value="element.position.y"

View File

@@ -88,7 +88,7 @@ const tableItemFields = computed(() => {
<!-- Data source -->
<div class="prop-section">
<div class="prop-section__title">Veri Kaynagi</div>
<div class="prop-row">
<div class="prop-row" data-tip="Tablonun baglanacagi array veri kaynagi">
<label class="prop-label">Kaynak</label>
<select class="prop-input prop-select"
:value="element.dataSource.path"
@@ -112,24 +112,31 @@ const tableItemFields = computed(() => {
<div
v-for="col in element.columns"
:key="col.id"
class="prop-column-card"
class="tbl-col"
>
<div class="prop-column-header">
<span class="prop-column-title">{{ col.title || col.field }}</span>
<div class="prop-column-actions">
<button class="prop-icon-btn" @click="moveColumn(col.id, -1)" title="Yukari">&#8593;</button>
<button class="prop-icon-btn" @click="moveColumn(col.id, 1)" title="Asagi">&#8595;</button>
<button class="prop-icon-btn prop-icon-btn--danger" @click="removeColumn(col.id)" title="Sil">x</button>
<!-- Row 1: title + actions -->
<div class="tbl-col__head">
<input class="tbl-col__title" type="text" :value="col.title"
@change="(e) => updateColumn(col.id, { title: (e.target as HTMLInputElement).value })"
:placeholder="col.field"
data-tip="Sutun basligi" />
<div class="tbl-col__actions">
<button class="tbl-col__act" @click="moveColumn(col.id, -1)" data-tip="Yukari tasi">
<svg width="10" height="10" viewBox="0 0 10 10"><path d="M5 2L2 6h6L5 2z" fill="currentColor"/></svg>
</button>
<button class="tbl-col__act" @click="moveColumn(col.id, 1)" data-tip="Asagi tasi">
<svg width="10" height="10" viewBox="0 0 10 10"><path d="M5 8L2 4h6L5 8z" fill="currentColor"/></svg>
</button>
<button class="tbl-col__act tbl-col__act--del" @click="removeColumn(col.id)" data-tip="Sutunu sil">
<svg width="10" height="10" viewBox="0 0 10 10"><path d="M2 2l6 6M8 2l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</button>
</div>
</div>
<div class="prop-row">
<label class="prop-label">Baslik</label>
<input class="prop-input" type="text" :value="col.title"
@change="(e) => updateColumn(col.id, { title: (e.target as HTMLInputElement).value })" />
</div>
<div class="prop-row">
<label class="prop-label">Alan</label>
<select v-if="tableItemFields.length > 0" class="prop-input prop-select" :value="col.field"
<!-- Row 2: field + align + format + width compact -->
<div class="tbl-col__controls">
<!-- Field -->
<select v-if="tableItemFields.length > 0" class="tbl-col__field" :value="col.field" data-tip="Veri alani"
@change="(e) => {
const field = (e.target as HTMLSelectElement).value
const node = tableItemFields.find(f => f.key === field)
@@ -144,23 +151,30 @@ const tableItemFields = computed(() => {
updateColumn(col.id, { field })
}
}">
<option v-for="f in tableItemFields" :key="f.key" :value="f.key">{{ f.title }} ({{ f.key }})</option>
<option v-for="f in tableItemFields" :key="f.key" :value="f.key">{{ f.key }}</option>
</select>
<input v-else class="prop-input" type="text" :value="col.field"
@change="(e) => updateColumn(col.id, { field: (e.target as HTMLInputElement).value })" />
<input v-else class="tbl-col__field" type="text" :value="col.field"
@change="(e) => updateColumn(col.id, { field: (e.target as HTMLInputElement).value })"
data-tip="Veri alani" />
<!-- Alignment icons -->
<div class="tbl-col__align">
<button class="tbl-col__align-btn" :class="{ 'tbl-col__align-btn--on': col.align === 'left' }" @click="updateColumn(col.id, { align: 'left' })" data-tip="Sola hizala">
<svg width="12" height="12" viewBox="0 0 12 12"><line x1="1" y1="3" x2="11" y2="3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><line x1="1" y1="6" x2="8" y2="6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><line x1="1" y1="9" x2="10" y2="9" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
</button>
<button class="tbl-col__align-btn" :class="{ 'tbl-col__align-btn--on': col.align === 'center' }" @click="updateColumn(col.id, { align: 'center' })" data-tip="Ortala">
<svg width="12" height="12" viewBox="0 0 12 12"><line x1="1" y1="3" x2="11" y2="3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><line x1="2.5" y1="6" x2="9.5" y2="6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><line x1="1.5" y1="9" x2="10.5" y2="9" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
</button>
<button class="tbl-col__align-btn" :class="{ 'tbl-col__align-btn--on': col.align === 'right' }" @click="updateColumn(col.id, { align: 'right' })" data-tip="Saga hizala">
<svg width="12" height="12" viewBox="0 0 12 12"><line x1="1" y1="3" x2="11" y2="3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><line x1="4" y1="6" x2="11" y2="6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><line x1="2" y1="9" x2="11" y2="9" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
</button>
</div>
</div>
<div class="prop-row">
<label class="prop-label">Hizalama</label>
<select class="prop-input prop-select" :value="col.align"
@change="(e) => updateColumn(col.id, { align: (e.target as HTMLSelectElement).value as 'left'|'center'|'right' })">
<option value="left">Sol</option>
<option value="center">Orta</option>
<option value="right">Sag</option>
</select>
</div>
<div class="prop-row">
<label class="prop-label">Format</label>
<select class="prop-input prop-select" :value="col.format ?? ''"
<!-- Row 3: format + width -->
<div class="tbl-col__extra" data-tip="Veri gosterim formati">
<label class="tbl-col__elabel">Format</label>
<select class="tbl-col__fmt" :value="col.format ?? ''"
@change="(e) => updateColumn(col.id, { format: ((e.target as HTMLSelectElement).value || undefined) as FormatType | undefined })">
<option value="">Yok</option>
<option value="currency">Para birimi</option>
@@ -169,10 +183,9 @@ const tableItemFields = computed(() => {
<option value="percentage">Yuzde</option>
</select>
</div>
<div class="prop-row">
<label class="prop-label">Genislik</label>
<select class="prop-input prop-select"
:value="col.width.type"
<div class="tbl-col__extra" data-tip="Sutun genislik modu">
<label class="tbl-col__elabel">Genislik</label>
<select class="tbl-col__wtype" :value="col.width.type"
@change="(e) => {
const t = (e.target as HTMLSelectElement).value
if (t === 'auto') updateColumn(col.id, { width: { type: 'auto' } })
@@ -183,71 +196,484 @@ const tableItemFields = computed(() => {
<option value="fixed">Sabit (mm)</option>
<option value="fr">Oran (fr)</option>
</select>
<span v-if="col.width.type === 'fixed' || col.width.type === 'fr'" class="ts-tip-wrap" :data-tip="col.width.type === 'fixed' ? 'Sabit genislik (mm)' : 'Oran degeri (fr)'">
<input class="tbl-col__wval" type="number" step="1"
:min="col.width.type === 'fixed' ? 5 : 1"
:value="(col.width as any).value"
@change="(e) => updateColumn(col.id, { width: { type: col.width.type, value: parseFloat((e.target as HTMLInputElement).value) || (col.width.type === 'fixed' ? 30 : 1) } as any })" />
</span>
</div>
<div v-if="col.width.type === 'fixed'" class="prop-row">
<label class="prop-label">mm</label>
<input class="prop-input" type="number" step="1" min="5"
:value="(col.width as any).value"
@change="(e) => updateColumn(col.id, { width: { type: 'fixed', value: parseFloat((e.target as HTMLInputElement).value) || 30 } })" />
</div>
</div>
</div>
<!-- Sayfa bölme ayarları -->
<div class="prop-section">
<div class="prop-section__title">Sayfa Bolme</div>
<div class="prop-row">
<label class="prop-label">Header tekrarla</label>
<input type="checkbox"
:checked="element.repeatHeader !== false"
@change="(e) => update({ repeatHeader: (e.target as HTMLInputElement).checked } as any)" />
</div>
</div>
<!-- Table style -->
<div class="prop-section">
<div class="prop-section__title">Tablo Stili</div>
<div class="prop-row">
<label class="prop-label">Yazi boyutu</label>
<input class="prop-input" type="number" step="1" min="6"
:value="element.style.fontSize ?? 10"
@input="(e) => updateTableStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)" />
</div>
<div class="prop-row">
<label class="prop-label">Header bg</label>
<input class="prop-input prop-color" type="color"
:value="element.style.headerBg ?? '#f0f0f0'"
@input="(e) => updateTableStyle('headerBg', (e.target as HTMLInputElement).value)" />
</div>
<div class="prop-row">
<label class="prop-label">Header renk</label>
<input class="prop-input prop-color" type="color"
:value="element.style.headerColor ?? '#000000'"
@input="(e) => updateTableStyle('headerColor', (e.target as HTMLInputElement).value)" />
</div>
<div class="prop-row">
<label class="prop-label">Zebra tek</label>
<div class="prop-row-inline">
<input class="prop-input prop-color" type="color"
:value="element.style.zebraOdd ?? '#fafafa'"
@input="(e) => updateTableStyle('zebraOdd', (e.target as HTMLInputElement).value)" />
<button v-if="element.style.zebraOdd" class="prop-clear" @click="updateTableStyle('zebraOdd', undefined)">x</button>
<div class="ts-form">
<!-- Font sizes -->
<label class="ts-lbl" data-tip="Icerik ve header yazi boyutu (pt)">Yazi boyutu</label>
<div class="ts-val ts-val--pair">
<span class="ts-sep">Icerik</span>
<span class="ts-tip-wrap" data-tip="Icerik yazi boyutu (pt)">
<input class="ts-num" type="number" step="1" min="6" max="99"
:value="element.style.fontSize ?? 10"
@input="(e) => updateTableStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)" />
</span>
<span class="ts-sep">Header</span>
<span class="ts-tip-wrap" data-tip="Header yazi boyutu (pt)">
<input class="ts-num" type="number" step="1" min="6" max="99"
:value="element.style.headerFontSize ?? element.style.fontSize ?? 10"
@input="(e) => updateTableStyle('headerFontSize', parseFloat((e.target as HTMLInputElement).value) || 10)" />
</span>
</div>
</div>
<div class="prop-row">
<label class="prop-label">Kenarlik rengi</label>
<div class="prop-row-inline">
<input class="prop-input prop-color" type="color"
:value="element.style.borderColor ?? '#cccccc'"
@input="(e) => updateTableStyle('borderColor', (e.target as HTMLInputElement).value)" />
<button v-if="element.style.borderColor" class="prop-clear" @click="updateTableStyle('borderColor', undefined)">x</button>
<!-- Colors -->
<label class="ts-lbl" data-tip="Header, metin ve zebra satirlari renkleri">Renkler</label>
<div class="ts-val ts-val--colors">
<div class="ts-color-item" data-tip="Header arkaplan rengi">
<input class="ts-swatch" type="color"
:value="element.style.headerBg ?? '#f0f0f0'"
@input="(e) => updateTableStyle('headerBg', (e.target as HTMLInputElement).value)" />
<span class="ts-clbl">Arkaplan</span>
</div>
<div class="ts-color-item" data-tip="Header metin rengi">
<input class="ts-swatch" type="color"
:value="element.style.headerColor ?? '#000000'"
@input="(e) => updateTableStyle('headerColor', (e.target as HTMLInputElement).value)" />
<span class="ts-clbl">Metin</span>
</div>
<div class="ts-color-item" data-tip="Zebra satir rengi tek satirlar">
<div class="ts-swatch-wrap">
<input class="ts-swatch" type="color"
:value="element.style.zebraOdd ?? '#fafafa'"
@input="(e) => updateTableStyle('zebraOdd', (e.target as HTMLInputElement).value)" />
<button v-if="element.style.zebraOdd" class="ts-swatch-clr" @click="updateTableStyle('zebraOdd', undefined)">&times;</button>
</div>
<span class="ts-clbl">Zebra</span>
</div>
</div>
<!-- Border -->
<label class="ts-lbl" data-tip="Tablo kenarlik rengi ve kalinligi">Kenarlik</label>
<div class="ts-val ts-val--pair">
<div class="ts-swatch-wrap" data-tip="Kenarlik rengi">
<input class="ts-swatch" type="color"
:value="element.style.borderColor ?? '#cccccc'"
@input="(e) => updateTableStyle('borderColor', (e.target as HTMLInputElement).value)" />
<button v-if="element.style.borderColor" class="ts-swatch-clr" @click="updateTableStyle('borderColor', undefined)">&times;</button>
</div>
<span class="ts-tip-wrap" data-tip="Kenarlik kalinligi (mm)">
<input class="ts-num" type="number" step="0.1" min="0" max="99"
:value="element.style.borderWidth ?? 0.5"
@input="(e) => updateTableStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)" />
</span>
<span class="ts-unit">mm</span>
</div>
<!-- Cell padding -->
<label class="ts-lbl" data-tip="Hucre ic bosluklari yatay ve dikey (mm)">Ic bosluk</label>
<div class="ts-val ts-val--pair">
<span class="ts-pad-icon" data-tip="Yatay bosluk (mm)">&#8596;</span>
<span class="ts-tip-wrap" data-tip="Yatay ic bosluk (mm)">
<input class="ts-num" type="number" step="0.5" min="0" max="99"
:value="element.style.cellPaddingH ?? 2"
@input="(e) => updateTableStyle('cellPaddingH', parseFloat((e.target as HTMLInputElement).value) || 0)" />
</span>
<span class="ts-pad-icon" data-tip="Dikey bosluk (mm)">&#8597;</span>
<span class="ts-tip-wrap" data-tip="Dikey ic bosluk (mm)">
<input class="ts-num" type="number" step="0.5" min="0" max="99"
:value="element.style.cellPaddingV ?? 1"
@input="(e) => updateTableStyle('cellPaddingV', parseFloat((e.target as HTMLInputElement).value) || 0)" />
</span>
</div>
<!-- Header padding -->
<label class="ts-lbl" data-tip="Header hucre bosluklari yatay ve dikey (mm)">Header bosluk</label>
<div class="ts-val ts-val--pair">
<span class="ts-pad-icon" data-tip="Yatay bosluk (mm)">&#8596;</span>
<span class="ts-tip-wrap" data-tip="Header yatay bosluk (mm)">
<input class="ts-num" type="number" step="0.5" min="0" max="99"
:value="element.style.headerPaddingH ?? element.style.cellPaddingH ?? 2"
@input="(e) => updateTableStyle('headerPaddingH', parseFloat((e.target as HTMLInputElement).value) || 0)" />
</span>
<span class="ts-pad-icon" data-tip="Dikey bosluk (mm)">&#8597;</span>
<span class="ts-tip-wrap" data-tip="Header dikey bosluk (mm)">
<input class="ts-num" type="number" step="0.5" min="0" max="99"
:value="element.style.headerPaddingV ?? element.style.cellPaddingV ?? 1"
@input="(e) => updateTableStyle('headerPaddingV', parseFloat((e.target as HTMLInputElement).value) || 0)" />
</span>
</div>
<!-- Repeat header -->
<label class="ts-lbl" data-tip="Cok sayfali tablolarda header'i her sayfada tekrarla">Header tekrarla</label>
<div class="ts-val">
<label class="ts-toggle">
<input type="checkbox"
:checked="element.repeatHeader !== false"
@change="(e) => update({ repeatHeader: (e.target as HTMLInputElement).checked } as any)" />
<span class="ts-toggle__track"></span>
</label>
</div>
</div>
<div class="prop-row">
<label class="prop-label">Kenarlik (mm)</label>
<input class="prop-input" type="number" step="0.1" min="0"
:value="element.style.borderWidth ?? 0.5"
@input="(e) => updateTableStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)" />
</div>
</div>
</template>
<style scoped>
/* Column card - compact */
.tbl-col {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 5px;
padding: 5px 6px;
margin-bottom: 5px;
}
.tbl-col__head {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 4px;
}
.tbl-col__title {
flex: 1;
min-width: 0;
border: none;
background: transparent;
font-size: 12px;
font-weight: 500;
color: #334155;
padding: 1px 0;
outline: none;
}
.tbl-col__title:focus {
border-bottom: 1px solid #93c5fd;
}
.tbl-col__actions {
display: flex;
gap: 1px;
flex-shrink: 0;
}
.tbl-col__act {
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border: none;
border-radius: 3px;
background: transparent;
color: #94a3b8;
cursor: pointer;
padding: 0;
}
.tbl-col__act:hover {
background: #e2e8f0;
color: #475569;
}
.tbl-col__act--del:hover {
background: #fef2f2;
color: #dc2626;
}
.tbl-col__controls {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 3px;
}
.tbl-col__field {
flex: 1;
min-width: 0;
padding: 2px 4px;
border: 1px solid #e2e8f0;
border-radius: 3px;
font-size: 11px;
background: white;
color: #334155;
}
.tbl-col__field:focus {
outline: none;
border-color: #93c5fd;
}
.tbl-col__align {
display: flex;
gap: 0;
flex-shrink: 0;
}
.tbl-col__align-btn {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border: 1px solid #e2e8f0;
background: white;
color: #94a3b8;
cursor: pointer;
padding: 0;
}
.tbl-col__align-btn:first-child {
border-radius: 3px 0 0 3px;
}
.tbl-col__align-btn:last-child {
border-radius: 0 3px 3px 0;
}
.tbl-col__align-btn:not(:first-child) {
border-left: none;
}
.tbl-col__align-btn--on {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.tbl-col__extra {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 3px;
}
.tbl-col__elabel {
font-size: 11px;
color: #64748b;
flex-shrink: 0;
}
.tbl-col__fmt {
flex: 1;
min-width: 0;
padding: 2px 4px;
border: 1px solid #e2e8f0;
border-radius: 3px;
font-size: 11px;
background: white;
color: #334155;
cursor: pointer;
}
.tbl-col__wtype {
width: 80px;
padding: 2px 4px;
border: 1px solid #e2e8f0;
border-radius: 3px;
font-size: 11px;
background: white;
color: #334155;
cursor: pointer;
}
.tbl-col__wval {
width: 36px;
padding: 2px 3px;
border: 1px solid #e2e8f0;
border-radius: 3px;
font-size: 11px;
background: white;
color: #334155;
text-align: center;
-moz-appearance: textfield;
}
.tbl-col__wval::-webkit-inner-spin-button,
.tbl-col__wval::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.tbl-col__wval:focus {
outline: none;
border-color: #93c5fd;
}
/* Table style — aligned 2-column form */
.ts-form {
display: grid;
grid-template-columns: auto 1fr;
gap: 5px 8px;
align-items: center;
}
.ts-lbl {
font-size: 11px;
color: #64748b;
white-space: nowrap;
}
.ts-val {
display: flex;
align-items: center;
justify-content: flex-end;
}
.ts-val--pair {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
}
.ts-val--colors {
display: flex;
align-items: flex-end;
justify-content: flex-end;
gap: 6px;
}
.ts-sep {
font-size: 10px;
color: #94a3b8;
}
.ts-num {
width: 32px;
padding: 2px 3px;
border: 1px solid #e2e8f0;
border-radius: 3px;
font-size: 11px;
background: white;
color: #334155;
text-align: center;
-moz-appearance: textfield;
}
.ts-num::-webkit-inner-spin-button,
.ts-num::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.ts-num:focus {
outline: none;
border-color: #93c5fd;
}
.ts-unit {
font-size: 10px;
color: #94a3b8;
}
/* Color swatches */
.ts-color-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.ts-clbl {
font-size: 9px;
color: #94a3b8;
white-space: nowrap;
}
.ts-swatch {
width: 22px;
height: 22px;
padding: 0;
cursor: pointer;
border: 1px solid #e2e8f0;
border-radius: 3px;
}
.ts-swatch-wrap {
position: relative;
display: inline-flex;
}
.ts-swatch-clr {
position: absolute;
top: -4px;
right: -4px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #f1f5f9;
border: 1px solid #e2e8f0;
font-size: 9px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #94a3b8;
padding: 0;
}
.ts-swatch-clr:hover {
background: #fef2f2;
color: #dc2626;
border-color: #fecaca;
}
.ts-pad-icon {
font-size: 11px;
color: #94a3b8;
line-height: 1;
}
.ts-tip-wrap {
position: relative;
display: inline-flex;
}
/* Toggle switch */
.ts-toggle {
position: relative;
display: inline-block;
cursor: pointer;
}
.ts-toggle input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.ts-toggle__track {
display: block;
width: 28px;
height: 16px;
background: #e2e8f0;
border-radius: 8px;
transition: background 0.15s;
position: relative;
}
.ts-toggle__track::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 12px;
height: 12px;
background: white;
border-radius: 50%;
transition: transform 0.15s;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.ts-toggle input:checked + .ts-toggle__track {
background: #3b82f6;
}
.ts-toggle input:checked + .ts-toggle__track::after {
transform: translateX(12px);
}
</style>

View File

@@ -44,19 +44,19 @@ function removeSpan(index: number) {
<template>
<div class="prop-section">
<div class="prop-section__title">Varsayilan Stil</div>
<div class="prop-row">
<div class="prop-row" data-tip="Varsayilan yazi tipi boyutu (point)">
<label class="prop-label">Boyut (pt)</label>
<input class="prop-input" type="number" step="1" min="1"
:value="element.style.fontSize ?? 11"
@input="(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 11)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Varsayilan metin rengi">
<label class="prop-label">Renk</label>
<input class="prop-input prop-color" type="color"
:value="element.style.color ?? '#000000'"
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Metnin yatay hizalamasi">
<label class="prop-label">Hizalama</label>
<select class="prop-input prop-select"
:value="element.style.align ?? 'left'"
@@ -85,13 +85,13 @@ function removeSpan(index: number) {
>&times;</button>
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Span metin icerigi">
<label class="prop-label">Metin</label>
<input class="prop-input" type="text"
:value="span.text ?? ''"
@input="(e) => updateSpan(idx, { text: (e.target as HTMLInputElement).value })" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Span yazi boyutu — bos birakilirsa varsayilan kullanilir">
<label class="prop-label">Boyut</label>
<input class="prop-input" type="number" step="1" min="1"
:value="(span.style as TextStyle).fontSize ?? ''"
@@ -101,7 +101,7 @@ function removeSpan(index: number) {
updateSpanStyle(idx, 'fontSize', v ? parseFloat(v) : undefined)
}" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Span yazi kalinligi">
<label class="prop-label">Kalinlik</label>
<select class="prop-input prop-select"
:value="(span.style as TextStyle).fontWeight ?? ''"
@@ -114,7 +114,7 @@ function removeSpan(index: number) {
<option value="bold">Kalin</option>
</select>
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Span metin rengi">
<label class="prop-label">Renk</label>
<input class="prop-input prop-color" type="color"
:value="(span.style as TextStyle).color ?? element.style.color ?? '#000000'"

View File

@@ -22,7 +22,7 @@ function updateStyle(key: string, value: unknown) {
<template>
<div class="prop-section">
<div class="prop-section__title">Sekil</div>
<div class="prop-row">
<div class="prop-row" data-tip="Sekil tipi">
<label class="prop-label">Tip</label>
<select class="prop-input prop-select"
:value="element.shapeType"
@@ -32,25 +32,25 @@ function updateStyle(key: string, value: unknown) {
<option value="ellipse">Elips</option>
</select>
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Sekil arka plan rengi">
<label class="prop-label">Arka Plan</label>
<input class="prop-input prop-color" type="color"
:value="element.style.backgroundColor ?? '#f0f0f0'"
@input="(e) => updateStyle('backgroundColor', (e.target as HTMLInputElement).value)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Kenarlik cizgisi rengi">
<label class="prop-label">Kenar Rengi</label>
<input class="prop-input prop-color" type="color"
:value="element.style.borderColor ?? '#333333'"
@input="(e) => updateStyle('borderColor', (e.target as HTMLInputElement).value)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Kenarlik cizgi kalinligi (mm)">
<label class="prop-label">Kenar Kalinligi</label>
<input class="prop-input" type="number" step="0.25" min="0"
:value="element.style.borderWidth ?? 0.5"
@input="(e) => updateStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)" />
</div>
<div v-if="element.shapeType === 'rounded_rectangle'" class="prop-row">
<div v-if="element.shapeType === 'rounded_rectangle'" class="prop-row" data-tip="Kose yuvarlakligi (mm)">
<label class="prop-label">Kose Yuvarlakligi</label>
<input class="prop-input" type="number" step="0.5" min="0"
:value="element.style.borderRadius ?? 2"

View File

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

View File

@@ -23,20 +23,20 @@ function updateStyle(key: string, value: unknown) {
<div class="prop-section">
<div class="prop-section__title">Metin Stili</div>
<div v-if="element.type === 'static_text'" class="prop-row">
<div v-if="element.type === 'static_text'" class="prop-row" data-tip="Sabit metin icerigi">
<label class="prop-label">Metin</label>
<input class="prop-input" type="text"
:value="(element as StaticTextElement).content"
@input="(e) => update({ content: (e.target as HTMLInputElement).value } as any)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Yazi tipi boyutu (point)">
<label class="prop-label">Boyut (pt)</label>
<input class="prop-input" type="number" step="1" min="1"
:value="(element.style as TextStyle).fontSize ?? 11"
@input="(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 11)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Yazi tipi kalinligi">
<label class="prop-label">Kalinlik</label>
<select class="prop-input prop-select"
:value="(element.style as TextStyle).fontWeight ?? 'normal'"
@@ -45,13 +45,13 @@ function updateStyle(key: string, value: unknown) {
<option value="bold">Kalin</option>
</select>
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Metin rengi">
<label class="prop-label">Renk</label>
<input class="prop-input prop-color" type="color"
:value="(element.style as TextStyle).color ?? '#000000'"
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
</div>
<div class="prop-row">
<div class="prop-row" data-tip="Metnin yatay hizalamasi">
<label class="prop-label">Hizalama</label>
<select class="prop-input prop-select"
:value="(element.style as TextStyle).align ?? 'left'"