mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-02 02:49:16 +00:00
fixes
This commit is contained in:
237
frontend/src/components/common/DexprEditor.vue
Normal file
237
frontend/src/components/common/DexprEditor.vue
Normal 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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {},
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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 ?? ''"
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">↑</button>
|
||||
<button class="prop-icon-btn" @click="moveColumn(col.id, 1)" title="Asagi">↓</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)">×</button>
|
||||
</div>
|
||||
<span class="ts-clbl">Zebra</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Border -->
|
||||
<label class="ts-lbl" data-tip="Tablo kenarlik rengi ve kalinligi">Kenarlik</label>
|
||||
<div class="ts-val ts-val--pair">
|
||||
<div class="ts-swatch-wrap" data-tip="Kenarlik rengi">
|
||||
<input class="ts-swatch" type="color"
|
||||
:value="element.style.borderColor ?? '#cccccc'"
|
||||
@input="(e) => updateTableStyle('borderColor', (e.target as HTMLInputElement).value)" />
|
||||
<button v-if="element.style.borderColor" class="ts-swatch-clr" @click="updateTableStyle('borderColor', undefined)">×</button>
|
||||
</div>
|
||||
<span class="ts-tip-wrap" data-tip="Kenarlik kalinligi (mm)">
|
||||
<input class="ts-num" type="number" step="0.1" min="0" max="99"
|
||||
:value="element.style.borderWidth ?? 0.5"
|
||||
@input="(e) => updateTableStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
</span>
|
||||
<span class="ts-unit">mm</span>
|
||||
</div>
|
||||
|
||||
<!-- Cell padding -->
|
||||
<label class="ts-lbl" data-tip="Hucre ic bosluklari — yatay ve dikey (mm)">Ic bosluk</label>
|
||||
<div class="ts-val ts-val--pair">
|
||||
<span class="ts-pad-icon" data-tip="Yatay bosluk (mm)">↔</span>
|
||||
<span class="ts-tip-wrap" data-tip="Yatay ic bosluk (mm)">
|
||||
<input class="ts-num" type="number" step="0.5" min="0" max="99"
|
||||
:value="element.style.cellPaddingH ?? 2"
|
||||
@input="(e) => updateTableStyle('cellPaddingH', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
</span>
|
||||
<span class="ts-pad-icon" data-tip="Dikey bosluk (mm)">↕</span>
|
||||
<span class="ts-tip-wrap" data-tip="Dikey ic bosluk (mm)">
|
||||
<input class="ts-num" type="number" step="0.5" min="0" max="99"
|
||||
:value="element.style.cellPaddingV ?? 1"
|
||||
@input="(e) => updateTableStyle('cellPaddingV', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Header padding -->
|
||||
<label class="ts-lbl" data-tip="Header hucre bosluklari — yatay ve dikey (mm)">Header bosluk</label>
|
||||
<div class="ts-val ts-val--pair">
|
||||
<span class="ts-pad-icon" data-tip="Yatay bosluk (mm)">↔</span>
|
||||
<span class="ts-tip-wrap" data-tip="Header yatay bosluk (mm)">
|
||||
<input class="ts-num" type="number" step="0.5" min="0" max="99"
|
||||
:value="element.style.headerPaddingH ?? element.style.cellPaddingH ?? 2"
|
||||
@input="(e) => updateTableStyle('headerPaddingH', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
</span>
|
||||
<span class="ts-pad-icon" data-tip="Dikey bosluk (mm)">↕</span>
|
||||
<span class="ts-tip-wrap" data-tip="Header dikey bosluk (mm)">
|
||||
<input class="ts-num" type="number" step="0.5" min="0" max="99"
|
||||
:value="element.style.headerPaddingV ?? element.style.cellPaddingV ?? 1"
|
||||
@input="(e) => updateTableStyle('headerPaddingV', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Repeat header -->
|
||||
<label class="ts-lbl" data-tip="Cok sayfali tablolarda header'i her sayfada tekrarla">Header tekrarla</label>
|
||||
<div class="ts-val">
|
||||
<label class="ts-toggle">
|
||||
<input type="checkbox"
|
||||
:checked="element.repeatHeader !== false"
|
||||
@change="(e) => update({ repeatHeader: (e.target as HTMLInputElement).checked } as any)" />
|
||||
<span class="ts-toggle__track"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div 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>
|
||||
|
||||
@@ -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) {
|
||||
>×</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'"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'"
|
||||
|
||||
Reference in New Issue
Block a user