Cap the dashboard at four cards, and shrink the hover map
The card track was 28rem, which fits four across a full-width page but only three once the sidebar takes its 250px - and 1920 with the sidebar is the common case here, so the board showed three. The track is 22rem now, with an explicit four-column cap above 96rem: left to auto-fit alone a wide screen reaches five, and a fifth column only makes the cards narrower until the rows they hold start truncating again. Measured at 1366, 1600, 1920 and 2560: three, four, four, four. The floor-plan preview drops from 500x385 to 390x300. At the old size it covered the row it was launched from, which is the row you are trying to read.
This commit is contained in:
@@ -120,16 +120,27 @@ defineExpose({ load })
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.dc-grid {
|
.dc-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
/* Four across on a wide screen, not five. A card holds a hostname, a machine
|
/* Four across on a wide screen. A card holds a hostname, a machine number
|
||||||
number and a state on one line; at five columns those lines truncate on
|
and a state on one line, so the track cannot go much below this without
|
||||||
the very rows that matter most. auto-fit rather than auto-fill so two
|
truncating the rows that matter most - but 28rem only fits three once the
|
||||||
cards fill the width instead of leaving three empty tracks. */
|
sidebar takes its share at 1920, which is the common case here. auto-fit
|
||||||
grid-template-columns: repeat(auto-fit, minmax(28rem, 1fr));
|
rather than auto-fill so two cards fill the width instead of leaving
|
||||||
|
empty tracks. */
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
|
||||||
max-width: 120rem;
|
max-width: 120rem;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 1.75rem;
|
margin-bottom: 1.75rem;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Four is the ceiling, not a consequence of the track width. Left to auto-fit
|
||||||
|
alone this reaches five on an ultrawide, and a fifth column buys nothing -
|
||||||
|
the cards get narrower and the rows they hold start truncating again. */
|
||||||
|
@media (min-width: 96rem) {
|
||||||
|
.dc-grid {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
.dc-card {
|
.dc-card {
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
|||||||
@@ -1,268 +1,270 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="location-tooltip-wrapper" @mouseenter="showTooltip" @mouseleave="onWrapperLeave">
|
<div class="location-tooltip-wrapper" @mouseenter="showTooltip" @mouseleave="onWrapperLeave">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div
|
<div
|
||||||
v-if="visible && hasPosition"
|
v-if="visible && hasPosition"
|
||||||
class="map-tooltip"
|
class="map-tooltip"
|
||||||
:style="tooltipStyle"
|
:style="tooltipStyle"
|
||||||
ref="tooltipRef"
|
ref="tooltipRef"
|
||||||
@mouseenter="onTooltipEnter"
|
@mouseenter="onTooltipEnter"
|
||||||
@mouseleave="onTooltipLeave"
|
@mouseleave="onTooltipLeave"
|
||||||
@wheel.prevent="onWheel"
|
@wheel.prevent="onWheel"
|
||||||
>
|
>
|
||||||
<div class="map-tooltip-content">
|
<div class="map-tooltip-content">
|
||||||
<div class="map-preview" ref="mapPreview">
|
<div class="map-preview" ref="mapPreview">
|
||||||
<div
|
<div
|
||||||
class="map-transform"
|
class="map-transform"
|
||||||
:style="transformStyle"
|
:style="transformStyle"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="blueprintUrl"
|
:src="blueprintUrl"
|
||||||
alt="Shop Floor Map"
|
alt="Shop Floor Map"
|
||||||
class="map-image"
|
class="map-image"
|
||||||
@load="onImageLoad"
|
@load="onImageLoad"
|
||||||
/>
|
/>
|
||||||
<!-- Marker dot -->
|
<!-- Marker dot -->
|
||||||
<div
|
<div
|
||||||
class="marker-dot"
|
class="marker-dot"
|
||||||
:style="markerStyle"
|
:style="markerStyle"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="map-tooltip-footer">
|
<div class="map-tooltip-footer">
|
||||||
<span class="coordinates">{{ left }}, {{ top }}</span>
|
<span class="coordinates">{{ left }}, {{ top }}</span>
|
||||||
<span class="zoom-hint">Scroll to zoom</span>
|
<span class="zoom-hint">Scroll to zoom</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, nextTick, watch } from 'vue'
|
import { ref, computed, nextTick, watch } from 'vue'
|
||||||
import { currentTheme } from '../stores/theme'
|
import { currentTheme } from '../stores/theme'
|
||||||
import { loadMapConfig, blueprintUrlFor, state as mapConfig } from '../composables/mapConfig'
|
import { loadMapConfig, blueprintUrlFor, state as mapConfig } from '../composables/mapConfig'
|
||||||
|
|
||||||
// Fetch this facility's blueprint + dimensions once; computeds below react
|
// Fetch this facility's blueprint + dimensions once; computeds below react
|
||||||
// when it loads.
|
// when it loads.
|
||||||
loadMapConfig()
|
loadMapConfig()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
left: { type: Number, default: null },
|
left: { type: Number, default: null },
|
||||||
top: { type: Number, default: null },
|
top: { type: Number, default: null },
|
||||||
machineName: { type: String, default: '' }
|
machineName: { type: String, default: '' }
|
||||||
})
|
})
|
||||||
|
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
const tooltipRef = ref(null)
|
const tooltipRef = ref(null)
|
||||||
const mapPreview = ref(null)
|
const mapPreview = ref(null)
|
||||||
const tooltipPosition = ref({ x: 0, y: 0 })
|
const tooltipPosition = ref({ x: 0, y: 0 })
|
||||||
const isOverTooltip = ref(false)
|
const isOverTooltip = ref(false)
|
||||||
const zoom = ref(1)
|
const zoom = ref(1)
|
||||||
const imageLoaded = ref(false)
|
const imageLoaded = ref(false)
|
||||||
|
|
||||||
const hasPosition = computed(() => {
|
const hasPosition = computed(() => {
|
||||||
return props.left !== null && props.top !== null
|
return props.left !== null && props.top !== null
|
||||||
})
|
})
|
||||||
|
|
||||||
const blueprintUrl = computed(() => {
|
const blueprintUrl = computed(() => {
|
||||||
// Reading currentTheme keeps this reactive to theme changes.
|
// Reading currentTheme keeps this reactive to theme changes.
|
||||||
return blueprintUrlFor(currentTheme.value)
|
return blueprintUrlFor(currentTheme.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Calculate marker position as percentage of the facility blueprint size
|
// Calculate marker position as percentage of the facility blueprint size
|
||||||
const markerX = computed(() => {
|
const markerX = computed(() => {
|
||||||
return (props.left / mapConfig.width) * 100
|
return (props.left / mapConfig.width) * 100
|
||||||
})
|
})
|
||||||
|
|
||||||
const markerY = computed(() => {
|
const markerY = computed(() => {
|
||||||
return (props.top / mapConfig.height) * 100
|
return (props.top / mapConfig.height) * 100
|
||||||
})
|
})
|
||||||
|
|
||||||
// Marker style with counter-scale to maintain constant size
|
// Marker style with counter-scale to maintain constant size
|
||||||
const markerStyle = computed(() => ({
|
const markerStyle = computed(() => ({
|
||||||
left: markerX.value + '%',
|
left: markerX.value + '%',
|
||||||
top: markerY.value + '%',
|
top: markerY.value + '%',
|
||||||
transform: `translate(-50%, -50%) scale(${1 / zoom.value})`
|
transform: `translate(-50%, -50%) scale(${1 / zoom.value})`
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Transform style that centers on the marker and zooms toward it
|
// Transform style that centers on the marker and zooms toward it
|
||||||
const transformStyle = computed(() => {
|
const transformStyle = computed(() => {
|
||||||
// Calculate translation to center the marker in the preview
|
// Calculate translation to center the marker in the preview
|
||||||
const translateX = 50 - markerX.value
|
const translateX = 50 - markerX.value
|
||||||
const translateY = 50 - markerY.value
|
const translateY = 50 - markerY.value
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transform: `translate(${translateX}%, ${translateY}%) scale(${zoom.value})`,
|
transform: `translate(${translateX}%, ${translateY}%) scale(${zoom.value})`,
|
||||||
transformOrigin: `${markerX.value}% ${markerY.value}%`
|
transformOrigin: `${markerX.value}% ${markerY.value}%`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const tooltipStyle = computed(() => ({
|
const tooltipStyle = computed(() => ({
|
||||||
left: `${tooltipPosition.value.x}px`,
|
left: `${tooltipPosition.value.x}px`,
|
||||||
top: `${tooltipPosition.value.y}px`
|
top: `${tooltipPosition.value.y}px`
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function onImageLoad() {
|
function onImageLoad() {
|
||||||
imageLoaded.value = true
|
imageLoaded.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function showTooltip(event) {
|
function showTooltip(event) {
|
||||||
if (!hasPosition.value) return
|
if (!hasPosition.value) return
|
||||||
|
|
||||||
visible.value = true
|
visible.value = true
|
||||||
zoom.value = 1
|
zoom.value = 1
|
||||||
|
|
||||||
const rect = event.target.getBoundingClientRect()
|
const rect = event.target.getBoundingClientRect()
|
||||||
tooltipPosition.value = {
|
tooltipPosition.value = {
|
||||||
x: rect.left + rect.width / 2,
|
x: rect.left + rect.width / 2,
|
||||||
y: rect.bottom + 10
|
y: rect.bottom + 10
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
adjustPosition()
|
adjustPosition()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWrapperLeave() {
|
function onWrapperLeave() {
|
||||||
// Small delay to allow moving to tooltip
|
// Small delay to allow moving to tooltip
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!isOverTooltip.value) {
|
if (!isOverTooltip.value) {
|
||||||
hideTooltip()
|
hideTooltip()
|
||||||
}
|
}
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTooltipEnter() {
|
function onTooltipEnter() {
|
||||||
isOverTooltip.value = true
|
isOverTooltip.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTooltipLeave() {
|
function onTooltipLeave() {
|
||||||
isOverTooltip.value = false
|
isOverTooltip.value = false
|
||||||
hideTooltip()
|
hideTooltip()
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideTooltip() {
|
function hideTooltip() {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
zoom.value = 1
|
zoom.value = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWheel(event) {
|
function onWheel(event) {
|
||||||
const delta = event.deltaY > 0 ? -0.3 : 0.3
|
const delta = event.deltaY > 0 ? -0.3 : 0.3
|
||||||
const newZoom = Math.max(1, Math.min(8, zoom.value + delta))
|
const newZoom = Math.max(1, Math.min(8, zoom.value + delta))
|
||||||
zoom.value = newZoom
|
zoom.value = newZoom
|
||||||
}
|
}
|
||||||
|
|
||||||
function adjustPosition() {
|
function adjustPosition() {
|
||||||
if (!tooltipRef.value) return
|
if (!tooltipRef.value) return
|
||||||
|
|
||||||
const tooltip = tooltipRef.value
|
const tooltip = tooltipRef.value
|
||||||
const rect = tooltip.getBoundingClientRect()
|
const rect = tooltip.getBoundingClientRect()
|
||||||
const viewportWidth = window.innerWidth
|
const viewportWidth = window.innerWidth
|
||||||
const viewportHeight = window.innerHeight
|
const viewportHeight = window.innerHeight
|
||||||
|
|
||||||
if (rect.right > viewportWidth - 20) {
|
if (rect.right > viewportWidth - 20) {
|
||||||
tooltipPosition.value.x -= (rect.right - viewportWidth + 20)
|
tooltipPosition.value.x -= (rect.right - viewportWidth + 20)
|
||||||
}
|
}
|
||||||
if (rect.left < 20) {
|
if (rect.left < 20) {
|
||||||
tooltipPosition.value.x += (20 - rect.left)
|
tooltipPosition.value.x += (20 - rect.left)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rect.bottom > viewportHeight - 20) {
|
if (rect.bottom > viewportHeight - 20) {
|
||||||
tooltipPosition.value.y = rect.top - tooltip.offsetHeight - 20
|
tooltipPosition.value.y = rect.top - tooltip.offsetHeight - 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset zoom when tooltip becomes visible
|
// Reset zoom when tooltip becomes visible
|
||||||
watch(visible, (newVal) => {
|
watch(visible, (newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
zoom.value = 1
|
zoom.value = 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reset imageLoaded when theme changes to force reload
|
// Reset imageLoaded when theme changes to force reload
|
||||||
watch(currentTheme, () => {
|
watch(currentTheme, () => {
|
||||||
imageLoaded.value = false
|
imageLoaded.value = false
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.location-tooltip-wrapper {
|
.location-tooltip-wrapper {
|
||||||
display: inline;
|
display: inline;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.location-tooltip-wrapper:hover {
|
.location-tooltip-wrapper:hover {
|
||||||
color: var(--primary, #1976d2);
|
color: var(--primary, #1976d2);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.map-tooltip {
|
.map-tooltip {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-tooltip-content {
|
.map-tooltip-content {
|
||||||
background: var(--bg-card, #ffffff);
|
background: var(--bg-card, #ffffff);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--border, #e0e0e0);
|
border: 1px solid var(--border, #e0e0e0);
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.25);
|
box-shadow: 0 4px 20px rgba(0,0,0,0.25);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-preview {
|
.map-preview {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 500px;
|
/* Smaller than it was: this is a glance-and-move-on preview, and at 500px it
|
||||||
height: 385px;
|
covered the row it was launched from. Aspect ratio kept. */
|
||||||
overflow: hidden;
|
width: 390px;
|
||||||
background: var(--bg, #f5f5f5);
|
height: 300px;
|
||||||
}
|
overflow: hidden;
|
||||||
|
background: var(--bg, #f5f5f5);
|
||||||
.map-transform {
|
}
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
.map-transform {
|
||||||
height: 100%;
|
position: relative;
|
||||||
transition: transform 0.15s ease-out;
|
width: 100%;
|
||||||
}
|
height: 100%;
|
||||||
|
transition: transform 0.15s ease-out;
|
||||||
.map-image {
|
}
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
.map-image {
|
||||||
object-fit: contain;
|
width: 100%;
|
||||||
}
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
.marker-dot {
|
}
|
||||||
position: absolute;
|
|
||||||
width: 16px;
|
.marker-dot {
|
||||||
height: 16px;
|
position: absolute;
|
||||||
background: #ff0000;
|
width: 16px;
|
||||||
border: 2px solid #ffffff;
|
height: 16px;
|
||||||
border-radius: 50%;
|
background: #ff0000;
|
||||||
box-shadow: 0 0 0 3px rgba(255,0,0,0.3), 0 0 10px #ff0000;
|
border: 2px solid #ffffff;
|
||||||
pointer-events: none;
|
border-radius: 50%;
|
||||||
}
|
box-shadow: 0 0 0 3px rgba(255,0,0,0.3), 0 0 10px #ff0000;
|
||||||
|
pointer-events: none;
|
||||||
.map-tooltip-footer {
|
}
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
background: var(--bg, #f5f5f5);
|
.map-tooltip-footer {
|
||||||
border-top: 1px solid var(--border, #e0e0e0);
|
padding: 0.5rem 0.75rem;
|
||||||
display: flex;
|
background: var(--bg, #f5f5f5);
|
||||||
justify-content: space-between;
|
border-top: 1px solid var(--border, #e0e0e0);
|
||||||
align-items: center;
|
display: flex;
|
||||||
}
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
.coordinates {
|
}
|
||||||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
|
||||||
font-size: 0.875rem;
|
.coordinates {
|
||||||
color: var(--text-light, #666666);
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||||
}
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-light, #666666);
|
||||||
.zoom-hint {
|
}
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-light, #666666);
|
.zoom-hint {
|
||||||
}
|
font-size: 0.75rem;
|
||||||
</style>
|
color: var(--text-light, #666666);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user