mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(ui): render the Recent imagery readout as components
createRecentImageryPanel() computes the readout from the layer snapshot and renders RecentImageryReadout and its day cards synchronously; its delegated listeners stay on the stable root and the mode buttons keep their segmented control. Rail card actions take optional pressed and reserved states, replacing the owner's patches after render. Thumbnails, focus, scroll preservation, the DETAILS reveal and export behave as before, and nothing paints after teardown.
This commit is contained in:
1 parent
a318376203
commit
e949584c0a
7 files changed
+425
-398
No files matched your search
@@ -0,0 +1,67 @@
|
||||
<script setup>
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
// One imagery day in the strip. The owner computes every value; clicks reach
|
||||
// it through the readout's delegated listener.
|
||||
defineProps({
|
||||
card: { type: Object, required: true },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:id="card.id"
|
||||
:class="[
|
||||
'ri-card',
|
||||
{
|
||||
focused: card.focused,
|
||||
pinned: card.pinned,
|
||||
preview: card.preview,
|
||||
},
|
||||
]"
|
||||
:data-key="card.key"
|
||||
role="option"
|
||||
:aria-selected="String(card.focused)"
|
||||
:aria-label="card.ariaLabel"
|
||||
><div class="ri-thumb" :data-status="card.status"
|
||||
><img
|
||||
v-if="card.image"
|
||||
class="ri-thumb-img"
|
||||
alt=""
|
||||
decoding="async"
|
||||
:src="card.image.src"
|
||||
:hidden="card.image.hidden"
|
||||
/><span
|
||||
class="ri-thumb-text"
|
||||
:hidden="Boolean(card.image && !card.image.hidden)"
|
||||
>{{ card.placeholder }}</span
|
||||
><span class="ri-card-flag" :hidden="!card.flag">{{ card.flag }}</span
|
||||
><span
|
||||
class="ri-card-start"
|
||||
:hidden="!card.start"
|
||||
:title="card.start || undefined"
|
||||
>{{ t('START HERE') }}</span
|
||||
></div
|
||||
><div class="ri-card-date">{{ card.date }}</div
|
||||
><div class="ri-card-sensor">{{ card.sensor }}</div
|
||||
><div class="ri-card-cloud">{{ card.cloud }}</div
|
||||
><div class="ri-card-chips"
|
||||
><button
|
||||
v-for="chip in card.chips"
|
||||
:key="chip.slot"
|
||||
type="button"
|
||||
:class="[
|
||||
'data-toggle-chip ri-chip',
|
||||
{ active: chip.active, 'is-reserved': chip.reserved },
|
||||
]"
|
||||
tabindex="-1"
|
||||
:data-slot="chip.slot"
|
||||
:disabled="chip.disabled"
|
||||
:aria-pressed="String(chip.active)"
|
||||
:aria-hidden="String(chip.reserved)"
|
||||
:aria-label="chip.ariaLabel"
|
||||
>{{ chip.label }}</button
|
||||
></div
|
||||
></div
|
||||
>
|
||||
</template>
|
||||
@@ -0,0 +1,156 @@
|
||||
<script setup>
|
||||
import { t } from '../../i18n/index.js';
|
||||
import RailCardBlocks from '../rail/RailCardBlocks.vue';
|
||||
import RailCards from '../rail/RailCards.vue';
|
||||
import { vRangeValue } from '../rail/rangeValue.js';
|
||||
import RecentImageryCard from './RecentImageryCard.vue';
|
||||
|
||||
// Every block exists from the start, in its final order. The owner computes
|
||||
// the view and listens on the root; the mode buttons are painted by its
|
||||
// segmented control.
|
||||
defineProps({
|
||||
view: { type: Object, required: true },
|
||||
modes: { type: Array, required: true },
|
||||
onOpenDetails: { type: Function, default: () => {} },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="recent-imagery-readout"
|
||||
:aria-label="t('Recent imagery')"
|
||||
:hidden="view.hidden"
|
||||
:data-mode="view.mode"
|
||||
><div id="ri-actions" class="ri-block ri-actions-row"
|
||||
><RailCardBlocks :blocks="view.actions" card-id="recent-imagery" /></div
|
||||
><div
|
||||
id="ri-notice"
|
||||
:class="[
|
||||
'ri-block ri-line ri-notice',
|
||||
{
|
||||
warn: view.notice.warn,
|
||||
info: view.notice.info,
|
||||
'has-action': view.notice.fit,
|
||||
},
|
||||
]"
|
||||
><span
|
||||
id="ri-notice-text"
|
||||
class="ri-notice-text"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
:title="view.notice.text"
|
||||
>{{ view.notice.text }}</span
|
||||
><button
|
||||
id="ri-zoom-in"
|
||||
:class="[
|
||||
'data-toggle-chip ri-zoom-in',
|
||||
{ 'is-reserved': !view.notice.fit },
|
||||
]"
|
||||
type="button"
|
||||
:title="t('Fly in until the view fits the 1,000 km limit')"
|
||||
:disabled="!view.notice.fit"
|
||||
:aria-hidden="String(!view.notice.fit)"
|
||||
>{{ t('ZOOM IN') }}</button
|
||||
></div
|
||||
><div
|
||||
id="ri-strip"
|
||||
:class="['ri-block ri-strip', { searching: view.strip.searching }]"
|
||||
tabindex="0"
|
||||
role="listbox"
|
||||
aria-orientation="horizontal"
|
||||
:aria-label="
|
||||
t('Imagery days, newest first · arrows move, S or A and B pin')
|
||||
"
|
||||
aria-keyshortcuts="A B S"
|
||||
:aria-activedescendant="view.strip.activeDescendant || undefined"
|
||||
><RecentImageryCard
|
||||
v-for="card in view.strip.cards"
|
||||
:key="card.key"
|
||||
:card="card"
|
||||
/><div class="ri-strip-empty" :hidden="view.strip.cards.length > 0">{{
|
||||
view.strip.empty
|
||||
}}</div></div
|
||||
><div id="ri-hint" class="ri-block ri-line ri-hint" :title="view.hint">{{
|
||||
view.hint
|
||||
}}</div
|
||||
><section
|
||||
id="ri-selection"
|
||||
class="ri-block ri-selection"
|
||||
:aria-label="t('On the map')"
|
||||
:data-mode="view.mode"
|
||||
><div
|
||||
id="ri-mode"
|
||||
class="gev-segmented ri-mode"
|
||||
role="radiogroup"
|
||||
:aria-label="t('Mode')"
|
||||
><button
|
||||
v-for="[mode, label] in modes"
|
||||
:key="mode"
|
||||
class="gev-segment ri-mode-btn"
|
||||
type="button"
|
||||
:data-mode="mode"
|
||||
role="radio"
|
||||
>{{ t(label) }}</button
|
||||
></div
|
||||
><div
|
||||
v-for="slot in view.slots"
|
||||
:id="`ri-slot-${slot.id}`"
|
||||
:key="slot.id"
|
||||
:class="['ri-slot', { lit: slot.lit }]"
|
||||
:data-state="slot.state"
|
||||
><span class="ri-slot-tag">{{ slot.tag }}</span
|
||||
><span class="ri-slot-value" :title="slot.text">{{ slot.text }}</span
|
||||
><button
|
||||
:id="`ri-unpin-${slot.id}`"
|
||||
:class="[
|
||||
'data-toggle-chip ri-unpin',
|
||||
{ 'is-reserved': !slot.pinned },
|
||||
]"
|
||||
type="button"
|
||||
:data-unpin="slot.id"
|
||||
:disabled="!slot.pinned"
|
||||
:aria-hidden="String(!slot.pinned)"
|
||||
:aria-label="slot.unpinLabel"
|
||||
:title="slot.unpinLabel"
|
||||
>×</button
|
||||
></div
|
||||
></section
|
||||
><div id="ri-controls" class="ri-block ri-controls"
|
||||
><label class="ri-opacity-label" for="ri-opacity"
|
||||
><span>{{ t('OPACITY') }}</span
|
||||
><span class="ri-opacity-value">{{ view.alpha }}%</span></label
|
||||
><input
|
||||
id="ri-opacity"
|
||||
v-range-value="view.alpha"
|
||||
type="range"
|
||||
class="gev-quantitative-slider ri-opacity"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
:aria-label="t('Imagery opacity')"
|
||||
:disabled="!view.opacityEnabled" /><button
|
||||
id="ri-swap"
|
||||
:class="[
|
||||
'data-toggle-chip ri-swap',
|
||||
{
|
||||
'is-reserved': !view.swap.live,
|
||||
active: view.swap.pressed,
|
||||
},
|
||||
]"
|
||||
type="button"
|
||||
:title="t('Trade the two sides of the divider')"
|
||||
:disabled="!view.swap.live"
|
||||
:aria-hidden="String(!view.swap.live)"
|
||||
:aria-pressed="String(view.swap.pressed)"
|
||||
>{{ t('SWAP') }}</button
|
||||
><div class="ri-exports"
|
||||
><RailCardBlocks
|
||||
:blocks="view.exports"
|
||||
card-id="recent-imagery" /></div></div
|
||||
><div id="ri-details" class="ri-details"
|
||||
><RailCards
|
||||
:cards="view.details"
|
||||
card-class-name="ri-details-card"
|
||||
:on-open="onOpenDetails" /></div
|
||||
></section>
|
||||
</template>
|
||||
Reference in new issue
Block a user