Files
OSIT-AE-App-Svelte/src/routes/events/[event_id]/(launcher)/launcher_session_view.svelte
Scott Idem 5f57d81ead fix(launcher): stabilize session header height to prevent bouncing
Root cause: flex-row flex-wrap on the session header caused the datetime
and name to compete for the same row. Long session names (up to 300 chars)
wrapped onto 2-3 lines while short names stayed 1 line, making the header
jump in height every time the operator switched sessions.

Fix:
- header: flex-row flex-wrap -> flex-col; datetime and name are now
  always on separate rows, header height is predictable in both cases
- h2 name: shrink -> grow line-clamp-2 min-w-0; height is always exactly
  2 lines, never less, never more; full text accessible via title attribute
- code badge: added shrink-0 so it is never squeezed by a long name
- removed justify-between/justify-end conditional classes (no longer relevant)
- Section 508: title attribute on h2 provides full text for screen readers
2026-03-06 20:37:23 -05:00

476 lines
22 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
interface Props {
slct__event_session_id?: string | null;
log_lvl?: number;
}
let {
slct__event_session_id = $bindable(null),
log_lvl = $bindable(1)
}: Props = $props();
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
import Event_launcher_file_cont from './launcher_file_cont.svelte';
import Launcher_presentation_view from './launcher_presentation_view.svelte';
import Launcher_presenter_view from './launcher_presenter_view.svelte';
import Launcher_presenter_view_posters from './launcher_presenter_view_posters.svelte';
import { liveQuery } from 'dexie';
// import { core_func } from '$lib/ae_core_functions';
// import { db_core } from "$lib/db_core";
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events';
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events_functions';
// Event Session (Main View Trigger)
// WHY: We use a simple derived observable. The template handles the $ prefix.
let lq__event_session_obj = $derived(
liveQuery(() => db_events.session.get(slct__event_session_id))
);
// WHY: type_code drives poster vs. oral UI branching throughout this component.
// It was previously a prop that was never passed by the parent, so all poster
// code paths were silently dead. Deriving it here from the session object
// ensures it always reflects the current session.
let type_code = $derived($lq__event_session_obj?.type_code ?? '');
// Event File (for a Session)
// WHY: Pure data retrieval. Side effects (updating global stores) are removed
// to prevent circular reactivity loops during rapid navigation.
let lq__event_file_obj_li = $derived(
liveQuery(async () => {
if (!slct__event_session_id) return [];
if (log_lvl > 1) {
console.log(`[LQ] Fetching files for session: ${slct__event_session_id}`);
}
return await db_events.file
.where('for_id')
.equals(slct__event_session_id)
.reverse()
.sortBy('created_on');
})
);
// Event Presentation
let lq__event_presentation_obj_li = $derived(
liveQuery(async () => {
if (!slct__event_session_id) return [];
if (log_lvl > 1) {
console.log(`[LQ] Fetching presentations for session: ${slct__event_session_id}`);
}
let sort_by = 'start_datetime';
if (type_code == 'poster') {
sort_by = 'name';
}
return await db_events.presentation
.where('event_session_id')
.equals(slct__event_session_id)
.sortBy(sort_by);
})
);
// Event Presenter
let lq__event_presenter_obj_li = $derived(
liveQuery(async () => {
if (!slct__event_session_id) return [];
if (log_lvl > 1) {
console.log(`[LQ] Fetching presenters for session: ${slct__event_session_id}`);
}
return await db_events.presenter
.where('event_session_id')
.equals(slct__event_session_id)
.sortBy('full_name');
})
);
// let show_modal_upload_files: boolean = false;
// let link_to_type: null|string = null;
// let link_to_id: null|string = null;
let ae_promises: key_val = $state({});
// $events_slct.id_li__event_presenter = [];
// await tick();
// ae_promises[slct__event_session_id] = events_func.load_ae_obj_li__event_presenter({
// api_cfg: $ae_api,
// for_obj_type: 'event_session',
// for_obj_id: slct__event_session_id,
// // inc_file_li: false,
// params: {qry__enabled: 'enabled', qry__limit: 550},
// try_cache: true,
// log_lvl: 1,
// })
// .then(async function (load_results) {
// console.log(`load_results = `, load_results);
// // let event_presenter_id_li = [];
// // let tmp_li = []; // This is to prevent the array from constantly updating and triggering the liveQuery.
// // for (let i = 0; i < load_results.length; i++) {
// // let event_presenter_obj = load_results[i];
// // let event_presenter_id = event_presenter_obj.event_presenter_id;
// // tmp_li.push(event_presenter_id);
// // }
// // event_presenter_id_li = tmp_li;
// // console.log(`event_presenter_id_li:`, event_presenter_id_li);
// // $events_slct.id_li__event_presenter = event_presenter_id_li;
// return load_results;
// });
</script>
<div
class="
event_launcher_session_view
grow h-full w-full
space-y-1
relative
"
>
<!-- <slot name="event_session_message">event session message</slot> -->
{#if $events_sess.launcher.loading__session_id_status}
<span class="absolute top-0 right-0 text-sm text-center text-gray-400">
<span class="fas fa-spinner fa-spin"></span>
Loading session information...
</span>
<!-- {:else}
<span class="absolute top-0 right-0 text-sm text-center text-gray-400">
Session loaded?
</span> -->
{/if}
{#if $lq__event_session_obj && $lq__event_session_obj.event_session_id}
<!--
Session header: flex-col keeps datetime and name on separate rows so
the header height is predictable regardless of session name length.
Long names (300+ chars) are clamped to 2 lines; short names never
collapse the header below that height. Zero layout shift between sessions.
-->
<header
class="event_session_about border-b-2 border-gray-400 dark:border-gray-600 flex flex-col gap-0.5 items-stretch"
>
<h3
class:hidden={!$lq__event_session_obj?.start_datetime ||
$events_loc.launcher.hide__session_datetimes}
class="event_session_datetimes text-sm text-center"
>
<button
type="button"
onclick={() => {
if (
$events_loc.launcher.time_format == 'time_12_short'
) {
// $events_loc.launcher.datetime_format = 'datetime_long';
$events_loc.launcher.time_format = 'time_short';
$events_loc.launcher.time_hours = 24;
} else {
$events_loc.launcher.time_format = 'time_12_short';
// $events_loc.launcher.datetime_format = 'datetime_12_long';
$events_loc.launcher.time_hours = 12;
}
}}
>
<strong
>{ae_util.iso_datetime_formatter(
$lq__event_session_obj.start_datetime,
'week_long'
)}</strong
>
<span class="font-normal">
{ae_util.iso_datetime_formatter(
$lq__event_session_obj.start_datetime,
'date_long_month_day'
)}
</span>
<strong
>{ae_util.iso_datetime_formatter(
$lq__event_session_obj.start_datetime,
$events_loc.launcher.time_format
)}</strong
>
<span class="font-normal">
{ae_util.iso_datetime_formatter(
$lq__event_session_obj.end_datetime,
$events_loc.launcher.time_format
)}
</span>
</button>
</h3>
<span
class="w-full flex flex-row gap-2 items-center justify-between"
>
<!-- grow + line-clamp-2 = stable 2-line max; title provides full text for screen readers + hover -->
<h2
class="grow text-xl line-clamp-2 min-w-0"
title={`Name: ${$lq__event_session_obj.name}\nType: ${$lq__event_session_obj.type_code} \nCode: ${$lq__event_session_obj.code} \nID: ${$lq__event_session_obj.event_session_id} \nStart Date/Time: ${ae_util.iso_datetime_formatter($lq__event_session_obj.start_datetime, 'week_long')} ${ae_util.iso_datetime_formatter($lq__event_session_obj.start_datetime, $events_loc.launcher.time_format)} \nEnd Date/Time: ${ae_util.iso_datetime_formatter($lq__event_session_obj.end_datetime, $events_loc.launcher.time_format)}`}
>
{$lq__event_session_obj?.name}
</h2>
{#if $lq__event_session_obj?.code}
<!-- shrink-0: code badge never gets squeezed by a long name -->
<span
class="shrink-0 text-base text-gray-500 font-normal p-1"
title="Session code {$lq__event_session_obj.code}"
>
<span class="fas fa-barcode"></span>
{$lq__event_session_obj?.code}
</span>
{/if}
</span>
</header>
<!-- <section class="event_session_description text-xs" class:d_none="{hide_description}">
{@html $lq__event_session_obj.description}
</section> -->
{#if $lq__event_session_obj?.file_count_all === 0}
<p class="text-2xl text-center text-red-500 font-bold">
<span class="fas fa-exclamation-triangle"></span>
Warning
<span class="fas fa-exclamation-triangle"></span>
<br />
No files available show for this session.
</p>
{/if}
{#if $lq__event_file_obj_li && $lq__event_file_obj_li.length}
<section class="event_session_file_list">
<div>
<div class="text-xs text-surface-600-400">
<strong>
<span class="fas fa-file-archive"></span>
Session Files:
<span
class:hidden={!$ae_loc.trusted_access ||
!$ae_loc.edit_mode}
>
({$lq__event_file_obj_li?.length}&times;)
</span>
</strong>
</div>
<!-- {#if $ae_loc.trusted_access || $events_loc.launcher.trusted_access}
<button type="button" on:click={async () => {
show_modal_upload_files = true;
link_to_type = 'event_session';
link_to_id = $lq__event_session_obj.event_session_id;
}}
type="button" class="ae_btn btn_outline_warning btn_xs" title="Upload updated or additional files"
>
<span class="fas fa-upload"></span> Upload Session File(s)
</button>
{/if} -->
</div>
<ul class="space-y-1">
{#each $lq__event_file_obj_li as event_file_obj (event_file_obj.event_file_id)}
<li
class="flex flex-row flex-wrap gap-1 items-center justify-center"
class:hidden={!$events_loc.launcher
.show_content__hidden_files &&
event_file_obj.hide}
>
<Event_launcher_file_cont
event_file_id={event_file_obj.event_file_id}
{event_file_obj}
hide_created_on={true}
show_bak_download={$ae_loc.trusted_access &&
$ae_loc.edit_mode}
session_type={event_file_obj?.event_session_type_code ??
'oral'}
open_method={event_file_obj?.event_session_type_code ==
'poster'
? 'modal'
: null}
modal_title={$lq__event_session_obj?.name}
bind:modal__title={
$events_sess.launcher.modal__title
}
bind:modal__open_event_file_id={
$events_sess.launcher
.modal__open_event_file_id
}
bind:modal__event_file_obj={
$events_sess.launcher.modal__event_file_obj
}
/>
<!-- <Launcher_file_cont {event_file_obj} hide_created_on={false} show_bak_download={($ae_loc.trusted_access || $events_loc.launcher.trusted_access)} open_file_as={$lq__event_session_obj.type_code} poster_title={$lq__event_session_obj.title} /> -->
<!-- <a
href="{$ae_api.base_url}/event/file/{event_file_obj.event_file_id}/download?filename={event_file_obj.filename}&key={$ae_api.account_id}"
class="btn btn-sm variant-soft-secondary m-0.5 *:hover:inline"
class:hidden={!ae_tmp.show__direct_download}
title={`Download this file:\n${event_file_obj.filename}\n[API] SHA256: ${event_file_obj.hash_sha256.slice(0, 10)}... Hosted ID: ${event_file_obj.hosted_file_id} Event File ID: ${event_file_obj.event_file_id}`}
>
<span class="fas fa-download mx-1"></span>
<div class="hidden">
Download
</div>
</a> -->
</li>
{/each}
</ul>
</section>
{/if}
<!-- <hr class="w-full border border-gray-200" /> -->
<section class="event_presentation_list">
<!-- {$lq__event_session_obj?.event_presentation_li?.length ?? 'loading...?'} -->
{#if $lq__event_presentation_obj_li}
<div class="text-xs text-surface-600-400">
<strong>
{#if type_code == 'poster'}
<span class="fas fa-image"></span>
Posters:
{:else}
<span class="fas fa-tv"></span>
Presentations:
{/if}
{#if $ae_loc.administrator_access && $ae_loc.edit_mode}
({$lq__event_presentation_obj_li?.length}&times;)
{/if}
</strong>
</div>
<!-- Maybe set max with? max-w-(--breakpoint-md) -->
<ul class="event_presentation_list max-w-full space-y-2">
{#each $lq__event_presentation_obj_li as event_presentation_obj (event_presentation_obj.event_presentation_id)}
<li
class="border-b-2 border-gray-300 dark:border-gray-700 my-1 py-1 text-center md:text-left"
>
<!-- The presentation information -->
<div
class="event_presentation_datetime_name flex flex-row justify-evenly gap-4"
>
<!-- <div class="event_presentation_datetime_name"> -->
{#if event_presentation_obj?.start_datetime}
<span class="event_presentation_datetime"
><strong
>{ae_util.iso_datetime_formatter(
event_presentation_obj?.start_datetime,
'time_12_short_no_leading'
)}</strong
></span
>
{/if}
<span class="event_presentation_name grow"
>{event_presentation_obj?.name}</span
>
<!-- </div> -->
<!-- Yes, this is kind of inefficient, but it works for now. -->
{#if $lq__event_presenter_obj_li && type_code == 'poster'}
{#each $lq__event_presenter_obj_li as event_presenter_obj, index (event_presenter_obj.event_presenter_id)}
{#if event_presenter_obj.event_presentation_id == event_presentation_obj.event_presentation_id}
<span
class="event_presentation_single_presenter italic text-sm text-gray-500"
>
{#if $lq__event_presenter_obj_li[index]?.given_name && $lq__event_presenter_obj_li[index]?.given_name != 'Group'}
<span class="fas fa-user"
></span>
{$lq__event_presenter_obj_li[
index
]?.full_name}
{:else if $lq__event_presenter_obj_li[index]?.given_name == 'Group'}
<span class="fas fa-users"
></span>
{$lq__event_presenter_obj_li[
index
]?.affiliations}
{:else}
--not set--
{/if}
</span>
{/if}
{/each}
{/if}
</div>
<!-- Presentation-level files -->
<Launcher_presentation_view
lq__event_presentation_obj={event_presentation_obj}
session_type={type_code}
/>
<!-- The presenter list -->
<!-- WHY: In poster mode, presenter names are already shown inline
in the presentation header above, so hide_name=true.
We still render Launcher_presenter_view_posters here because
some events store files at the PRESENTER level (for_id=event_presenter_id)
rather than the presentation level — particularly group/company presenters.
The component renders nothing if there are no presenter-level files,
so this has no visual cost for events that use presentation-level files. -->
{#if $lq__event_presenter_obj_li && $lq__event_presenter_obj_li.length}
<ul class="event_presentation_presenter_list">
{#each $lq__event_presenter_obj_li as event_presenter_obj (event_presenter_obj.event_presenter_id)}
{#if event_presenter_obj.event_presentation_id == event_presentation_obj.event_presentation_id}
<li
class="
border border-transparent
rounded-lg
hover:bg-surface-100-900
hover:border-surface-400-600
p-1
transition-all
"
>
{#if type_code == 'poster'}
<Launcher_presenter_view_posters
lq__event_presenter_obj={event_presenter_obj}
hide_name={true}
/>
{:else}
<Launcher_presenter_view
lq__event_presenter_obj={event_presenter_obj}
/>
{/if}
</li>
{/if}
{/each}
</ul>
{/if}
</li>
{/each}
</ul>
{:else}
<p>No presentations available to display.</p>
{/if}
</section>
{:else}
<span class="fas fa-spinner fa-spin"></span>
No session selected
{/if}
</div>
<style>
</style>