feat(renderer): add bounded session image element
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
defineOptions({ name: 'SessionImage' });
|
||||
|
||||
const props = defineProps({
|
||||
src: { type: String, required: true },
|
||||
alt: { type: String, default: '' },
|
||||
title: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const status = ref('loading');
|
||||
const accessibleLabel = computed(() => props.alt || props.title || 'Session image');
|
||||
|
||||
function handleLoad() {
|
||||
status.value = 'loaded';
|
||||
}
|
||||
|
||||
function handleError() {
|
||||
status.value = 'error';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<figure
|
||||
class="session-image"
|
||||
:class="`is-${status}`"
|
||||
:aria-busy="status === 'loading' ? 'true' : undefined"
|
||||
>
|
||||
<img
|
||||
v-if="status !== 'error'"
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
:title="title || undefined"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
@load="handleLoad"
|
||||
@error="handleError"
|
||||
>
|
||||
<figcaption v-else role="status">
|
||||
<span>Image unavailable</span>
|
||||
<span v-if="accessibleLabel" class="image-label">{{ accessibleLabel }}</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
max-inline-size: 100%;
|
||||
min-inline-size: 0;
|
||||
margin: 0.7em 0;
|
||||
contain: inline-size;
|
||||
}
|
||||
|
||||
.session-image {
|
||||
max-inline-size: 100%;
|
||||
min-inline-size: 0;
|
||||
margin: 0;
|
||||
overflow: clip;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 6px;
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
inline-size: auto;
|
||||
max-inline-size: 100%;
|
||||
block-size: auto;
|
||||
max-block-size: min(70vh, 720px);
|
||||
object-fit: contain;
|
||||
color: rgba(255, 255, 255, 0.48);
|
||||
}
|
||||
|
||||
.is-loading {
|
||||
min-block-size: 48px;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
display: flex;
|
||||
min-block-size: 48px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 12px;
|
||||
color: rgba(255, 255, 255, 0.48);
|
||||
font: 12px/1.5 ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
}
|
||||
|
||||
.image-label::before {
|
||||
content: '·';
|
||||
margin-inline-end: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ import router from './router.js';
|
||||
import { commitInitialData, fetchInitialData } from './data.js';
|
||||
import { noteSessionUpdated, sessionLiveState } from './session-live.mjs';
|
||||
import { createGlobalDataRefreshCoordinator } from './session-global-refresh.mjs';
|
||||
import { registerSessionImageElement } from './session-image-element.js';
|
||||
|
||||
// Import shared renderer CSS globally
|
||||
import '../styles/base.css';
|
||||
@@ -14,6 +15,8 @@ import '../styles/toolbar.css';
|
||||
import '../styles/list.css';
|
||||
import '../styles/detail.css';
|
||||
|
||||
registerSessionImageElement();
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(router);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineCustomElement } from 'vue';
|
||||
import SessionImage from './components/SessionImage.ce.vue';
|
||||
|
||||
export const SESSION_IMAGE_TAG = 'obelisk-session-image';
|
||||
|
||||
export function registerSessionImageElement() {
|
||||
if (customElements.get(SESSION_IMAGE_TAG)) return;
|
||||
customElements.define(SESSION_IMAGE_TAG, defineCustomElement(SessionImage));
|
||||
}
|
||||
Reference in New Issue
Block a user