zakladna implementacia by Codex
This commit is contained in:
109
frontend/src/views/EditorView.vue
Normal file
109
frontend/src/views/EditorView.vue
Normal file
@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useMosaicStore } from '@/stores/mosaic'
|
||||
import EditorToolbar from '@/components/EditorToolbar.vue'
|
||||
import MosaicCanvas from '@/components/MosaicCanvas.vue'
|
||||
import SettingsPanel from '@/components/SettingsPanel.vue'
|
||||
import ColorGroupList from '@/components/ColorGroupList.vue'
|
||||
import RegionList from '@/components/RegionList.vue'
|
||||
import ExportPanel from '@/components/ExportPanel.vue'
|
||||
|
||||
const store = useMosaicStore()
|
||||
const { imageName, imageWidth, imageHeight, regions, groups, toasts, hasRecovery, recoveryTimestamp } = storeToRefs(store)
|
||||
const mosaicCanvas = ref<InstanceType<typeof MosaicCanvas> | null>(null)
|
||||
|
||||
function keyboard(event: KeyboardEvent): void {
|
||||
const target = event.target as HTMLElement | null
|
||||
if (target?.matches('input, select, textarea, [contenteditable="true"]')) return
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'z') {
|
||||
event.preventDefault()
|
||||
if (event.shiftKey) store.redo()
|
||||
else store.undo()
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'y') {
|
||||
event.preventDefault()
|
||||
store.redo()
|
||||
} else if (event.key === 'Delete' && store.selectedRegionId) {
|
||||
event.preventDefault()
|
||||
store.removeRegion(store.selectedRegionId)
|
||||
} else if (event.key === 'Escape') {
|
||||
store.selectedRegionId = null
|
||||
store.selectedGroupId = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', keyboard)
|
||||
void store.checkRecovery()
|
||||
})
|
||||
onBeforeUnmount(() => window.removeEventListener('keydown', keyboard))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-shell">
|
||||
<EditorToolbar @reset-viewport="mosaicCanvas?.resetViewport()" />
|
||||
<div v-if="hasRecovery" class="recovery-banner">
|
||||
<span><b>Nájdená rozpracovaná práca.</b> Uložená {{ new Date(recoveryTimestamp).toLocaleString('sk-SK') }}.</span>
|
||||
<div><button class="button primary" type="button" @click="store.restoreRecovery">Pokračovať</button><button class="button" type="button" @click="store.dismissRecovery">Zahodiť</button></div>
|
||||
</div>
|
||||
<main class="workspace">
|
||||
<section class="editor-pane"><MosaicCanvas ref="mosaicCanvas" /></section>
|
||||
<aside class="control-panel">
|
||||
<section class="panel-section image-info">
|
||||
<div class="section-heading"><h2>Obrázok</h2><span class="local-badge">Iba lokálne</span></div>
|
||||
<template v-if="imageWidth">
|
||||
<b class="filename" :title="imageName">{{ imageName }}</b>
|
||||
<div class="stats"><span>{{ imageWidth }} × {{ imageHeight }} px</span><span>{{ regions.length }} plôch</span><span>{{ groups.length }} skupín</span></div>
|
||||
</template>
|
||||
<p v-else class="empty-copy">Načítajte obrázok a klikajte dovnútra samostatných farebných plôch.</p>
|
||||
</section>
|
||||
<SettingsPanel />
|
||||
<ColorGroupList />
|
||||
<RegionList />
|
||||
<ExportPanel />
|
||||
</aside>
|
||||
</main>
|
||||
<div class="toast-stack" aria-live="polite">
|
||||
<button v-for="toast in toasts" :key="toast.id" type="button" class="toast" :class="toast.type" @click="store.dismissToast(toast.id)">{{ toast.text }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
:root { font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #1e293b; background: #f2f4f7; font-synthesis: none; text-rendering: optimizeLegibility; }
|
||||
* { box-sizing: border-box; }
|
||||
html, body, #app { min-width: 320px; min-height: 100%; margin: 0; }
|
||||
body { min-height: 100vh; overflow: hidden; }
|
||||
button, input, select { font: inherit; }
|
||||
button:focus-visible, input:focus-visible, select:focus-visible, canvas:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
|
||||
.app-shell { min-height: 100vh; }
|
||||
.workspace { display: grid; height: calc(100vh - 58px); grid-template-columns: minmax(0, 1fr) 360px; }
|
||||
.editor-pane { min-width: 0; min-height: 0; border-right: 1px solid #d9e0e7; }
|
||||
.control-panel { min-width: 0; overflow-y: auto; background: #f8fafc; }
|
||||
.panel-section { padding: 15px; border-bottom: 1px solid #dfe5eb; }
|
||||
.panel-section h2 { margin: 0; color: #263244; font-size: .82rem; font-weight: 720; letter-spacing: .01em; }
|
||||
.section-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.section-heading > span { color: #718096; font-size: .7rem; }
|
||||
.empty-copy { margin: 10px 0 0; color: #718096; font-size: .73rem; line-height: 1.5; }
|
||||
.button { min-height: 31px; padding: 6px 10px; border: 1px solid #cbd5e1; border-radius: 7px; background: #fff; color: #334155; font-size: .73rem; font-weight: 600; white-space: nowrap; cursor: pointer; }
|
||||
.button:hover:not(:disabled) { border-color: #94a3b8; background: #f8fafc; }
|
||||
.button.primary { border-color: #1d4ed8; background: #2563eb; color: white; }
|
||||
.button.primary:hover:not(:disabled) { border-color: #1e40af; background: #1d4ed8; }
|
||||
.button.danger-quiet { color: #b42318; }
|
||||
.button.small { min-height: 28px; padding: 4px 8px; font-size: .67rem; }
|
||||
.button:disabled, .icon-button:disabled { opacity: .42; cursor: not-allowed; }
|
||||
.icon-button { display: grid; width: 32px; height: 32px; place-items: center; padding: 0; border: 1px solid #cbd5e1; border-radius: 7px; background: white; color: #334155; font-size: 19px; cursor: pointer; }
|
||||
select { min-height: 31px; padding: 5px 7px; border: 1px solid #cbd5e1; border-radius: 7px; background: white; color: #334155; font-size: .72rem; }
|
||||
input[type="range"] { width: 100%; accent-color: #2563eb; }
|
||||
input[type="checkbox"] { accent-color: #2563eb; }
|
||||
.image-info .filename { display: block; overflow: hidden; margin-top: 9px; color: #334155; font-size: .76rem; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.stats { display: flex; flex-wrap: wrap; gap: 5px 11px; margin-top: 7px; color: #64748b; font-size: .68rem; }
|
||||
.local-badge { padding: 3px 6px; border-radius: 9px; background: #e8f5ee; color: #247044 !important; font-weight: 650; }
|
||||
.recovery-banner { position: fixed; z-index: 30; top: 68px; left: 50%; display: flex; max-width: min(680px, calc(100vw - 24px)); align-items: center; gap: 16px; transform: translateX(-50%); padding: 11px 13px; border: 1px solid #93b4e8; border-radius: 10px; background: #eff6ff; box-shadow: 0 10px 30px #0f172a22; color: #334155; font-size: .76rem; }
|
||||
.recovery-banner > div { display: flex; gap: 6px; }
|
||||
.toast-stack { position: fixed; z-index: 50; right: 16px; bottom: 16px; display: grid; width: min(370px, calc(100vw - 32px)); gap: 8px; }
|
||||
.toast { padding: 11px 13px; border: 1px solid #cbd5e1; border-left-width: 4px; border-radius: 8px; background: white; box-shadow: 0 8px 25px #0f172a22; color: #334155; font-size: .76rem; line-height: 1.4; text-align: left; cursor: pointer; }
|
||||
.toast.success { border-left-color: #16a34a; }.toast.warning { border-left-color: #d97706; }.toast.error { border-left-color: #dc2626; }.toast.info { border-left-color: #2563eb; }
|
||||
@media (max-width: 950px) { .workspace { grid-template-columns: minmax(0,1fr) 330px; } }
|
||||
@media (max-width: 800px) { body { overflow: auto; } .workspace { height: auto; grid-template-columns: 1fr; } .editor-pane { height: 58vh; border-right: 0; border-bottom: 1px solid #d9e0e7; } .control-panel { overflow: visible; } .recovery-banner { align-items: flex-start; } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user