mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-17 06:30:50 +01:00
Add device management and database-backed desktop settings
This update migrates desktop grid settings from localStorage to the database and introduces a comprehensive device management system. Features: - New haex_devices table for device identification and naming - Device-specific settings with foreign key relationships - Preset-based icon sizes (Small, Medium, Large, Extra Large) - Grid positioning improvements to prevent dragging behind PageHeader - Dynamic icon sizing based on database settings Database Changes: - Created haex_devices table with deviceId (UUID) and name columns - Modified haex_settings to include device_id FK and updated unique constraint - Migration 0002_loose_quasimodo.sql for schema changes Technical Improvements: - Replaced arbitrary icon size slider (60-200px) with preset system - Icons use actual measured dimensions for proper grid centering - Settings sync on vault mount for cross-device consistency - Proper bounds checking during icon drag operations Bump version to 0.1.7
This commit is contained in:
@ -24,29 +24,25 @@
|
||||
<div class="flex flex-col items-center gap-2 p-3 group">
|
||||
<div
|
||||
:class="[
|
||||
'w-20 h-20 flex items-center justify-center rounded-2xl transition-all duration-200 ease-out',
|
||||
'flex items-center justify-center rounded-2xl transition-all duration-200 ease-out',
|
||||
'backdrop-blur-sm border',
|
||||
isSelected
|
||||
? 'bg-white/95 dark:bg-gray-800/95 border-blue-500 dark:border-blue-400 shadow-lg scale-105'
|
||||
: 'bg-white/80 dark:bg-gray-800/80 border-gray-200/50 dark:border-gray-700/50 hover:bg-white/90 dark:hover:bg-gray-800/90 hover:border-gray-300 dark:hover:border-gray-600 hover:shadow-md hover:scale-105',
|
||||
]"
|
||||
:style="{ width: `${containerSize}px`, height: `${containerSize}px` }"
|
||||
>
|
||||
<img
|
||||
v-if="icon"
|
||||
:src="icon"
|
||||
:alt="label"
|
||||
class="w-14 h-14 object-contain transition-transform duration-200"
|
||||
:class="{ 'scale-110': isSelected }"
|
||||
/>
|
||||
<UIcon
|
||||
v-else
|
||||
name="i-heroicons-puzzle-piece-solid"
|
||||
<HaexIcon
|
||||
:name="icon || 'i-heroicons-puzzle-piece-solid'"
|
||||
:class="[
|
||||
'w-14 h-14 transition-all duration-200',
|
||||
isSelected
|
||||
? 'text-blue-500 dark:text-blue-400 scale-110'
|
||||
: 'text-gray-400 dark:text-gray-500 group-hover:text-gray-500 dark:group-hover:text-gray-400',
|
||||
'object-contain transition-all duration-200',
|
||||
isSelected && 'scale-110',
|
||||
!icon &&
|
||||
(isSelected
|
||||
? 'text-blue-500 dark:text-blue-400'
|
||||
: 'text-gray-400 dark:text-gray-500 group-hover:text-gray-500 dark:group-hover:text-gray-400'),
|
||||
]"
|
||||
:style="{ width: `${innerIconSize}px`, height: `${innerIconSize}px` }"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
@ -79,15 +75,19 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
positionChanged: [id: string, x: number, y: number]
|
||||
dragStart: [id: string, itemType: string, referenceId: string]
|
||||
dragStart: [id: string, itemType: string, referenceId: string, width: number, height: number, x: number, y: number]
|
||||
dragging: [id: string, x: number, y: number]
|
||||
dragEnd: []
|
||||
}>()
|
||||
|
||||
const desktopStore = useDesktopStore()
|
||||
const { effectiveIconSize } = storeToRefs(desktopStore)
|
||||
const showUninstallDialog = ref(false)
|
||||
const { t } = useI18n()
|
||||
|
||||
const isSelected = computed(() => desktopStore.isItemSelected(props.id))
|
||||
const containerSize = computed(() => effectiveIconSize.value) // Container size
|
||||
const innerIconSize = computed(() => effectiveIconSize.value * 0.7) // Inner icon is 70% of container
|
||||
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
// Prevent selection during drag
|
||||
@ -131,9 +131,40 @@ const isDragging = ref(false)
|
||||
const offsetX = ref(0)
|
||||
const offsetY = ref(0)
|
||||
|
||||
// Icon dimensions (approximate)
|
||||
const iconWidth = 120 // Matches design in template
|
||||
const iconHeight = 140
|
||||
// Track actual icon dimensions dynamically
|
||||
const { width: iconWidth, height: iconHeight } = useElementSize(draggableEl)
|
||||
|
||||
// Re-center icon position when dimensions are measured
|
||||
watch([iconWidth, iconHeight], async ([width, height]) => {
|
||||
if (width > 0 && height > 0) {
|
||||
console.log('📐 Icon dimensions measured:', {
|
||||
label: props.label,
|
||||
width,
|
||||
height,
|
||||
currentPosition: { x: x.value, y: y.value },
|
||||
gridCellSize: desktopStore.gridCellSize,
|
||||
})
|
||||
|
||||
// Re-snap to grid with actual dimensions to ensure proper centering
|
||||
const snapped = desktopStore.snapToGrid(x.value, y.value, width, height)
|
||||
|
||||
console.log('📍 Snapped position:', {
|
||||
label: props.label,
|
||||
oldPosition: { x: x.value, y: y.value },
|
||||
newPosition: snapped,
|
||||
})
|
||||
|
||||
const oldX = x.value
|
||||
const oldY = y.value
|
||||
x.value = snapped.x
|
||||
y.value = snapped.y
|
||||
|
||||
// Save corrected position to database if it changed
|
||||
if (oldX !== snapped.x || oldY !== snapped.y) {
|
||||
emit('positionChanged', props.id, snapped.x, snapped.y)
|
||||
}
|
||||
}
|
||||
}, { once: true }) // Only run once when dimensions are first measured
|
||||
|
||||
const style = computed(() => ({
|
||||
position: 'absolute' as const,
|
||||
@ -146,7 +177,7 @@ const handlePointerDown = (e: PointerEvent) => {
|
||||
if (!draggableEl.value || !draggableEl.value.parentElement) return
|
||||
|
||||
isDragging.value = true
|
||||
emit('dragStart', props.id, props.itemType, props.referenceId)
|
||||
emit('dragStart', props.id, props.itemType, props.referenceId, iconWidth.value, iconHeight.value, x.value, y.value)
|
||||
|
||||
// Get parent offset to convert from viewport coordinates to parent-relative coordinates
|
||||
const parentRect = draggableEl.value.parentElement.getBoundingClientRect()
|
||||
@ -165,8 +196,12 @@ const handlePointerMove = (e: PointerEvent) => {
|
||||
const newX = e.clientX - parentRect.left - offsetX.value
|
||||
const newY = e.clientY - parentRect.top - offsetY.value
|
||||
|
||||
// Clamp y position to minimum 0 (parent is already below header)
|
||||
x.value = newX
|
||||
y.value = newY
|
||||
y.value = Math.max(0, newY)
|
||||
|
||||
// Emit current position during drag
|
||||
emit('dragging', props.id, x.value, y.value)
|
||||
}
|
||||
|
||||
const handlePointerUp = (e: PointerEvent) => {
|
||||
@ -177,10 +212,15 @@ const handlePointerUp = (e: PointerEvent) => {
|
||||
draggableEl.value.releasePointerCapture(e.pointerId)
|
||||
}
|
||||
|
||||
// Snap to grid with icon dimensions
|
||||
const snapped = desktopStore.snapToGrid(x.value, y.value, iconWidth.value, iconHeight.value)
|
||||
x.value = snapped.x
|
||||
y.value = snapped.y
|
||||
|
||||
// Snap icon to viewport bounds if outside
|
||||
if (viewportSize) {
|
||||
const maxX = Math.max(0, viewportSize.width.value - iconWidth)
|
||||
const maxY = Math.max(0, viewportSize.height.value - iconHeight)
|
||||
const maxX = Math.max(0, viewportSize.width.value - iconWidth.value)
|
||||
const maxY = Math.max(0, viewportSize.height.value - iconHeight.value)
|
||||
x.value = Math.max(0, Math.min(maxX, x.value))
|
||||
y.value = Math.max(0, Math.min(maxY, y.value))
|
||||
}
|
||||
|
||||
@ -32,13 +32,15 @@
|
||||
@dragover.prevent="handleDragOver"
|
||||
@drop.prevent="handleDrop($event, workspace.id)"
|
||||
>
|
||||
<!-- Grid Pattern Background -->
|
||||
<!-- Drop Target Zone (visible during drag) -->
|
||||
<div
|
||||
class="absolute inset-0 pointer-events-none opacity-30"
|
||||
v-if="dropTargetZone"
|
||||
class="absolute border-2 border-blue-500 bg-blue-500/10 rounded-lg pointer-events-none z-10 transition-all duration-75"
|
||||
:style="{
|
||||
backgroundImage:
|
||||
'linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 0, 0, 0.1) 1px, transparent 1px)',
|
||||
backgroundSize: '32px 32px',
|
||||
left: `${dropTargetZone.x}px`,
|
||||
top: `${dropTargetZone.y}px`,
|
||||
width: `${dropTargetZone.width}px`,
|
||||
height: `${dropTargetZone.height}px`,
|
||||
}"
|
||||
/>
|
||||
|
||||
@ -79,6 +81,7 @@
|
||||
class="no-swipe"
|
||||
@position-changed="handlePositionChanged"
|
||||
@drag-start="handleDragStart"
|
||||
@dragging="handleDragging"
|
||||
@drag-end="handleDragEnd"
|
||||
/>
|
||||
|
||||
@ -249,8 +252,6 @@ const {
|
||||
const { getWorkspaceBackgroundStyle, getWorkspaceContextMenuItems } =
|
||||
workspaceStore
|
||||
|
||||
const { x: mouseX } = useMouse()
|
||||
|
||||
const desktopEl = useTemplateRef('desktopEl')
|
||||
|
||||
// Track desktop viewport size reactively
|
||||
@ -284,9 +285,41 @@ const selectionBoxStyle = computed(() => {
|
||||
|
||||
// Drag state for desktop icons
|
||||
const isDragging = ref(false)
|
||||
const currentDraggedItemId = ref<string>()
|
||||
const currentDraggedItemType = ref<string>()
|
||||
const currentDraggedReferenceId = ref<string>()
|
||||
const currentDraggedItem = reactive({
|
||||
id: '',
|
||||
itemType: '',
|
||||
referenceId: '',
|
||||
width: 0,
|
||||
height: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
|
||||
// Track mouse position for showing drop target
|
||||
const { x: mouseX, y: mouseY } = useMouse()
|
||||
|
||||
const dropTargetZone = computed(() => {
|
||||
if (!isDragging.value) return null
|
||||
|
||||
// Use the actual icon position during drag, not the mouse position
|
||||
const iconX = currentDraggedItem.x
|
||||
const iconY = currentDraggedItem.y
|
||||
|
||||
// Use snapToGrid to get the exact position where the icon will land
|
||||
const snapped = desktopStore.snapToGrid(
|
||||
iconX,
|
||||
iconY,
|
||||
currentDraggedItem.width || undefined,
|
||||
currentDraggedItem.height || undefined,
|
||||
)
|
||||
|
||||
return {
|
||||
x: snapped.x,
|
||||
y: snapped.y,
|
||||
width: currentDraggedItem.width || desktopStore.gridCellSize,
|
||||
height: currentDraggedItem.height || desktopStore.gridCellSize,
|
||||
}
|
||||
})
|
||||
|
||||
// Window drag state for snap zones
|
||||
const isWindowDragging = ref(false)
|
||||
@ -378,20 +411,43 @@ const handlePositionChanged = async (id: string, x: number, y: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragStart = (id: string, itemType: string, referenceId: string) => {
|
||||
const handleDragStart = (
|
||||
id: string,
|
||||
itemType: string,
|
||||
referenceId: string,
|
||||
width: number,
|
||||
height: number,
|
||||
x: number,
|
||||
y: number,
|
||||
) => {
|
||||
isDragging.value = true
|
||||
currentDraggedItemId.value = id
|
||||
currentDraggedItemType.value = itemType
|
||||
currentDraggedReferenceId.value = referenceId
|
||||
currentDraggedItem.id = id
|
||||
currentDraggedItem.itemType = itemType
|
||||
currentDraggedItem.referenceId = referenceId
|
||||
currentDraggedItem.width = width
|
||||
currentDraggedItem.height = height
|
||||
currentDraggedItem.x = x
|
||||
currentDraggedItem.y = y
|
||||
allowSwipe.value = false // Disable Swiper during icon drag
|
||||
}
|
||||
|
||||
const handleDragging = (id: string, x: number, y: number) => {
|
||||
if (currentDraggedItem.id === id) {
|
||||
currentDraggedItem.x = x
|
||||
currentDraggedItem.y = y
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragEnd = async () => {
|
||||
// Cleanup drag state
|
||||
isDragging.value = false
|
||||
currentDraggedItemId.value = undefined
|
||||
currentDraggedItemType.value = undefined
|
||||
currentDraggedReferenceId.value = undefined
|
||||
currentDraggedItem.id = ''
|
||||
currentDraggedItem.itemType = ''
|
||||
currentDraggedItem.referenceId = ''
|
||||
currentDraggedItem.width = 0
|
||||
currentDraggedItem.height = 0
|
||||
currentDraggedItem.x = 0
|
||||
currentDraggedItem.y = 0
|
||||
allowSwipe.value = true // Re-enable Swiper after drag
|
||||
}
|
||||
|
||||
@ -426,15 +482,18 @@ const handleDrop = async (event: DragEvent, workspaceId: string) => {
|
||||
const desktopRect = (
|
||||
event.currentTarget as HTMLElement
|
||||
).getBoundingClientRect()
|
||||
const x = Math.max(0, event.clientX - desktopRect.left - 32) // Center icon (64px / 2)
|
||||
const y = Math.max(0, event.clientY - desktopRect.top - 32)
|
||||
const rawX = Math.max(0, event.clientX - desktopRect.left - 32) // Center icon (64px / 2)
|
||||
const rawY = Math.max(0, event.clientY - desktopRect.top - 32)
|
||||
|
||||
// Snap to grid
|
||||
const snapped = desktopStore.snapToGrid(rawX, rawY)
|
||||
|
||||
// Create desktop icon on the specific workspace
|
||||
await desktopStore.addDesktopItemAsync(
|
||||
item.type as DesktopItemType,
|
||||
item.id,
|
||||
x,
|
||||
y,
|
||||
snapped.x,
|
||||
snapped.y,
|
||||
workspaceId,
|
||||
)
|
||||
} catch (error) {
|
||||
|
||||
65
src/components/haex/icon.vue
Normal file
65
src/components/haex/icon.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="inline-flex">
|
||||
<UTooltip :text="tooltip">
|
||||
<!-- Bundled Icon (iconify) -->
|
||||
<UIcon
|
||||
v-if="isBundledIcon"
|
||||
:name="name"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
|
||||
<!-- External Image (Extension icon) -->
|
||||
<img
|
||||
v-else
|
||||
:src="imageUrl"
|
||||
v-bind="$attrs"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
name: string
|
||||
tooltip?: string
|
||||
}>()
|
||||
|
||||
// Check if it's a bundled icon (no file extension)
|
||||
const isBundledIcon = computed(() => {
|
||||
return !props.name.match(/\.(png|jpg|jpeg|svg|gif|webp|ico)$/i)
|
||||
})
|
||||
|
||||
// Convert file path to Tauri URL for images
|
||||
const imageUrl = ref('')
|
||||
const showFallback = ref(false)
|
||||
|
||||
// Default fallback icon
|
||||
const FALLBACK_ICON = 'i-heroicons-puzzle-piece-solid'
|
||||
|
||||
watchEffect(() => {
|
||||
if (!isBundledIcon.value && !showFallback.value) {
|
||||
// Convert local file path to Tauri asset URL
|
||||
imageUrl.value = convertFileSrc(props.name)
|
||||
}
|
||||
})
|
||||
|
||||
const handleImageError = () => {
|
||||
console.warn(`Failed to load icon: ${props.name}`)
|
||||
showFallback.value = true
|
||||
}
|
||||
|
||||
// Use fallback icon if image failed to load
|
||||
const name = computed(() => {
|
||||
if (showFallback.value) {
|
||||
return FALLBACK_ICON
|
||||
}
|
||||
return props.name
|
||||
})
|
||||
</script>
|
||||
@ -47,6 +47,21 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Grid Settings -->
|
||||
<div
|
||||
class="col-span-2 mt-4 border-t border-gray-200 dark:border-gray-700 pt-4"
|
||||
>
|
||||
<h3 class="text-lg font-semibold mb-4">{{ t('desktopGrid.title') }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="p-2">{{ t('desktopGrid.iconSize.label') }}</div>
|
||||
<div>
|
||||
<USelect
|
||||
v-model="iconSizePreset"
|
||||
:items="iconSizePresetOptions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="h-full" />
|
||||
</div>
|
||||
</div>
|
||||
@ -63,6 +78,7 @@ import {
|
||||
remove,
|
||||
} from '@tauri-apps/plugin-fs'
|
||||
import { appLocalDataDir } from '@tauri-apps/api/path'
|
||||
import { DesktopIconSizePreset } from '~/stores/vault/settings'
|
||||
|
||||
const { t, setLocale } = useI18n()
|
||||
|
||||
@ -104,8 +120,40 @@ const workspaceStore = useWorkspaceStore()
|
||||
const { currentWorkspace } = storeToRefs(workspaceStore)
|
||||
const { updateWorkspaceBackgroundAsync } = workspaceStore
|
||||
|
||||
const desktopStore = useDesktopStore()
|
||||
const { iconSizePreset } = storeToRefs(desktopStore)
|
||||
const { syncDesktopIconSizeAsync, updateDesktopIconSizeAsync } = desktopStore
|
||||
|
||||
// Icon size preset options
|
||||
const iconSizePresetOptions = [
|
||||
{
|
||||
label: t('desktopGrid.iconSize.presets.small'),
|
||||
value: DesktopIconSizePreset.small,
|
||||
},
|
||||
{
|
||||
label: t('desktopGrid.iconSize.presets.medium'),
|
||||
value: DesktopIconSizePreset.medium,
|
||||
},
|
||||
{
|
||||
label: t('desktopGrid.iconSize.presets.large'),
|
||||
value: DesktopIconSizePreset.large,
|
||||
},
|
||||
{
|
||||
label: t('desktopGrid.iconSize.presets.extraLarge'),
|
||||
value: DesktopIconSizePreset.extraLarge,
|
||||
},
|
||||
]
|
||||
|
||||
// Watch for icon size preset changes and update DB
|
||||
watch(iconSizePreset, async (newPreset) => {
|
||||
if (newPreset) {
|
||||
await updateDesktopIconSizeAsync(newPreset)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await readDeviceNameAsync()
|
||||
await syncDesktopIconSizeAsync()
|
||||
})
|
||||
|
||||
const onUpdateDeviceNameAsync = async () => {
|
||||
@ -295,6 +343,22 @@ de:
|
||||
label: Hintergrund entfernen
|
||||
success: Hintergrund erfolgreich entfernt
|
||||
error: Fehler beim Entfernen des Hintergrunds
|
||||
desktopGrid:
|
||||
title: Desktop-Raster
|
||||
columns:
|
||||
label: Spalten
|
||||
unit: Spalten
|
||||
rows:
|
||||
label: Zeilen
|
||||
unit: Zeilen
|
||||
iconSize:
|
||||
label: Icon-Größe
|
||||
presets:
|
||||
small: Klein
|
||||
medium: Mittel
|
||||
large: Groß
|
||||
extraLarge: Sehr groß
|
||||
unit: px
|
||||
en:
|
||||
language: Language
|
||||
design: Design
|
||||
@ -322,4 +386,20 @@ en:
|
||||
label: Remove Background
|
||||
success: Background successfully removed
|
||||
error: Error removing background
|
||||
desktopGrid:
|
||||
title: Desktop Grid
|
||||
columns:
|
||||
label: Columns
|
||||
unit: columns
|
||||
rows:
|
||||
label: Rows
|
||||
unit: rows
|
||||
iconSize:
|
||||
label: Icon Size
|
||||
presets:
|
||||
small: Small
|
||||
medium: Medium
|
||||
large: Large
|
||||
extraLarge: Extra Large
|
||||
unit: px
|
||||
</i18n>
|
||||
|
||||
@ -26,10 +26,10 @@
|
||||
>
|
||||
<!-- Left: Icon -->
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
<HaexIcon
|
||||
v-if="icon"
|
||||
:src="icon"
|
||||
:alt="title"
|
||||
:name="icon"
|
||||
:tooltip="title"
|
||||
class="w-5 h-5 object-contain shrink-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -25,17 +25,70 @@
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Window Icons Preview -->
|
||||
<div
|
||||
v-if="workspaceWindows.length > 0"
|
||||
class="flex flex-wrap gap-2 items-center"
|
||||
>
|
||||
<!-- Show first 8 window icons -->
|
||||
<HaexIcon
|
||||
v-for="window in visibleWindows"
|
||||
:key="window.id"
|
||||
:name="window.icon || 'i-heroicons-window'"
|
||||
:tooltip="window.title"
|
||||
class="size-6 opacity-70"
|
||||
/>
|
||||
|
||||
<!-- Show remaining count badge if more than 8 windows -->
|
||||
<UBadge
|
||||
v-if="remainingCount > 0"
|
||||
color="neutral"
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
>
|
||||
+{{ remainingCount }}
|
||||
</UBadge>
|
||||
</div>
|
||||
|
||||
<!-- Empty state when no windows -->
|
||||
<div
|
||||
v-else
|
||||
class="text-sm text-gray-400 dark:text-gray-600 italic"
|
||||
>
|
||||
{{ t('noWindows') }}
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ workspace: IWorkspace }>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const windowManager = useWindowManagerStore()
|
||||
|
||||
const { currentWorkspace } = storeToRefs(workspaceStore)
|
||||
|
||||
// Get all windows for this workspace
|
||||
const workspaceWindows = computed(() => {
|
||||
return windowManager.windows.filter(
|
||||
(window) => window.workspaceId === props.workspace.id,
|
||||
)
|
||||
})
|
||||
|
||||
// Limit to 8 visible icons
|
||||
const MAX_VISIBLE_ICONS = 8
|
||||
const visibleWindows = computed(() => {
|
||||
return workspaceWindows.value.slice(0, MAX_VISIBLE_ICONS)
|
||||
})
|
||||
|
||||
// Count remaining windows
|
||||
const remainingCount = computed(() => {
|
||||
const remaining = workspaceWindows.value.length - MAX_VISIBLE_ICONS
|
||||
return remaining > 0 ? remaining : 0
|
||||
})
|
||||
|
||||
const cardEl = useTemplateRef('cardEl')
|
||||
const isDragOver = ref(false)
|
||||
|
||||
@ -96,3 +149,10 @@ watch(
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
noWindows: Keine Fenster geöffnet
|
||||
en:
|
||||
noWindows: No windows open
|
||||
</i18n>
|
||||
|
||||
54
src/components/haex/workspace/drawer.vue
Normal file
54
src/components/haex/workspace/drawer.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<UDrawer
|
||||
v-model:open="isOverviewMode"
|
||||
direction="left"
|
||||
:overlay="false"
|
||||
:modal="false"
|
||||
title="Workspaces"
|
||||
description="Workspaces"
|
||||
>
|
||||
<template #content>
|
||||
<div class="py-8 pl-8 pr-4 h-full overflow-y-auto">
|
||||
<!-- Workspace Cards -->
|
||||
<div class="flex flex-col gap-3">
|
||||
<HaexWorkspaceCard
|
||||
v-for="workspace in workspaces"
|
||||
:key="workspace.id"
|
||||
:workspace
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Add New Workspace Button -->
|
||||
<UButton
|
||||
block
|
||||
variant="outline"
|
||||
class="mt-6"
|
||||
icon="i-heroicons-plus"
|
||||
:label="t('add')"
|
||||
@click="handleAddWorkspaceAsync"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n()
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const { workspaces, isOverviewMode } = storeToRefs(workspaceStore)
|
||||
|
||||
const handleAddWorkspaceAsync = async () => {
|
||||
const workspace = await workspaceStore.addWorkspaceAsync()
|
||||
nextTick(() => {
|
||||
workspaceStore.slideToWorkspace(workspace?.id)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
add: Workspace hinzufügen
|
||||
en:
|
||||
add: Add Workspace
|
||||
</i18n>
|
||||
Reference in New Issue
Block a user