30-04-2026
This commit is contained in:
parent
761b1156c1
commit
d054732bf5
35 changed files with 2796 additions and 505 deletions
|
|
@ -8,9 +8,14 @@
|
|||
'glow-dot--label-above': labelAbove
|
||||
}"
|
||||
:style="dotStyle"
|
||||
:role="isGhost ? undefined : 'button'"
|
||||
:tabindex="isGhost ? -1 : 0"
|
||||
:aria-label="dotAriaLabel"
|
||||
@click.stop="onSelect"
|
||||
@keydown.enter.prevent.stop="onSelect"
|
||||
@keydown.space.prevent.stop="onSelect"
|
||||
>
|
||||
<div class="glow-dot__inner" :style="{ boxShadow: glowShadow }">
|
||||
<div class="glow-dot__inner" :style="innerStyle">
|
||||
<img
|
||||
v-if="imageSrc"
|
||||
:src="imageSrc"
|
||||
|
|
@ -19,6 +24,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div v-if="!isGhost && event.title" class="glow-dot__label" :style="labelStyle">
|
||||
<span class="glow-dot__connector" :style="connectorStyle"></span>
|
||||
<span class="glow-dot__title" :style="titleStyle">{{ event.title }}</span>
|
||||
<span class="glow-dot__date" :style="dateStyle">{{ formattedDate }}</span>
|
||||
</div>
|
||||
|
|
@ -43,9 +49,9 @@ const eventsStore = useEventsStore()
|
|||
const settingsStore = useSettingsStore()
|
||||
|
||||
// Resolve image: cached thumbnail from IndexedDB or fetch & cache
|
||||
const { resolvedSrc: imageSrc } = props.event.image
|
||||
? useImageCache(props.event.image, props.event.id)
|
||||
: { resolvedSrc: computed(() => null) }
|
||||
const imageUrl = computed(() => props.event.image || null)
|
||||
const eventId = computed(() => props.event.id)
|
||||
const { resolvedSrc: imageSrc } = useImageCache(imageUrl, eventId)
|
||||
|
||||
const fl = computed(() => settingsStore.floatingLines)
|
||||
const glowColor = computed(() => eventsStore.getGlowColor(props.event))
|
||||
|
|
@ -67,19 +73,42 @@ const formattedDate = computed(() => {
|
|||
const d = new Date(props.event.date)
|
||||
return `${d.getDate()}. ${MONTH_SHORT[d.getMonth()]} ${d.getFullYear()}`
|
||||
})
|
||||
const dotAriaLabel = computed(() => {
|
||||
if (props.isGhost) return undefined
|
||||
const title = props.event.title || 'Event'
|
||||
return `${title}, ${formattedDate.value}`
|
||||
})
|
||||
|
||||
// Label font sizes per setting
|
||||
const LABEL_FONT = { small: { title: 10, date: 9 }, medium: { title: 12, date: 11 }, large: { title: 14, date: 13 } }
|
||||
const LABEL_FONT = {
|
||||
small: { title: 10, date: 9 },
|
||||
medium: { title: 12, date: 11 },
|
||||
large: { title: 14, date: 13 },
|
||||
xlarge: { title: 18, date: 16 }
|
||||
}
|
||||
const labelFont = computed(() => LABEL_FONT[fl.value.labelSize] ?? LABEL_FONT.small)
|
||||
const labelColor = computed(() => fl.value.labelColor ?? '#ffffff')
|
||||
const labelOpacity = computed(() => Math.max(0.5, Math.min(1, fl.value.labelOpacity ?? 0.75)))
|
||||
const connectorLengthScale = computed(() => Math.max(0, Math.min(1, fl.value.labelConnectorLength ?? 0.2)))
|
||||
// 0 -> no connector, 1 -> ~5x current connector length (about 70px)
|
||||
const connectorLengthPx = computed(() => connectorLengthScale.value * 70)
|
||||
const labelGapPx = computed(() => 4 + connectorLengthPx.value)
|
||||
|
||||
const labelStyle = computed(() => ({
|
||||
maxWidth: labelFont.value.title >= 14 ? '120px' : '90px'
|
||||
maxWidth: labelFont.value.title >= 18 ? '150px' : labelFont.value.title >= 14 ? '120px' : '90px',
|
||||
'--label-gap': `${labelGapPx.value}px`,
|
||||
'--label-connector-len': `${connectorLengthPx.value}px`,
|
||||
'--label-opacity': labelOpacity.value.toFixed(2),
|
||||
'--label-connector-color': labelColor.value,
|
||||
'--label-connector-opacity': Math.max(0.45, labelOpacity.value * 0.9).toFixed(2)
|
||||
}))
|
||||
const connectorStyle = computed(() => ({
|
||||
display: connectorLengthPx.value <= 0.5 ? 'none' : 'block'
|
||||
}))
|
||||
const titleStyle = computed(() => ({
|
||||
fontSize: `${labelFont.value.title}px`,
|
||||
color: labelColor.value,
|
||||
maxWidth: labelFont.value.title >= 14 ? '120px' : '90px'
|
||||
maxWidth: labelFont.value.title >= 18 ? '150px' : labelFont.value.title >= 14 ? '120px' : '90px'
|
||||
}))
|
||||
const dateStyle = computed(() => ({
|
||||
fontSize: `${labelFont.value.date}px`,
|
||||
|
|
@ -93,14 +122,18 @@ const dotStyle = computed(() => ({
|
|||
height: `${dotSize.value}px`
|
||||
}))
|
||||
|
||||
// Two-layer box-shadow: tight bright core + wide soft halo
|
||||
const glowShadow = computed(() => {
|
||||
const size = fl.value.glowSize
|
||||
const innerStyle = computed(() => {
|
||||
const size = fl.value.glowSize
|
||||
const strength = fl.value.glowStrength
|
||||
const color = glowColor.value
|
||||
const core = alphaHex(Math.min(strength / 3, 1))
|
||||
const halo = alphaHex(Math.min(strength / 7, 1))
|
||||
return `0 0 ${size}px 0px ${color}${core}, 0 0 ${size * 2.5}px ${size * 0.3}px ${color}${halo}`
|
||||
const color = glowColor.value
|
||||
const core = alphaHex(Math.min(strength / 3, 1))
|
||||
const halo = alphaHex(Math.min(strength / 7, 1))
|
||||
const shadow = `0 0 ${size}px 0px ${color}${core}, 0 0 ${size * 2.5}px ${size * 0.3}px ${color}${halo}`
|
||||
const bw = fl.value.dotBorderWidth ?? 0
|
||||
return {
|
||||
boxShadow: shadow,
|
||||
border: bw > 0 ? `${bw}px solid ${fl.value.dotBorderColor ?? '#ffffff'}` : 'none'
|
||||
}
|
||||
})
|
||||
|
||||
function alphaHex(a) {
|
||||
|
|
@ -163,25 +196,41 @@ function onSelect() {
|
|||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: calc(100% + 6px);
|
||||
top: calc(100% + var(--label-gap, 18px));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
max-width: 90px;
|
||||
pointer-events: none;
|
||||
opacity: var(--label-opacity, 0.75);
|
||||
}
|
||||
|
||||
/* When dot is in lower half, show label above */
|
||||
.glow-dot--label-above .glow-dot__label {
|
||||
top: auto;
|
||||
bottom: calc(100% + 6px);
|
||||
bottom: calc(100% + var(--label-gap, 18px));
|
||||
}
|
||||
|
||||
.glow-dot__connector {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
width: 1px;
|
||||
height: var(--label-connector-len, 14px);
|
||||
top: calc(-1 * var(--label-connector-len, 14px));
|
||||
transform: translateX(-50%);
|
||||
background: var(--label-connector-color, #ffffff);
|
||||
opacity: var(--label-connector-opacity, 0.65);
|
||||
}
|
||||
|
||||
.glow-dot--label-above .glow-dot__connector {
|
||||
top: auto;
|
||||
bottom: calc(-1 * var(--label-connector-len, 14px));
|
||||
}
|
||||
|
||||
.glow-dot__title {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
@ -193,7 +242,6 @@ function onSelect() {
|
|||
.glow-dot__date {
|
||||
font-size: 9px;
|
||||
font-weight: 400;
|
||||
opacity: 0.4;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue