Prettier for Event Launcher

This commit is contained in:
Scott Idem
2026-03-24 12:13:59 -04:00
parent a3ed379b17
commit 7f6e286b73
27 changed files with 3766 additions and 3807 deletions

View File

@@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
/** /**
* events/[event_id]/(launcher)/+layout.svelte * events/[event_id]/(launcher)/+layout.svelte
* Root layout for the launcher area. * Root layout for the launcher area.
* Ensures background sync runs globally regardless of active tab. * Ensures background sync runs globally regardless of active tab.
*/ */
// import { ae_loc } from '$lib/stores/ae_stores'; // import { ae_loc } from '$lib/stores/ae_stores';
import Launcher_Background_Sync from './launcher_background_sync.svelte'; import Launcher_Background_Sync from './launcher_background_sync.svelte';
interface Props { interface Props {
children?: import('svelte').Snippet; children?: import('svelte').Snippet;
} }
let { children }: Props = $props(); let { children }: Props = $props();
</script> </script>
<!-- Background Sync Process (Invisible) --> <!-- Background Sync Process (Invisible) -->

View File

@@ -1,55 +1,56 @@
<script lang="ts"> <script lang="ts">
import { ae_loc } from '$lib/stores/ae_stores'; import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores'; import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte'; import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { Clock, GraduationCap, IdCard, LayoutGrid } from '@lucide/svelte'; import { Clock, GraduationCap, IdCard, LayoutGrid } from '@lucide/svelte';
interface Props { interface Props {
on_expand?: () => void; on_expand?: () => void;
} }
let { on_expand }: Props = $props(); let { on_expand }: Props = $props();
// Poster Kiosk mode presets: // Poster Kiosk mode presets:
// iframe=true hides global site chrome // iframe=true hides global site chrome
// hide__launcher_menu/header/footer removes all launcher panels // hide__launcher_menu/header/footer removes all launcher panels
// Oral/Default restores everything. // Oral/Default restores everything.
// WHY: A single tap lets event staff reconfigure a device (or all WS-connected // WHY: A single tap lets event staff reconfigure a device (or all WS-connected
// remote devices) for a session type without touching individual toggles. // remote devices) for a session type without touching individual toggles.
const POSTER_PRESET = { const POSTER_PRESET = {
iframe: true, iframe: true,
hide__launcher_menu: true, hide__launcher_menu: true,
hide__launcher_header: true, hide__launcher_header: true,
hide__launcher_footer: true hide__launcher_footer: true
}; };
const ORAL_PRESET = { const ORAL_PRESET = {
iframe: false, iframe: false,
hide__launcher_menu: false, hide__launcher_menu: false,
hide__launcher_header: false, hide__launcher_header: false,
hide__launcher_footer: false hide__launcher_footer: false
}; };
// Detect current mode: if both iframe AND hide_menu are on, we're in poster mode. // Detect current mode: if both iframe AND hide_menu are on, we're in poster mode.
// Individual overrides are still possible via the checkboxes below. // Individual overrides are still possible via the checkboxes below.
let is_poster_mode = $derived( let is_poster_mode = $derived(
$ae_loc.iframe === true && $events_loc.launcher.hide__launcher_menu === true $ae_loc.iframe === true && $events_loc.launcher.hide__launcher_menu === true
); );
function apply_mode(mode: 'poster' | 'oral') { function apply_mode(mode: 'poster' | 'oral') {
const preset = mode === 'poster' ? POSTER_PRESET : ORAL_PRESET; const preset = mode === 'poster' ? POSTER_PRESET : ORAL_PRESET;
$ae_loc.iframe = preset.iframe; $ae_loc.iframe = preset.iframe;
$events_loc.launcher.hide__launcher_menu = preset.hide__launcher_menu; $events_loc.launcher.hide__launcher_menu = preset.hide__launcher_menu;
$events_loc.launcher.hide__launcher_header = preset.hide__launcher_header; $events_loc.launcher.hide__launcher_header = preset.hide__launcher_header;
$events_loc.launcher.hide__launcher_footer = preset.hide__launcher_footer; $events_loc.launcher.hide__launcher_footer = preset.hide__launcher_footer;
// Push to WS-connected remote devices when we're acting as a controller. // Push to WS-connected remote devices when we're acting as a controller.
// Only send when connected so the UI button doesn't silently no-op. // Only send when connected so the UI button doesn't silently no-op.
if ( if (
$events_loc.launcher.ws_connect && $events_loc.launcher.ws_connect &&
($events_loc.launcher.controller === 'local_push' || $events_loc.launcher.controller === 'remote') ($events_loc.launcher.controller === 'local_push' ||
) { $events_loc.launcher.controller === 'remote')
$events_sess.launcher.controller_cmd = `ae_mode:${mode}`; ) {
$events_sess.launcher.controller_trigger_send = 'trigger'; $events_sess.launcher.controller_cmd = `ae_mode:${mode}`;
} $events_sess.launcher.controller_trigger_send = 'trigger';
} }
}
</script> </script>
<Launcher_Cfg_Section <Launcher_Cfg_Section
@@ -57,135 +58,126 @@
icon={LayoutGrid} icon={LayoutGrid}
bind:state={$events_loc.launcher.section_state__app_modes} bind:state={$events_loc.launcher.section_state__app_modes}
{on_expand} {on_expand}
description="Mode: {$events_loc.launcher.app_mode} | UI Layout" description="Mode: {$events_loc.launcher.app_mode} | UI Layout">
>
<!-- Content omitted for brevity, preserved in file --> <!-- Content omitted for brevity, preserved in file -->
<div class="col-span-full flex flex-col gap-3"> <div class="col-span-full flex flex-col gap-3">
<!-- 0. Oral / Poster Kiosk Mode Preset Toggle --> <!-- 0. Oral / Poster Kiosk Mode Preset Toggle -->
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1">Session Mode Preset</p> <p class="ml-1 text-[9px] font-bold uppercase opacity-50">
<div class="grid grid-cols-2 gap-1 bg-surface-500/5 p-1 rounded-lg"> Session Mode Preset
</p>
<div class="bg-surface-500/5 grid grid-cols-2 gap-1 rounded-lg p-1">
<button <button
type="button" type="button"
onclick={() => apply_mode('oral')} onclick={() => apply_mode('oral')}
class="btn btn-xs font-bold text-[10px]" class="btn btn-xs text-[10px] font-bold"
class:preset-filled-secondary={!is_poster_mode} class:preset-filled-secondary={!is_poster_mode}
class:preset-tonal-surface={is_poster_mode} class:preset-tonal-surface={is_poster_mode}
title="Standard oral/presentation layout — menus and headers visible" title="Standard oral/presentation layout — menus and headers visible">
>
<GraduationCap size="0.85em" class="mr-1 opacity-70" /> <GraduationCap size="0.85em" class="mr-1 opacity-70" />
Oral / Default Oral / Default
</button> </button>
<button <button
type="button" type="button"
onclick={() => apply_mode('poster')} onclick={() => apply_mode('poster')}
class="btn btn-xs font-bold text-[10px]" class="btn btn-xs text-[10px] font-bold"
class:preset-filled-primary={is_poster_mode} class:preset-filled-primary={is_poster_mode}
class:preset-tonal-surface={!is_poster_mode} class:preset-tonal-surface={!is_poster_mode}
title="Digital Poster kiosk — hides site chrome, menu, header & footer" title="Digital Poster kiosk — hides site chrome, menu, header & footer">
>
<IdCard size="0.85em" class="mr-1 opacity-70" /> <IdCard size="0.85em" class="mr-1 opacity-70" />
Poster Kiosk Poster Kiosk
</button> </button>
</div> </div>
{#if $events_loc.launcher.ws_connect && ($events_loc.launcher.controller === 'local_push' || $events_loc.launcher.controller === 'remote')} {#if $events_loc.launcher.ws_connect && ($events_loc.launcher.controller === 'local_push' || $events_loc.launcher.controller === 'remote')}
<p class="text-[8px] opacity-40 italic ml-1">Applies to all connected WS devices</p> <p class="ml-1 text-[8px] italic opacity-40">
Applies to all connected WS devices
</p>
{/if} {/if}
</div> </div>
<!-- 1. App Mode Selection --> <!-- 1. App Mode Selection -->
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1" <p class="ml-1 text-[9px] font-bold uppercase opacity-50">
>Operational Environment</p Operational Environment
> </p>
<div class="grid grid-cols-3 gap-1 bg-surface-500/5 p-1 rounded-lg"> <div class="bg-surface-500/5 grid grid-cols-3 gap-1 rounded-lg p-1">
<button <button
type="button" type="button"
onclick={() => ($events_loc.launcher.app_mode = 'default')} onclick={() => ($events_loc.launcher.app_mode = 'default')}
class="btn btn-xs text-[9px] font-bold" class="btn btn-xs text-[9px] font-bold"
class:preset-filled-primary={$events_loc.launcher class:preset-filled-primary={$events_loc.launcher
.app_mode === 'default'} .app_mode === 'default'}
class:preset-tonal-surface={$events_loc.launcher.app_mode !== class:preset-tonal-surface={$events_loc.launcher
'default'} .app_mode !== 'default'}
title="Default standard web browser (Chromium, Firefox, Safari based) launcher, for remote presenters and testing before being onsite"> title="Default standard web browser (Chromium, Firefox, Safari based) launcher, for remote presenters and testing before being onsite">
Web</button Web</button>
>
<button <button
type="button" type="button"
onclick={() => ($events_loc.launcher.app_mode = 'native')} onclick={() => ($events_loc.launcher.app_mode = 'native')}
class="btn btn-xs text-[9px] font-bold" class="btn btn-xs text-[9px] font-bold"
class:preset-filled-primary={$events_loc.launcher class:preset-filled-primary={$events_loc.launcher
.app_mode === 'native'} .app_mode === 'native'}
class:preset-tonal-surface={$events_loc.launcher.app_mode !== class:preset-tonal-surface={$events_loc.launcher
'native'} .app_mode !== 'native'}
title="Native Electron based app launcher, for onsite presenters in session rooms">App</button title="Native Electron based app launcher, for onsite presenters in session rooms"
> >App</button>
<button <button
type="button" type="button"
onclick={() => ($events_loc.launcher.app_mode = 'onsite')} onclick={() => ($events_loc.launcher.app_mode = 'onsite')}
class="btn btn-xs text-[9px] font-bold" class="btn btn-xs text-[9px] font-bold"
class:preset-filled-primary={$events_loc.launcher class:preset-filled-primary={$events_loc.launcher
.app_mode === 'onsite'} .app_mode === 'onsite'}
class:preset-tonal-surface={$events_loc.launcher.app_mode !== class:preset-tonal-surface={$events_loc.launcher
'onsite'} .app_mode !== 'onsite'}
title="Customized onsite OS and web browser (Chromium or Firefox based) launcher, for onsite presenters in for practice and onsite backup">Onsite</button title="Customized onsite OS and web browser (Chromium or Firefox based) launcher, for onsite presenters in for practice and onsite backup"
> >Onsite</button>
</div> </div>
</div> </div>
<!-- 2. UI Layout Toggles --> <!-- 2. UI Layout Toggles -->
<div <div
class="flex flex-col gap-1 border-t border-surface-500/10 pt-2 mt-1" class="border-surface-500/10 mt-1 flex flex-col gap-1 border-t pt-2">
> <p class="ml-1 text-[9px] font-bold uppercase opacity-50">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1" Interface Visibility
>Interface Visibility</p </p>
>
<div class="grid grid-cols-2 gap-2 p-1"> <div class="grid grid-cols-2 gap-2 p-1">
<label class="flex items-center gap-2 cursor-pointer group"> <label class="group flex cursor-pointer items-center gap-2">
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
$events_loc.launcher.hide__launcher_header $events_loc.launcher.hide__launcher_header
} }
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/> <span class="group-hover:text-primary-500 text-xs"
<span class="text-xs group-hover:text-primary-500" >Hide Header</span>
>Hide Header</span
>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer group"> <label class="group flex cursor-pointer items-center gap-2">
<input <input
type="checkbox" type="checkbox"
bind:checked={$events_loc.launcher.hide__launcher_menu} bind:checked={$events_loc.launcher.hide__launcher_menu}
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/> <span class="group-hover:text-primary-500 text-xs"
<span class="text-xs group-hover:text-primary-500" >Hide Menu</span>
>Hide Menu</span
>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer group"> <label class="group flex cursor-pointer items-center gap-2">
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
$events_loc.launcher.hide__launcher_footer $events_loc.launcher.hide__launcher_footer
} }
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/> <span class="group-hover:text-primary-500 text-xs"
<span class="text-xs group-hover:text-primary-500" >Hide Footer</span>
>Hide Footer</span
>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer group"> <label class="group flex cursor-pointer items-center gap-2">
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
$events_loc.launcher.hide__session_datetimes $events_loc.launcher.hide__session_datetimes
} }
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/> <span class="group-hover:text-primary-500 text-xs"
<span class="text-xs group-hover:text-primary-500" >Hide Times</span>
>Hide Times</span
>
</label> </label>
</div> </div>
</div> </div>
@@ -202,8 +194,7 @@
$events_loc.launcher.time_hours = 12; $events_loc.launcher.time_hours = 12;
} }
}} }}
class="btn btn-xs preset-tonal-surface w-full text-[10px]" class="btn btn-xs preset-tonal-surface w-full text-[10px]">
>
<Clock size="0.85em" class="mr-1 opacity-50" /> <Clock size="0.85em" class="mr-1 opacity-50" />
Clock Format: Clock Format:
<strong>{$events_loc.launcher.time_hours}-hour</strong> <strong>{$events_loc.launcher.time_hours}-hour</strong>
@@ -212,35 +203,30 @@
<!-- 4. Advanced Toggles (Edit Mode Only) --> <!-- 4. Advanced Toggles (Edit Mode Only) -->
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<div <div
class="col-span-full border-t border-surface-500/20 pt-3 mt-1 flex flex-col gap-2" class="border-surface-500/20 col-span-full mt-1 flex flex-col gap-2 border-t pt-3">
> <p class="ml-1 text-[9px] font-bold uppercase opacity-50">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1" Technical Layout
>Technical Layout</p </p>
>
<div class="grid grid-cols-1 gap-2 p-1"> <div class="grid grid-cols-1 gap-2 p-1">
<label class="flex items-center gap-2 cursor-pointer group"> <label class="group flex cursor-pointer items-center gap-2">
<input <input
type="checkbox" type="checkbox"
bind:checked={$events_loc.launcher.hide__ws_element} bind:checked={$events_loc.launcher.hide__ws_element}
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/>
<span <span
class="text-xs group-hover:text-primary-500 italic" class="group-hover:text-primary-500 text-xs italic"
>Hide WebSocket Debugger</span >Hide WebSocket Debugger</span>
>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer group"> <label class="group flex cursor-pointer items-center gap-2">
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
$events_loc.launcher.hide__modal_header_title $events_loc.launcher.hide__modal_header_title
} }
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/>
<span <span
class="text-xs group-hover:text-primary-500 italic" class="group-hover:text-primary-500 text-xs italic"
>Hide Poster Modal Title</span >Hide Poster Modal Title</span>
>
</label> </label>
</div> </div>
</div> </div>

View File

@@ -1,16 +1,24 @@
<script lang="ts"> <script lang="ts">
import { ae_loc } from '$lib/stores/ae_stores'; import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores'; import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte'; import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { Gamepad2, Link, Lock, LockOpen, Plug, RefreshCw, Unlink } from '@lucide/svelte'; import {
interface Props { Gamepad2,
on_expand?: () => void; Link,
} Lock,
let { on_expand }: Props = $props(); LockOpen,
Plug,
RefreshCw,
Unlink
} from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
const ws_connected = $derived( const ws_connected = $derived(
$events_sess.launcher.ws_connect_status === 'connected' $events_sess.launcher.ws_connect_status === 'connected'
); );
</script> </script>
<Launcher_Cfg_Section <Launcher_Cfg_Section
@@ -20,41 +28,35 @@
{on_expand} {on_expand}
description="Mode: {$events_loc.launcher?.controller} | WS: {ws_connected description="Mode: {$events_loc.launcher?.controller} | WS: {ws_connected
? 'Connected' ? 'Connected'
: 'Offline'}" : 'Offline'}">
>
<!-- Content omitted for brevity, preserved in file --> <!-- Content omitted for brevity, preserved in file -->
<div class="col-span-full flex flex-col gap-3"> <div class="col-span-full flex flex-col gap-3">
<!-- 1. Connection Status Badge --> <!-- 1. Connection Status Badge -->
<div <div
class="flex items-center justify-between bg-surface-500/5 p-2 rounded border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex items-center justify-between rounded border p-2">
>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Plug size="1em" class="opacity-50" /> <Plug size="1em" class="opacity-50" />
<span class="text-[10px] font-bold uppercase tracking-wider" <span class="text-[10px] font-bold tracking-wider uppercase"
>WebSocket Link</span >WebSocket Link</span>
>
</div> </div>
{#if ws_connected} {#if ws_connected}
<span <span
class="badge preset-filled-success text-[8px] animate-pulse" class="badge preset-filled-success animate-pulse text-[8px]"
>Connected</span >Connected</span>
>
{:else} {:else}
<span class="badge preset-filled-error text-[8px]" <span class="badge preset-filled-error text-[8px]"
>Disconnected</span >Disconnected</span>
>
{/if} {/if}
</div> </div>
<!-- 2. Controller Mode Selection --> <!-- 2. Controller Mode Selection -->
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1" <p class="ml-1 text-[9px] font-bold uppercase opacity-50">
>Controller Strategy</p Controller Strategy
> </p>
<select <select
bind:value={$events_loc.launcher.controller} bind:value={$events_loc.launcher.controller}
class="select select-sm text-xs preset-tonal-surface h-8" class="select select-sm preset-tonal-surface h-8 text-xs">
>
<option value="local">Local Only</option> <option value="local">Local Only</option>
<option value="remote">Remotely WS Controlled</option> <option value="remote">Remotely WS Controlled</option>
<option value="local_push">Local and WS Controller</option> <option value="local_push">Local and WS Controller</option>
@@ -62,7 +64,7 @@
</div> </div>
<!-- 3. Connection Actions --> <!-- 3. Connection Actions -->
<div class="grid grid-cols-2 gap-2 mt-1"> <div class="mt-1 grid grid-cols-2 gap-2">
<button <button
type="button" type="button"
onclick={() => { onclick={() => {
@@ -76,8 +78,7 @@
}} }}
class="btn btn-sm text-[10px] font-bold transition-all" class="btn btn-sm text-[10px] font-bold transition-all"
class:preset-tonal-error={$events_loc.launcher.ws_connect} class:preset-tonal-error={$events_loc.launcher.ws_connect}
class:preset-tonal-success={!$events_loc.launcher.ws_connect} class:preset-tonal-success={!$events_loc.launcher.ws_connect}>
>
{#if $events_loc.launcher.ws_connect} {#if $events_loc.launcher.ws_connect}
<Unlink size="0.85em" class="mr-1" /> Disconnect <Unlink size="0.85em" class="mr-1" /> Disconnect
{:else} {:else}
@@ -92,8 +93,7 @@
$events_sess.launcher.controller_trigger_send = 'trigger'; $events_sess.launcher.controller_trigger_send = 'trigger';
}} }}
class="btn btn-sm preset-tonal-secondary hover:preset-filled-secondary-500 text-[10px] font-bold" class="btn btn-sm preset-tonal-secondary hover:preset-filled-secondary-500 text-[10px] font-bold"
disabled={!ws_connected} disabled={!ws_connected}>
>
<RefreshCw size="0.85em" class="mr-1" /> Group Reload <RefreshCw size="0.85em" class="mr-1" /> Group Reload
</button> </button>
</div> </div>
@@ -101,21 +101,19 @@
<!-- 4. Technical Config (Edit Mode Only) --> <!-- 4. Technical Config (Edit Mode Only) -->
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<div <div
class="col-span-full border-t border-surface-500/20 pt-3 mt-1 flex flex-col gap-2" class="border-surface-500/20 col-span-full mt-1 flex flex-col gap-2 border-t pt-3">
> <p class="ml-1 text-[9px] font-bold uppercase opacity-50">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1" Channel Configuration
>Channel Configuration</p </p>
>
<div class="flex gap-1"> <div class="flex gap-1">
<input <input
bind:value={$events_loc.launcher.controller_group_code} bind:value={$events_loc.launcher.controller_group_code}
placeholder="Group Code" placeholder="Group Code"
class="input input-sm grow text-[10px] h-7 preset-tonal-surface font-mono" class="input input-sm preset-tonal-surface h-7 grow font-mono text-[10px]"
readonly={!$events_sess.launcher readonly={!$events_sess.launcher
.controller_unlock_group_code} .controller_unlock_group_code}
ondblclick={() => ondblclick={() =>
($events_sess.launcher.controller_unlock_group_code = true)} ($events_sess.launcher.controller_unlock_group_code = true)} />
/>
<button <button
type="button" type="button"
onclick={() => onclick={() =>
@@ -123,8 +121,7 @@
!$events_sess.launcher !$events_sess.launcher
.controller_unlock_group_code)} .controller_unlock_group_code)}
class="btn btn-xs preset-tonal-surface" class="btn btn-xs preset-tonal-surface"
title="Toggle Unlock" title="Toggle Unlock">
>
{#if $events_sess.launcher.controller_unlock_group_code} {#if $events_sess.launcher.controller_unlock_group_code}
<LockOpen size="0.85em" class="text-primary-500" /> <LockOpen size="0.85em" class="text-primary-500" />
{:else} {:else}
@@ -132,7 +129,7 @@
{/if} {/if}
</button> </button>
</div> </div>
<p class="text-[8px] opacity-40 italic ml-1"> <p class="ml-1 text-[8px] italic opacity-40">
Double-click input to unlock editing. Changing code triggers Double-click input to unlock editing. Changing code triggers
reconnect. reconnect.
</p> </p>

View File

@@ -1,44 +1,44 @@
<script lang="ts"> <script lang="ts">
import { ae_loc } from '$lib/stores/ae_stores'; import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores'; import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte'; import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { HeartPulse, RefreshCw } from '@lucide/svelte'; import { HeartPulse, RefreshCw } from '@lucide/svelte';
interface Props { interface Props {
on_expand?: () => void; on_expand?: () => void;
} }
let { on_expand }: Props = $props(); let { on_expand }: Props = $props();
// Derived Usage Percentage for Visuals // Derived Usage Percentage for Visuals
let cpu_load_pct = $derived.by(() => { let cpu_load_pct = $derived.by(() => {
const meta = $ae_loc.native_device?.meta_json; const meta = $ae_loc.native_device?.meta_json;
// loadavg is [1m, 5m, 15m] - use 1m load // loadavg is [1m, 5m, 15m] - use 1m load
if (!meta?.loadavg || !Array.isArray(meta.loadavg)) return 15; if (!meta?.loadavg || !Array.isArray(meta.loadavg)) return 15;
// Load average is usually 0.0 to N (cores). Normalize to 0-100 based on cores if available. // Load average is usually 0.0 to N (cores). Normalize to 0-100 based on cores if available.
const load = meta.loadavg[0]; const load = meta.loadavg[0];
const cores = (meta.cpus || []).length || 1; const cores = (meta.cpus || []).length || 1;
const pct = Math.round((load / cores) * 100); const pct = Math.round((load / cores) * 100);
return Math.min(Math.max(pct, 5), 100); // Clamp 5-100 return Math.min(Math.max(pct, 5), 100); // Clamp 5-100
}); });
let ram_usage_pct = $derived.by(() => { let ram_usage_pct = $derived.by(() => {
const meta = $ae_loc.native_device?.meta_json; const meta = $ae_loc.native_device?.meta_json;
if (!meta?.total_mem || !meta?.free_mem) return 0; if (!meta?.total_mem || !meta?.free_mem) return 0;
// Parse "16384MB" strings // Parse "16384MB" strings
const total = parseInt(meta.total_mem); const total = parseInt(meta.total_mem);
const free = parseInt(meta.free_mem); const free = parseInt(meta.free_mem);
if (isNaN(total) || isNaN(free)) return 0; if (isNaN(total) || isNaN(free)) return 0;
return Math.round(((total - free) / total) * 100); return Math.round(((total - free) / total) * 100);
}); });
// Helper for usage color // Helper for usage color
function get_usage_color(pct: number) { function get_usage_color(pct: number) {
if (pct > 90) return 'bg-error-500'; if (pct > 90) return 'bg-error-500';
if (pct > 70) return 'bg-warning-500'; if (pct > 70) return 'bg-warning-500';
return 'bg-success-500'; return 'bg-success-500';
} }
</script> </script>
<Launcher_Cfg_Section <Launcher_Cfg_Section
@@ -47,53 +47,48 @@
bind:state={$events_loc.launcher.section_state__health} bind:state={$events_loc.launcher.section_state__health}
{on_expand} {on_expand}
description="Heartbeat: {$events_sess.launcher.heartbeat_info description="Heartbeat: {$events_sess.launcher.heartbeat_info
.last_timestamp || 'Pending'}" .last_timestamp || 'Pending'}">
>
<!-- Content omitted for brevity in instruction, but preserved in file --> <!-- Content omitted for brevity in instruction, but preserved in file -->
<!-- Telemetry Dashboard --> <!-- Telemetry Dashboard -->
<div <div
class="col-span-full flex flex-col gap-3 bg-surface-500/5 p-3 rounded-lg border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 col-span-full flex flex-col gap-3 rounded-lg border p-3">
>
<!-- CPU Usage (Mock Logic if load not available yet) --> <!-- CPU Usage (Mock Logic if load not available yet) -->
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div <div
class="flex justify-between text-[9px] uppercase font-bold opacity-60" class="flex justify-between text-[9px] font-bold uppercase opacity-60">
>
<span <span
>CPU Architecture: {$ae_loc.native_device?.meta_json >CPU Architecture: {$ae_loc.native_device?.meta_json
?.arch || '...'}</span ?.arch || '...'}</span>
>
<span>Load: {cpu_load_pct}%</span> <span>Load: {cpu_load_pct}%</span>
</div> </div>
<div <div
class="w-full h-1.5 bg-surface-500/20 rounded-full overflow-hidden" class="bg-surface-500/20 h-1.5 w-full overflow-hidden rounded-full">
>
<div <div
class="h-full transition-all duration-1000 {get_usage_color(cpu_load_pct)}" class="h-full transition-all duration-1000 {get_usage_color(
style="width: {cpu_load_pct}%" cpu_load_pct
></div> )}"
style="width: {cpu_load_pct}%">
</div>
</div> </div>
</div> </div>
<!-- RAM Usage --> <!-- RAM Usage -->
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div <div
class="flex justify-between text-[9px] uppercase font-bold opacity-60" class="flex justify-between text-[9px] font-bold uppercase opacity-60">
>
<span>Memory (RAM)</span> <span>Memory (RAM)</span>
<span>{ram_usage_pct}% Used</span> <span>{ram_usage_pct}% Used</span>
</div> </div>
<div <div
class="w-full h-1.5 bg-surface-500/20 rounded-full overflow-hidden" class="bg-surface-500/20 h-1.5 w-full overflow-hidden rounded-full">
>
<div <div
class="h-full transition-all duration-1000 {get_usage_color( class="h-full transition-all duration-1000 {get_usage_color(
ram_usage_pct ram_usage_pct
)}" )}"
style="width: {ram_usage_pct}%" style="width: {ram_usage_pct}%">
></div> </div>
</div> </div>
<div class="text-[8px] opacity-40 text-right italic"> <div class="text-right text-[8px] italic opacity-40">
Free: {$ae_loc.native_device?.meta_json?.free_mem || '...'} / {$ae_loc Free: {$ae_loc.native_device?.meta_json?.free_mem || '...'} / {$ae_loc
.native_device?.meta_json?.total_mem || '...'} .native_device?.meta_json?.total_mem || '...'}
</div> </div>
@@ -101,26 +96,23 @@
</div> </div>
<!-- Heartbeat & Sync Info --> <!-- Heartbeat & Sync Info -->
<div class="grid grid-cols-2 gap-x-2 gap-y-2 w-full text-[10px] p-1"> <div class="grid w-full grid-cols-2 gap-x-2 gap-y-2 p-1 text-[10px]">
<div class="flex flex-col"> <div class="flex flex-col">
<span class="opacity-50 text-[8px] uppercase font-bold" <span class="text-[8px] font-bold uppercase opacity-50"
>Last Heartbeat</span >Last Heartbeat</span>
>
<span <span
class="font-mono {$events_sess.launcher.heartbeat_info class="font-mono {$events_sess.launcher.heartbeat_info
.status === 'success' .status === 'success'
? 'text-success-500' ? 'text-success-500'
: 'text-error-500'}" : 'text-error-500'}">
>
{$events_sess.launcher.heartbeat_info.last_timestamp || {$events_sess.launcher.heartbeat_info.last_timestamp ||
'Pending...'} 'Pending...'}
</span> </span>
</div> </div>
<div class="flex flex-col text-right"> <div class="flex flex-col text-right">
<span class="opacity-50 text-[8px] uppercase font-bold" <span class="text-[8px] font-bold uppercase opacity-50"
>Local File Cache</span >Local File Cache</span>
>
<span class="font-mono"> <span class="font-mono">
{$events_sess.launcher.sync_stats.cached} / {$events_sess {$events_sess.launcher.sync_stats.cached} / {$events_sess
.launcher.sync_stats.total} .launcher.sync_stats.total}
@@ -129,19 +121,18 @@
{#if $events_sess.launcher.sync_stats.currently_syncing} {#if $events_sess.launcher.sync_stats.currently_syncing}
<div <div
class="col-span-full bg-primary-500/10 p-2 rounded border border-primary-500/20 animate-pulse mt-1" class="bg-primary-500/10 border-primary-500/20 col-span-full mt-1 animate-pulse rounded border p-2">
>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<RefreshCw size="1em" class="animate-spin text-primary-500" /> <RefreshCw
size="1em"
class="text-primary-500 animate-spin" />
<div class="flex flex-col truncate"> <div class="flex flex-col truncate">
<span <span
class="text-[8px] uppercase font-bold text-primary-500" class="text-primary-500 text-[8px] font-bold uppercase"
>Syncing File...</span >Syncing File...</span>
>
<span class="truncate italic opacity-80" <span class="truncate italic opacity-80"
>{$events_sess.launcher.sync_stats >{$events_sess.launcher.sync_stats
.currently_syncing}</span .currently_syncing}</span>
>
</div> </div>
</div> </div>
</div> </div>
@@ -151,26 +142,22 @@
<!-- Device Metadata (Edit Mode Only) --> <!-- Device Metadata (Edit Mode Only) -->
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<div <div
class="col-span-full mt-1 pt-2 border-t border-surface-500/10 flex flex-col gap-1 text-[9px] opacity-60 px-1" class="border-surface-500/10 col-span-full mt-1 flex flex-col gap-1 border-t px-1 pt-2 text-[9px] opacity-60">
>
<div class="flex justify-between"> <div class="flex justify-between">
<span>Hostname:</span> <span>Hostname:</span>
<span class="font-mono" <span class="font-mono"
>{$ae_loc.native_device.info_hostname || '...'}</span >{$ae_loc.native_device.info_hostname || '...'}</span>
>
</div> </div>
<div class="flex justify-between gap-4"> <div class="flex justify-between gap-4">
<span>IP Addresses:</span> <span>IP Addresses:</span>
<span class="font-mono truncate" <span class="truncate font-mono"
>{$ae_loc.native_device.info_ip_list || '...'}</span >{$ae_loc.native_device.info_ip_list || '...'}</span>
>
</div> </div>
<div class="mt-2 opacity-40"> <div class="mt-2 opacity-40">
<span class="text-[8px] uppercase font-bold" <span class="text-[8px] font-bold uppercase"
>Raw Device JSON</span >Raw Device JSON</span>
>
<pre <pre
class="text-[7px] max-h-32 overflow-y-auto bg-black/20 p-1 rounded mt-1"> class="mt-1 max-h-32 overflow-y-auto rounded bg-black/20 p-1 text-[7px]">
{JSON.stringify($ae_loc.native_device, null, 2)} {JSON.stringify($ae_loc.native_device, null, 2)}
</pre> </pre>
</div> </div>

View File

@@ -1,77 +1,80 @@
<script lang="ts"> <script lang="ts">
import { ae_loc, ae_api } from '$lib/stores/ae_stores'; import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores'; import { events_loc } from '$lib/stores/ae_events_stores';
import { cleanup_tmp_files } from '$lib/electron/electron_relay'; import { cleanup_tmp_files } from '$lib/electron/electron_relay';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte'; import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { Bug, BugOff, Eraser, Eye, EyeOff, Wrench } from '@lucide/svelte'; import { Bug, BugOff, Eraser, Eye, EyeOff, Wrench } from '@lucide/svelte';
interface Props { interface Props {
on_expand?: () => void; on_expand?: () => void;
}
let { on_expand }: Props = $props();
let selected_reset = $state('');
let cleanup_status = $state('');
async function handle_cleanup_now() {
const cache_root = $ae_loc.local_file_cache_path;
if (!cache_root) {
cleanup_status = 'Error: Cache path not set.';
return;
} }
let { on_expand }: Props = $props(); const max_age_hours = $events_loc.launcher.cleanup_tmp_max_age_hours ?? 24;
cleanup_status = 'Cleaning...';
const result = await cleanup_tmp_files({
cache_root,
max_age_minutes: max_age_hours * 60
});
cleanup_status =
(result as any).success !== false
? 'Done.'
: `Error: ${(result as any).error}`;
setTimeout(() => (cleanup_status = ''), 4000);
}
let selected_reset = $state(''); function handle_reset_action(val: string) {
let cleanup_status = $state(''); if (!val) return;
async function handle_cleanup_now() { if (val == 'delete_idbs') {
const cache_root = $ae_loc.local_file_cache_path; if (
if (!cache_root) { cleanup_status = 'Error: Cache path not set.'; return; } confirm('Are you sure you want to delete ALL IndexedDB databases?')
const max_age_hours = $events_loc.launcher.cleanup_tmp_max_age_hours ?? 24; ) {
cleanup_status = 'Cleaning...'; indexedDB.deleteDatabase('ae_archives_db');
const result = await cleanup_tmp_files({ cache_root, max_age_minutes: max_age_hours * 60 }); indexedDB.deleteDatabase('ae_core_db');
cleanup_status = (result as any).success !== false ? 'Done.' : `Error: ${(result as any).error}`; indexedDB.deleteDatabase('ae_events_db');
setTimeout(() => (cleanup_status = ''), 4000); indexedDB.deleteDatabase('ae_journals_db');
} indexedDB.deleteDatabase('ae_posts_db');
indexedDB.deleteDatabase('ae_sponsorships_db');
function handle_reset_action(val: string) { alert('All IndexedDB databases deleted. Please reload the app.');
if (!val) return; }
} else if (val == 'delete_idbs_events') {
if (val == 'delete_idbs') { if (
if ( confirm(
confirm( 'Are you sure you want to delete ONLY the Events IndexedDB database?'
'Are you sure you want to delete ALL IndexedDB databases?' )
) ) {
) { indexedDB.deleteDatabase('ae_events_db');
indexedDB.deleteDatabase('ae_archives_db'); alert('Events IndexedDB database deleted. Please reload the app.');
indexedDB.deleteDatabase('ae_core_db'); }
indexedDB.deleteDatabase('ae_events_db'); } else if (val == 'delete_local') {
indexedDB.deleteDatabase('ae_journals_db'); if (confirm('Are you sure you want to delete ALL local config?')) {
indexedDB.deleteDatabase('ae_posts_db'); localStorage.removeItem('ae_loc');
indexedDB.deleteDatabase('ae_sponsorships_db'); localStorage.removeItem('ae_events_loc');
alert( localStorage.removeItem('ae_idaa_loc');
'All IndexedDB databases deleted. Please reload the app.' localStorage.removeItem('ae_journals_loc');
); location.reload();
} }
} else if (val == 'delete_idbs_events') { } else if (val == 'delete_local_events') {
if ( if (
confirm( confirm(
'Are you sure you want to delete ONLY the Events IndexedDB database?' 'Are you sure you want to delete ONLY the Events local config?'
) )
) { ) {
indexedDB.deleteDatabase('ae_events_db'); localStorage.removeItem('ae_events_loc');
alert( location.reload();
'Events IndexedDB database deleted. Please reload the app.'
);
}
} else if (val == 'delete_local') {
if (confirm('Are you sure you want to delete ALL local config?')) {
localStorage.removeItem('ae_loc');
localStorage.removeItem('ae_events_loc');
localStorage.removeItem('ae_idaa_loc');
localStorage.removeItem('ae_journals_loc');
location.reload();
}
} else if (val == 'delete_local_events') {
if (
confirm(
'Are you sure you want to delete ONLY the Events local config?'
)
) {
localStorage.removeItem('ae_events_loc');
location.reload();
}
} }
selected_reset = '';
} }
selected_reset = '';
}
</script> </script>
<Launcher_Cfg_Section <Launcher_Cfg_Section
@@ -79,44 +82,43 @@
icon={Wrench} icon={Wrench}
bind:state={$events_loc.launcher.section_state__local_actions} bind:state={$events_loc.launcher.section_state__local_actions}
{on_expand} {on_expand}
description="Cache wiping and global menu toggles" description="Cache wiping and global menu toggles">
>
<div class="col-span-full flex flex-col gap-3"> <div class="col-span-full flex flex-col gap-3">
<!-- 1. Reset Actions --> <!-- 1. Reset Actions -->
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span <span
class="text-[9px] font-bold uppercase opacity-50 ml-1 text-error-500" class="text-error-500 ml-1 text-[9px] font-bold uppercase opacity-50"
>Maintenance & Resets</span >Maintenance & Resets</span>
>
<select <select
bind:value={selected_reset} bind:value={selected_reset}
onchange={(e) => onchange={(e) =>
handle_reset_action((e.target as HTMLSelectElement).value)} handle_reset_action((e.target as HTMLSelectElement).value)}
class="select select-sm text-xs preset-tonal-surface h-8 text-error-500 border-error-500/20" class="select select-sm preset-tonal-surface text-error-500 border-error-500/20 h-8 text-xs">
>
<option value="">-- Select a reset action --</option> <option value="">-- Select a reset action --</option>
<option value="delete_idbs">Delete ALL Databases</option> <option value="delete_idbs">Delete ALL Databases</option>
<option value="delete_idbs_events">Delete Events DB Only</option <option value="delete_idbs_events"
> >Delete Events DB Only</option>
<option value="delete_local">Wipe ALL Local Storage</option> <option value="delete_local">Wipe ALL Local Storage</option>
<option value="delete_local_events" <option value="delete_local_events"
>Wipe Events Storage Only</option >Wipe Events Storage Only</option>
>
</select> </select>
<span class="text-[8px] opacity-40 italic ml-1 leading-tight"> <span class="ml-1 text-[8px] leading-tight italic opacity-40">
* Destructive actions require browser confirmation. * Destructive actions require browser confirmation.
</span> </span>
</div> </div>
<!-- 2. UI Toggles --> <!-- 2. UI Toggles -->
<div class="grid grid-cols-2 gap-2 mt-1"> <div class="mt-1 grid grid-cols-2 gap-2">
<button <button
type="button" type="button"
onclick={() => ($ae_loc.sys_menu.hide = !$ae_loc.sys_menu.hide)} onclick={() => ($ae_loc.sys_menu.hide = !$ae_loc.sys_menu.hide)}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500" class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500"
title="Show/Hide Aether global system menu" title="Show/Hide Aether global system menu">
> {#if $ae_loc.sys_menu.hide}<Eye
{#if $ae_loc.sys_menu.hide}<Eye size="1em" class="mr-2" />{:else}<EyeOff size="1em" class="mr-2" />{/if} size="1em"
class="mr-2" />{:else}<EyeOff
size="1em"
class="mr-2" />{/if}
{$ae_loc.sys_menu.hide ? 'Show' : 'Hide'} Sys Menu {$ae_loc.sys_menu.hide ? 'Show' : 'Hide'} Sys Menu
</button> </button>
@@ -125,41 +127,51 @@
onclick={() => onclick={() =>
($ae_loc.debug_menu.hide = !$ae_loc.debug_menu.hide)} ($ae_loc.debug_menu.hide = !$ae_loc.debug_menu.hide)}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500" class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500"
title="Show/Hide Aether global debug menu" title="Show/Hide Aether global debug menu">
> {#if $ae_loc.debug_menu.hide}<Bug
{#if $ae_loc.debug_menu.hide}<Bug size="1em" class="mr-2" />{:else}<BugOff size="1em" class="mr-2" />{/if} size="1em"
class="mr-2" />{:else}<BugOff
size="1em"
class="mr-2" />{/if}
{$ae_loc.debug_menu.hide ? 'Show' : 'Hide'} Debug {$ae_loc.debug_menu.hide ? 'Show' : 'Hide'} Debug
</button> </button>
</div> </div>
<!-- 3. Cache .tmp Cleanup (Native Only) --> <!-- 3. Cache .tmp Cleanup (Native Only) -->
{#if $ae_loc.is_native && $ae_loc.local_file_cache_path} {#if $ae_loc.is_native && $ae_loc.local_file_cache_path}
<div class="flex flex-col gap-1 border-t border-surface-500/20 pt-2 mt-1"> <div
<span class="text-[9px] font-bold uppercase opacity-50 ml-1">Cache Maintenance</span> class="border-surface-500/20 mt-1 flex flex-col gap-1 border-t pt-2">
<span class="ml-1 text-[9px] font-bold uppercase opacity-50"
>Cache Maintenance</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<label for="cleanup_max_age" class="text-[10px] opacity-70 whitespace-nowrap">Max age (hrs):</label> <label
for="cleanup_max_age"
class="text-[10px] whitespace-nowrap opacity-70"
>Max age (hrs):</label>
<input <input
id="cleanup_max_age" id="cleanup_max_age"
type="number" type="number"
min="1" min="1"
max="168" max="168"
bind:value={$events_loc.launcher.cleanup_tmp_max_age_hours} bind:value={
class="input input-sm w-16 h-7 text-xs text-center preset-tonal-surface" $events_loc.launcher.cleanup_tmp_max_age_hours
placeholder="24" }
/> class="input input-sm preset-tonal-surface h-7 w-16 text-center text-xs"
placeholder="24" />
<button <button
type="button" type="button"
onclick={handle_cleanup_now} onclick={handle_cleanup_now}
class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 grow" class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 grow">
>
<Eraser size="0.85em" class="mr-1" /> Clean .tmp Now <Eraser size="0.85em" class="mr-1" /> Clean .tmp Now
</button> </button>
</div> </div>
{#if cleanup_status} {#if cleanup_status}
<span class="text-[9px] italic opacity-60 ml-1">{cleanup_status}</span> <span class="ml-1 text-[9px] italic opacity-60"
>{cleanup_status}</span>
{/if} {/if}
<span class="text-[8px] opacity-40 italic ml-1 leading-tight"> <span class="ml-1 text-[8px] leading-tight italic opacity-40">
Removes stale in-progress download artifacts. Auto-runs on startup. Removes stale in-progress download artifacts. Auto-runs on
startup.
</span> </span>
</div> </div>
{/if} {/if}
@@ -167,14 +179,12 @@
<!-- 4. Connection Summary (Edit Mode Only) --> <!-- 4. Connection Summary (Edit Mode Only) -->
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<div <div
class="col-span-full border-t border-surface-500/20 pt-2 mt-1 flex flex-col gap-1" class="border-surface-500/20 col-span-full mt-1 flex flex-col gap-1 border-t pt-2">
> <p class="ml-1 text-[9px] font-bold uppercase opacity-50">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1" API Context
>API Context</p </p>
>
<div <div
class="bg-black/10 p-2 rounded text-[9px] font-mono opacity-60 break-all leading-tight" class="rounded bg-black/10 p-2 font-mono text-[9px] leading-tight break-all opacity-60">
>
Endpoint: {$ae_api.base_url}<br /> Endpoint: {$ae_api.base_url}<br />
Account: {$ae_loc.account_id} Account: {$ae_loc.account_id}
</div> </div>

View File

@@ -1,50 +1,62 @@
<script lang="ts"> <script lang="ts">
import { ae_loc, ae_api, ae_sess } from '$lib/stores/ae_stores'; import { ae_loc, ae_api, ae_sess } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores'; import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import * as native from '$lib/electron/electron_relay'; import * as native from '$lib/electron/electron_relay';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte'; import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { Code, Columns2, FlaskConical, FolderOpen, Image, Maximize2, Monitor, Play, Power, RefreshCw, SkipBack, SkipForward, Square } from '@lucide/svelte'; import {
interface Props { Code,
on_expand?: () => void; Columns2,
FlaskConical,
FolderOpen,
Image,
Maximize2,
Monitor,
Play,
Power,
RefreshCw,
SkipBack,
SkipForward,
Square
} from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
let test_cmd_result = $state('');
let remote_app: 'powerpoint' | 'keynote' = $state('powerpoint');
let remote_status = $state('');
let system_status = $state('');
async function handle_remote_control(
action: 'next' | 'prev' | 'start' | 'stop'
) {
remote_status = `Sending ${action}...`;
const res = await native.control_presentation({
app: remote_app,
action
});
if (res.success) {
remote_status = `Success: ${action}`;
} else {
remote_status = `Error: ${res.error}`;
} }
let { on_expand }: Props = $props(); setTimeout(() => (remote_status = ''), 3000);
}
let test_cmd_result = $state(''); async function handle_system_action(promise: Promise<any>, label: string) {
let remote_app: 'powerpoint' | 'keynote' = $state('powerpoint'); system_status = `Executing ${label}...`;
let remote_status = $state(''); const res = await promise;
let system_status = $state(''); if (res.success) {
system_status = `Success: ${label}`;
async function handle_remote_control( } else {
action: 'next' | 'prev' | 'start' | 'stop' system_status = `Error: ${res.error || 'Unknown error'}`;
) {
remote_status = `Sending ${action}...`;
const res = await native.control_presentation({
app: remote_app,
action
});
if (res.success) {
remote_status = `Success: ${action}`;
} else {
remote_status = `Error: ${res.error}`;
}
setTimeout(() => (remote_status = ''), 3000);
} }
setTimeout(() => (system_status = ''), 3000);
}
async function handle_system_action(promise: Promise<any>, label: string) { // Modal state for dangerous actions
system_status = `Executing ${label}...`; let show_power_confirm = $state<{ action: string; label: string } | null>(null);
const res = await promise;
if (res.success) {
system_status = `Success: ${label}`;
} else {
system_status = `Error: ${res.error || 'Unknown error'}`;
}
setTimeout(() => (system_status = ''), 3000);
}
// Modal state for dangerous actions
let show_power_confirm = $state<{ action: string; label: string } | null>(
null
);
</script> </script>
<Launcher_Cfg_Section <Launcher_Cfg_Section
@@ -53,51 +65,50 @@
bind:state={$events_loc.launcher.section_state__native_os} bind:state={$events_loc.launcher.section_state__native_os}
{on_expand} {on_expand}
description="OS: {$ae_loc.native_device?.meta_json?.platform || description="OS: {$ae_loc.native_device?.meta_json?.platform ||
'...'} | Kiosk & Apps" '...'} | Kiosk & Apps">
>
<!-- Dev preview banner: shown when edit_mode is on but not running in Electron. <!-- Dev preview banner: shown when edit_mode is on but not running in Electron.
electron_relay functions all return null when native is absent — no errors. --> electron_relay functions all return null when native is absent — no errors. -->
{#if $ae_loc.edit_mode && !$ae_loc.is_native} {#if $ae_loc.edit_mode && !$ae_loc.is_native}
<div class="flex items-center gap-2 px-2 py-1.5 rounded-lg bg-warning-500/10 border border-warning-500/30 mb-1"> <div
class="bg-warning-500/10 border-warning-500/30 mb-1 flex items-center gap-2 rounded-lg border px-2 py-1.5">
<FlaskConical size="0.75em" class="text-warning-500" /> <FlaskConical size="0.75em" class="text-warning-500" />
<span class="text-[9px] text-warning-500 font-bold uppercase tracking-wide">Dev Preview — controls visible but non-functional without Electron</span> <span
class="text-warning-500 text-[9px] font-bold tracking-wide uppercase"
>Dev Preview — controls visible but non-functional without
Electron</span>
</div> </div>
{/if} {/if}
{#if system_status} {#if system_status}
<div <div
class="col-span-full text-[10px] text-center italic bg-surface-500/10 py-1 rounded animate-pulse text-primary-500 border border-primary-500/20" class="bg-surface-500/10 text-primary-500 border-primary-500/20 col-span-full animate-pulse rounded border py-1 text-center text-[10px] italic">
>
{system_status} {system_status}
</div> </div>
{/if} {/if}
<!-- 1. Window & Folders (Common) --> <!-- 1. Window & Folders (Common) -->
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1" <p class="ml-1 text-[9px] font-bold uppercase opacity-50">
>Folders & View</p Folders & View
> </p>
<div class="grid grid-cols-2 gap-1"> <div class="grid grid-cols-2 gap-1">
<button <button
type="button" type="button"
onclick={() => onclick={() =>
native.open_folder($ae_loc.local_file_cache_path)} native.open_folder($ae_loc.local_file_cache_path)}
class="btn btn-xs preset-tonal-primary hover:preset-filled-primary-500 justify-start" class="btn btn-xs preset-tonal-primary hover:preset-filled-primary-500 justify-start">
>
<FolderOpen size="0.85em" class="mr-1 shrink-0" /> Cache <FolderOpen size="0.85em" class="mr-1 shrink-0" /> Cache
</button> </button>
<button <button
type="button" type="button"
onclick={() => native.open_folder($ae_loc.host_file_temp_path)} onclick={() => native.open_folder($ae_loc.host_file_temp_path)}
class="btn btn-xs preset-tonal-primary hover:preset-filled-primary-500 justify-start" class="btn btn-xs preset-tonal-primary hover:preset-filled-primary-500 justify-start">
>
<FolderOpen size="0.85em" class="mr-1 shrink-0" /> Temp <FolderOpen size="0.85em" class="mr-1 shrink-0" /> Temp
</button> </button>
<button <button
type="button" type="button"
onclick={() => native.window_control({ action: 'maximize' })} onclick={() => native.window_control({ action: 'maximize' })}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500" class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500">
>
<Maximize2 size="0.85em" class="mr-1" /> Maximize <Maximize2 size="0.85em" class="mr-1" /> Maximize
</button> </button>
<button <button
@@ -107,8 +118,7 @@
native.window_control({ action: 'kiosk', value: true }), native.window_control({ action: 'kiosk', value: true }),
'Kiosk Mode' 'Kiosk Mode'
)} )}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500" class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500">
>
<Monitor size="0.85em" class="mr-1" /> Kiosk <Monitor size="0.85em" class="mr-1" /> Kiosk
</button> </button>
</div> </div>
@@ -116,14 +126,13 @@
<!-- 2. Presentation Remote Control (Common) --> <!-- 2. Presentation Remote Control (Common) -->
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex flex-row justify-between items-center px-1"> <div class="flex flex-row items-center justify-between px-1">
<p class="text-[9px] font-bold uppercase opacity-50" <p class="text-[9px] font-bold uppercase opacity-50">
>Remote Control</p Remote Control
> </p>
<select <select
bind:value={remote_app} bind:value={remote_app}
class="select select-sm py-0 h-5 text-[9px] w-24 preset-tonal-surface" class="select select-sm preset-tonal-surface h-5 w-24 py-0 text-[9px]">
>
<option value="powerpoint">PowerPoint</option> <option value="powerpoint">PowerPoint</option>
<option value="keynote">Keynote</option> <option value="keynote">Keynote</option>
</select> </select>
@@ -134,39 +143,34 @@
type="button" type="button"
onclick={() => handle_remote_control('prev')} onclick={() => handle_remote_control('prev')}
class="btn btn-sm preset-tonal-secondary" class="btn btn-sm preset-tonal-secondary"
title="Previous Slide" title="Previous Slide">
>
<SkipBack size="1em" /> <SkipBack size="1em" />
</button> </button>
<button <button
type="button" type="button"
onclick={() => handle_remote_control('start')} onclick={() => handle_remote_control('start')}
class="btn btn-sm preset-tonal-success" class="btn btn-sm preset-tonal-success"
title="Start/Resume Slideshow" title="Start/Resume Slideshow">
>
<Play size="1em" /> <Play size="1em" />
</button> </button>
<button <button
type="button" type="button"
onclick={() => handle_remote_control('stop')} onclick={() => handle_remote_control('stop')}
class="btn btn-sm preset-tonal-error" class="btn btn-sm preset-tonal-error"
title="Stop Slideshow" title="Stop Slideshow">
>
<Square size="1em" /> <Square size="1em" />
</button> </button>
<button <button
type="button" type="button"
onclick={() => handle_remote_control('next')} onclick={() => handle_remote_control('next')}
class="btn btn-sm preset-tonal-secondary" class="btn btn-sm preset-tonal-secondary"
title="Next Slide" title="Next Slide">
>
<SkipForward size="1em" /> <SkipForward size="1em" />
</button> </button>
</div> </div>
{#if remote_status} {#if remote_status}
<div <div
class="text-[9px] text-center italic animate-pulse text-primary-500" class="text-primary-500 animate-pulse text-center text-[9px] italic">
>
{remote_status} {remote_status}
</div> </div>
{/if} {/if}
@@ -175,13 +179,12 @@
<!-- 3. Technical Management (Edit Mode Only) --> <!-- 3. Technical Management (Edit Mode Only) -->
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<div <div
class="col-span-full border-t border-surface-500/20 pt-3 mt-1 flex flex-col gap-3" class="border-surface-500/20 col-span-full mt-1 flex flex-col gap-3 border-t pt-3">
>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<p class="text-[9px] font-bold uppercase opacity-50" <p class="text-[9px] font-bold uppercase opacity-50">
>System Actions</p System Actions
> </p>
<div class="grid grid-cols-1 gap-1"> <div class="grid grid-cols-1 gap-1">
<button <button
type="button" type="button"
@@ -192,9 +195,9 @@
}), }),
'Extend Display' 'Extend Display'
)} )}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500 justify-start" class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500 justify-start">
> <Columns2 size="0.85em" class="mr-1 shrink-0" /> Extend
<Columns2 size="0.85em" class="mr-1 shrink-0" /> Extend Mode Mode
</button> </button>
<button <button
type="button" type="button"
@@ -206,17 +209,15 @@
'Wallpaper' 'Wallpaper'
)} )}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500 justify-start" class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500 justify-start"
disabled={!$ae_loc.site_header_image_path} disabled={!$ae_loc.site_header_image_path}>
>
<Image size="0.85em" class="mr-1 shrink-0" /> Reset Wallpaper <Image size="0.85em" class="mr-1 shrink-0" /> Reset Wallpaper
</button> </button>
</div> </div>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span <span
class="text-[9px] font-bold uppercase opacity-50 text-error-500" class="text-error-500 text-[9px] font-bold uppercase opacity-50"
>Power</span >Power</span>
>
<div class="grid grid-cols-1 gap-1"> <div class="grid grid-cols-1 gap-1">
<button <button
type="button" type="button"
@@ -225,8 +226,7 @@
action: 'reboot', action: 'reboot',
label: 'Reboot Laptop' label: 'Reboot Laptop'
})} })}
class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 justify-start" class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 justify-start">
>
<RefreshCw size="0.85em" class="mr-1 shrink-0" /> Reboot <RefreshCw size="0.85em" class="mr-1 shrink-0" /> Reboot
</button> </button>
<button <button
@@ -236,8 +236,7 @@
action: 'shutdown', action: 'shutdown',
label: 'Shutdown Laptop' label: 'Shutdown Laptop'
})} })}
class="btn btn-xs preset-tonal-error hover:preset-filled-error-500 justify-start" class="btn btn-xs preset-tonal-error hover:preset-filled-error-500 justify-start">
>
<Power size="0.85em" class="mr-1 shrink-0" /> Shutdown <Power size="0.85em" class="mr-1 shrink-0" /> Shutdown
</button> </button>
</div> </div>
@@ -245,16 +244,15 @@
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1" <p class="ml-1 text-[9px] font-bold uppercase opacity-50">
>Terminal Access</p Terminal Access
> </p>
<div class="flex gap-1"> <div class="flex gap-1">
<input <input
type="text" type="text"
bind:value={$events_sess.launcher.manual_cmd} bind:value={$events_sess.launcher.manual_cmd}
placeholder="ls -la" placeholder="ls -la"
class="input input-sm grow text-[10px] preset-tonal-surface h-7" class="input input-sm preset-tonal-surface h-7 grow text-[10px]" />
/>
<button <button
type="button" type="button"
onclick={async () => { onclick={async () => {
@@ -268,13 +266,12 @@
(res as any).error || (res as any).error ||
'No Output'; 'No Output';
}} }}
class="btn btn-sm preset-filled-secondary hover:preset-filled-primary-500 text-[10px] h-7" class="btn btn-sm preset-filled-secondary hover:preset-filled-primary-500 h-7 text-[10px]"
>Run</button >Run</button>
>
</div> </div>
{#if test_cmd_result} {#if test_cmd_result}
<pre <pre
class="text-[8px] bg-black text-green-500 p-2 mt-1 overflow-x-auto rounded border border-surface-500/50 max-h-24 shadow-inner">{test_cmd_result}</pre> class="border-surface-500/50 mt-1 max-h-24 overflow-x-auto rounded border bg-black p-2 text-[8px] text-green-500 shadow-inner">{test_cmd_result}</pre>
{/if} {/if}
</div> </div>
</div> </div>
@@ -284,25 +281,21 @@
<!-- Power Confirmation Modal --> <!-- Power Confirmation Modal -->
{#if show_power_confirm} {#if show_power_confirm}
<div <div
class="fixed inset-0 z-[1000] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4" class="fixed inset-0 z-[1000] flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm">
>
<div <div
class="card p-6 w-full max-w-sm preset-filled-surface-100-900 border border-error-500 shadow-2xl animate-in zoom-in-95 duration-200" class="card preset-filled-surface-100-900 border-error-500 animate-in zoom-in-95 w-full max-w-sm border p-6 shadow-2xl duration-200">
> <h4 class="h4 text-error-500 mb-2 font-bold">
<h4 class="h4 text-error-500 font-bold mb-2">
Confirm System Action Confirm System Action
</h4> </h4>
<p class="text-sm opacity-80 mb-6"> <p class="mb-6 text-sm opacity-80">
Are you sure you want to <strong Are you sure you want to <strong
>{show_power_confirm.action}</strong >{show_power_confirm.action}</strong> this host machine?
> this host machine?
</p> </p>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button <button
type="button" type="button"
onclick={() => (show_power_confirm = null)} onclick={() => (show_power_confirm = null)}
class="btn btn-sm preset-tonal-surface">Cancel</button class="btn btn-sm preset-tonal-surface">Cancel</button>
>
<button <button
type="button" type="button"
onclick={() => { onclick={() => {
@@ -314,8 +307,7 @@
action action
); );
}} }}
class="btn btn-sm preset-filled-error" class="btn btn-sm preset-filled-error">
>
Confirm {show_power_confirm.action} Confirm {show_power_confirm.action}
</button> </button>
</div> </div>

View File

@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { ae_loc } from '$lib/stores/ae_stores'; import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores'; import { events_loc } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte'; import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { IdCard } from '@lucide/svelte'; import { IdCard } from '@lucide/svelte';
interface Props { interface Props {
on_expand?: () => void; on_expand?: () => void;
} }
let { on_expand }: Props = $props(); let { on_expand }: Props = $props();
</script> </script>
<Launcher_Cfg_Section <Launcher_Cfg_Section
@@ -16,62 +16,54 @@
{on_expand} {on_expand}
description="Idle: {($events_loc.launcher.idle_timer / 60000).toFixed( description="Idle: {($events_loc.launcher.idle_timer / 60000).toFixed(
1 1
)}m | Auto-Posters" )}m | Auto-Posters">
>
<!-- Content omitted for brevity, preserved in file --> <!-- Content omitted for brevity, preserved in file -->
<div class="col-span-full flex flex-col gap-3"> <div class="col-span-full flex flex-col gap-3">
<!-- 1. Technical Timers (Edit Mode Only) --> <!-- 1. Technical Timers (Edit Mode Only) -->
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1" <p class="ml-1 text-[9px] font-bold uppercase opacity-50">
>Screen Saver Timers (ms)</p Screen Saver Timers (ms)
> </p>
<div <div
class="grid grid-cols-1 gap-2 bg-surface-500/5 p-2 rounded border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 grid grid-cols-1 gap-2 rounded border p-2">
> <div class="flex items-center justify-between gap-4">
<div class="flex justify-between items-center gap-4">
<span class="text-[10px] opacity-60">Idle Wait</span> <span class="text-[10px] opacity-60">Idle Wait</span>
<input <input
type="number" type="number"
min={3000} min={3000}
bind:value={$events_loc.launcher.idle_timer} bind:value={$events_loc.launcher.idle_timer}
class="input input-sm text-[10px] h-7 w-24 text-right preset-tonal-surface" class="input input-sm preset-tonal-surface h-7 w-24 text-right text-[10px]" />
/>
</div> </div>
<div class="flex justify-between items-center gap-4"> <div class="flex items-center justify-between gap-4">
<span class="text-[10px] opacity-60">Cycle Check</span> <span class="text-[10px] opacity-60">Cycle Check</span>
<input <input
type="number" type="number"
min={500} min={500}
bind:value={$events_loc.launcher.idle_cycle} bind:value={$events_loc.launcher.idle_cycle}
class="input input-sm text-[10px] h-7 w-24 text-right preset-tonal-surface" class="input input-sm preset-tonal-surface h-7 w-24 text-right text-[10px]" />
/>
</div> </div>
<div class="flex justify-between items-center gap-4"> <div class="flex items-center justify-between gap-4">
<span class="text-[10px] opacity-60" <span class="text-[10px] opacity-60"
>Image Rotation</span >Image Rotation</span>
>
<input <input
type="number" type="number"
min={750} min={750}
bind:value={$events_loc.launcher.idle_loop_period} bind:value={$events_loc.launcher.idle_loop_period}
class="input input-sm text-[10px] h-7 w-24 text-right preset-tonal-surface" class="input input-sm preset-tonal-surface h-7 w-24 text-right text-[10px]" />
/>
</div> </div>
</div> </div>
</div> </div>
{:else} {:else}
<!-- 2. Read Only Summary (Normal Mode) --> <!-- 2. Read Only Summary (Normal Mode) -->
<div <div
class="bg-surface-500/5 p-3 rounded-lg border border-surface-500/10 flex flex-col gap-2" class="bg-surface-500/5 border-surface-500/10 flex flex-col gap-2 rounded-lg border p-3">
> <div class="flex items-center justify-between text-xs">
<div class="flex justify-between items-center text-xs">
<span class="opacity-60">Active Idle Timeout:</span> <span class="opacity-60">Active Idle Timeout:</span>
<span class="font-bold text-primary-500" <span class="text-primary-500 font-bold"
>{($events_loc.launcher.idle_timer / 60000).toFixed(1)} minutes</span >{($events_loc.launcher.idle_timer / 60000).toFixed(1)} minutes</span>
>
</div> </div>
<p class="text-[9px] opacity-40 italic"> <p class="text-[9px] italic opacity-40">
The screen saver automatically rotates digital posters when The screen saver automatically rotates digital posters when
no activity is detected for the specified time. no activity is detected for the specified time.
</p> </p>
@@ -79,7 +71,7 @@
{/if} {/if}
<div class="text-center"> <div class="text-center">
<p class="text-[8px] opacity-40 italic uppercase tracking-tighter"> <p class="text-[8px] tracking-tighter uppercase italic opacity-40">
Applies to "Poster" session types only Applies to "Poster" session types only
</p> </p>
</div> </div>

View File

@@ -1,97 +1,92 @@
<script lang="ts"> <script lang="ts">
import { ae_loc } from '$lib/stores/ae_stores'; import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores'; import { events_loc } from '$lib/stores/ae_events_stores';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { ChevronDown, ChevronRight, Pencil, Pin } from '@lucide/svelte'; import { ChevronDown, ChevronRight, Pencil, Pin } from '@lucide/svelte';
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyComponent = any; type AnyComponent = any;
interface Props { interface Props {
title: string; title: string;
icon: AnyComponent; icon: AnyComponent;
state: 'collapsed' | 'auto' | 'pinned'; state: 'collapsed' | 'auto' | 'pinned';
description?: string; description?: string;
children?: import('svelte').Snippet; children?: import('svelte').Snippet;
on_expand?: () => void; on_expand?: () => void;
on_toggle?: (new_state: 'collapsed' | 'auto' | 'pinned') => void; on_toggle?: (new_state: 'collapsed' | 'auto' | 'pinned') => void;
}
let {
title,
icon,
state = $bindable(),
description,
children,
on_expand,
on_toggle
}: Props = $props();
// Uppercase alias required: Svelte 5 treats lowercase tags as HTML elements.
let Icon = $derived(icon);
function toggle_expand() {
if (state === 'collapsed') {
state = 'auto';
if (on_expand) on_expand();
} else {
state = 'collapsed';
} }
if (on_toggle) on_toggle(state);
}
let { function toggle_pin(e: MouseEvent) {
title, e.stopPropagation();
icon, if (state === 'pinned') {
state = $bindable(), state = 'auto';
description, if (on_expand) on_expand();
children, } else {
on_expand, state = 'pinned';
on_toggle
}: Props = $props();
// Uppercase alias required: Svelte 5 treats lowercase tags as HTML elements.
let Icon = $derived(icon);
function toggle_expand() {
if (state === 'collapsed') {
state = 'auto';
if (on_expand) on_expand();
} else {
state = 'collapsed';
}
if (on_toggle) on_toggle(state);
} }
if (on_toggle) on_toggle(state);
}
function toggle_pin(e: MouseEvent) { const is_open = $derived(state !== 'collapsed');
e.stopPropagation();
if (state === 'pinned') {
state = 'auto';
if (on_expand) on_expand();
} else {
state = 'pinned';
}
if (on_toggle) on_toggle(state);
}
const is_open = $derived(state !== 'collapsed');
</script> </script>
<section <section
class="w-full transition-all duration-300 border rounded-lg overflow-hidden mb-2 {!is_open class="mb-2 w-full overflow-hidden rounded-lg border transition-all duration-300 {!is_open
? 'preset-outlined-surface-300-700' ? 'preset-outlined-surface-300-700'
: ''} {state === 'auto' : ''} {state === 'auto'
? 'preset-outlined-primary-500 shadow-xl' ? 'preset-outlined-primary-500 shadow-xl'
: ''} {state === 'pinned' : ''} {state === 'pinned'
? 'preset-outlined-warning-500 shadow-xl' ? 'preset-outlined-warning-500 shadow-xl'
: ''}" : ''}">
>
<!-- Header --> <!-- Header -->
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<header <header
class="flex flex-row items-center justify-between p-2 cursor-pointer transition-colors {!is_open class="flex cursor-pointer flex-row items-center justify-between p-2 transition-colors {!is_open
? 'bg-surface-500/5' ? 'bg-surface-500/5'
: ''} {state === 'auto' ? 'bg-primary-500/10' : ''} {state === : ''} {state === 'auto' ? 'bg-primary-500/10' : ''} {state ===
'pinned' 'pinned'
? 'bg-warning-500/10' ? 'bg-warning-500/10'
: ''}" : ''}"
onclick={toggle_expand} onclick={toggle_expand}>
>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<Icon <Icon
size="1em" size="1em"
class="w-5 text-center opacity-70 {state === 'auto' class="w-5 text-center opacity-70 {state === 'auto'
? 'text-primary-500' ? 'text-primary-500'
: ''} {state === 'pinned' ? 'text-warning-500' : ''}" : ''} {state === 'pinned' ? 'text-warning-500' : ''}" />
/>
<div class="flex flex-col"> <div class="flex flex-col">
<span <span
class="text-sm font-bold tracking-tight uppercase {!is_open class="text-sm font-bold tracking-tight uppercase {!is_open
? 'opacity-50' ? 'opacity-50'
: ''}">{title}</span : ''}">{title}</span>
>
{#if description && !is_open} {#if description && !is_open}
<span <span
class="text-[9px] opacity-40 italic truncate max-w-[180px]" class="max-w-[180px] truncate text-[9px] italic opacity-40"
>{description}</span >{description}</span>
>
{/if} {/if}
</div> </div>
</div> </div>
@@ -106,16 +101,19 @@
class:text-warning-500={state === 'pinned'} class:text-warning-500={state === 'pinned'}
title={state === 'pinned' title={state === 'pinned'
? 'Unpin Section' ? 'Unpin Section'
: 'Pin Section (Stay open)'} : 'Pin Section (Stay open)'}>
>
<Pin size="0.7em" /> <Pin size="0.7em" />
</button> </button>
<!-- Collapse Icon --> <!-- Collapse Icon -->
{#if is_open} {#if is_open}
<ChevronDown size="1em" class="transition-transform duration-300 opacity-30" /> <ChevronDown
size="1em"
class="opacity-30 transition-transform duration-300" />
{:else} {:else}
<ChevronRight size="1em" class="transition-transform duration-300 opacity-30" /> <ChevronRight
size="1em"
class="opacity-30 transition-transform duration-300" />
{/if} {/if}
</div> </div>
</header> </header>
@@ -124,27 +122,22 @@
{#if is_open} {#if is_open}
<div <div
transition:slide={{ duration: 300 }} transition:slide={{ duration: 300 }}
class="p-3 bg-white/5 dark:bg-black/5" class="bg-white/5 p-3 dark:bg-black/5">
>
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<div class="mb-2 flex justify-between items-center px-1"> <div class="mb-2 flex items-center justify-between px-1">
<span <span
class="text-[8px] uppercase font-bold tracking-widest text-primary-500/60 flex items-center gap-1" class="text-primary-500/60 flex items-center gap-1 text-[8px] font-bold tracking-widest uppercase">
>
<Pencil size="0.7em" /> Technical Mode <Pencil size="0.7em" /> Technical Mode
</span> </span>
{#if state === 'pinned'} {#if state === 'pinned'}
<span <span
class="badge preset-filled-warning text-[8px] uppercase" class="badge preset-filled-warning text-[8px] uppercase"
>Pinned</span >Pinned</span>
>
{/if} {/if}
</div> </div>
{/if} {/if}
<div <div class="grid grid-cols-1 gap-3">
class="grid grid-cols-1 gap-3"
>
{@render children?.()} {@render children?.()}
</div> </div>
</div> </div>

View File

@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { ae_loc } from '$lib/stores/ae_stores'; import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores'; import { events_loc } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte'; import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { Pause, Play, RefreshCw } from '@lucide/svelte'; import { Pause, Play, RefreshCw } from '@lucide/svelte';
interface Props { interface Props {
on_expand?: () => void; on_expand?: () => void;
} }
let { on_expand }: Props = $props(); let { on_expand }: Props = $props();
</script> </script>
<Launcher_Cfg_Section <Launcher_Cfg_Section
@@ -15,21 +15,24 @@
bind:state={$events_loc.launcher.section_state__sync_timers} bind:state={$events_loc.launcher.section_state__sync_timers}
{on_expand} {on_expand}
description="Prefix: {$ae_loc.native_device?.hash_prefix_length || description="Prefix: {$ae_loc.native_device?.hash_prefix_length ||
2} | Loops: Active" 2} | Loops: Active">
>
<!-- Content omitted for brevity, preserved in file --> <!-- Content omitted for brevity, preserved in file -->
<!-- Pause toggle: always visible — useful during testing or onsite troubleshooting --> <!-- Pause toggle: always visible — useful during testing or onsite troubleshooting -->
<div class="flex items-center justify-between mb-2 p-2 rounded border border-surface-500/10 bg-surface-500/5"> <div
<span class="text-[10px] font-bold uppercase tracking-wider opacity-70"> class="border-surface-500/10 bg-surface-500/5 mb-2 flex items-center justify-between rounded border p-2">
{$events_loc.launcher.sync_paused ? '⏸ Sync Paused' : '▶ Sync Active'} <span class="text-[10px] font-bold tracking-wider uppercase opacity-70">
{$events_loc.launcher.sync_paused
? '⏸ Sync Paused'
: '▶ Sync Active'}
</span> </span>
<button <button
type="button" type="button"
onclick={() => ($events_loc.launcher.sync_paused = !$events_loc.launcher.sync_paused)} onclick={() =>
($events_loc.launcher.sync_paused =
!$events_loc.launcher.sync_paused)}
class="btn btn-xs transition-all" class="btn btn-xs transition-all"
class:preset-tonal-warning={$events_loc.launcher.sync_paused} class:preset-tonal-warning={$events_loc.launcher.sync_paused}
class:preset-tonal-success={!$events_loc.launcher.sync_paused} class:preset-tonal-success={!$events_loc.launcher.sync_paused}>
>
{#if $events_loc.launcher.sync_paused} {#if $events_loc.launcher.sync_paused}
<Play size="0.85em" class="mr-1" /> Resume <Play size="0.85em" class="mr-1" /> Resume
{:else} {:else}
@@ -43,92 +46,80 @@
<!-- Technical Timers (Edit Mode Only) --> <!-- Technical Timers (Edit Mode Only) -->
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<p <p class="ml-1 text-[9px] font-bold uppercase opacity-50">
class="text-[9px] font-bold uppercase opacity-50 ml-1" Polling Periods (ms)
>Polling Periods (ms)</p </p>
>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60">Event Data</span <span class="text-[8px] opacity-60"
> >Event Data</span>
<input <input
type="number" type="number"
bind:value={ bind:value={
$events_loc.launcher.sync_intervals.event $events_loc.launcher.sync_intervals.event
} }
class="input input-sm text-[10px] h-7 preset-tonal-surface" class="input input-sm preset-tonal-surface h-7 text-[10px]" />
/>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60" <span class="text-[8px] opacity-60"
>Device Config</span >Device Config</span>
>
<input <input
type="number" type="number"
bind:value={ bind:value={
$events_loc.launcher.sync_intervals.device $events_loc.launcher.sync_intervals.device
} }
class="input input-sm text-[10px] h-7 preset-tonal-surface" class="input input-sm preset-tonal-surface h-7 text-[10px]" />
/>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60" <span class="text-[8px] opacity-60"
>Room/Location</span >Room/Location</span>
>
<input <input
type="number" type="number"
bind:value={ bind:value={
$events_loc.launcher.sync_intervals.location $events_loc.launcher.sync_intervals.location
} }
class="input input-sm text-[10px] h-7 preset-tonal-surface" class="input input-sm preset-tonal-surface h-7 text-[10px]" />
/>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60" <span class="text-[8px] opacity-60"
>Session Loop</span >Session Loop</span>
>
<input <input
type="number" type="number"
bind:value={ bind:value={
$events_loc.launcher.sync_intervals.session $events_loc.launcher.sync_intervals.session
} }
class="input input-sm text-[10px] h-7 preset-tonal-surface" class="input input-sm preset-tonal-surface h-7 text-[10px]" />
/>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60" <span class="text-[8px] opacity-60"
>Presentation Loop</span >Presentation Loop</span>
>
<input <input
type="number" type="number"
bind:value={ bind:value={
$events_loc.launcher.sync_intervals.presentation $events_loc.launcher.sync_intervals
.presentation
} }
class="input input-sm text-[10px] h-7 preset-tonal-surface" class="input input-sm preset-tonal-surface h-7 text-[10px]" />
/>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60" <span class="text-[8px] opacity-60"
>Presenter Loop</span >Presenter Loop</span>
>
<input <input
type="number" type="number"
bind:value={ bind:value={
$events_loc.launcher.sync_intervals.presenter $events_loc.launcher.sync_intervals
.presenter
} }
class="input input-sm text-[10px] h-7 preset-tonal-surface" class="input input-sm preset-tonal-surface h-7 text-[10px]" />
/>
</div> </div>
</div> </div>
</div> </div>
<div <div
class="flex flex-col gap-1 mt-1 border-t border-surface-500/10 pt-2" class="border-surface-500/10 mt-1 flex flex-col gap-1 border-t pt-2">
> <p class="ml-1 text-[9px] font-bold uppercase opacity-50">
<p Cache Structure
class="text-[9px] font-bold uppercase opacity-50 ml-1" </p>
>Cache Structure</p
>
<div class="flex items-center justify-between px-1"> <div class="flex items-center justify-between px-1">
<span class="text-[9px]">Hash Prefix Length</span> <span class="text-[9px]">Hash Prefix Length</span>
{#if $ae_loc.native_device} {#if $ae_loc.native_device}
@@ -136,17 +127,17 @@
bind:value={ bind:value={
$ae_loc.native_device.hash_prefix_length $ae_loc.native_device.hash_prefix_length
} }
class="select select-sm h-6 py-0 text-[10px] w-16 preset-tonal-surface" class="select select-sm preset-tonal-surface h-6 w-16 py-0 text-[10px]">
>
<option value={1}>1 char</option> <option value={1}>1 char</option>
<option value={2}>2 chars</option> <option value={2}>2 chars</option>
<option value={3}>3 chars</option> <option value={3}>3 chars</option>
</select> </select>
{:else} {:else}
<span class="text-[9px] opacity-50 italic">loading…</span> <span class="text-[9px] italic opacity-50"
>loading…</span>
{/if} {/if}
</div> </div>
<p class="text-[8px] opacity-40 italic mt-1"> <p class="mt-1 text-[8px] italic opacity-40">
* Prefix change requires a full app reload to take * Prefix change requires a full app reload to take
effect. effect.
</p> </p>
@@ -154,47 +145,40 @@
{:else} {:else}
<!-- Read Only Summary (Normal Mode) --> <!-- Read Only Summary (Normal Mode) -->
<div <div
class="bg-surface-500/5 p-2 rounded border border-surface-500/10 flex flex-col gap-1" class="bg-surface-500/5 border-surface-500/10 flex flex-col gap-1 rounded border p-2">
>
<div <div
class="flex justify-between text-[9px] opacity-60 font-mono" class="flex justify-between font-mono text-[9px] opacity-60">
>
<span>Event Sync:</span> <span>Event Sync:</span>
<span <span
>{( >{(
$events_loc.launcher.sync_intervals.event / $events_loc.launcher.sync_intervals.event / 1000
1000 ).toFixed(1)}s</span>
).toFixed(1)}s</span
>
</div> </div>
<div <div
class="flex justify-between text-[9px] opacity-60 font-mono" class="flex justify-between font-mono text-[9px] opacity-60">
>
<span>Room Monitor:</span> <span>Room Monitor:</span>
<span <span
>{( >{(
$events_loc.launcher.sync_intervals.location / 1000 $events_loc.launcher.sync_intervals.location /
).toFixed(1)}s</span 1000
> ).toFixed(1)}s</span>
</div> </div>
<div <div
class="flex justify-between text-[9px] opacity-60 font-mono border-t border-surface-500/10 pt-1" class="border-surface-500/10 flex justify-between border-t pt-1 font-mono text-[9px] opacity-60">
>
<span>Prefix Sharding:</span> <span>Prefix Sharding:</span>
<span <span
>{$ae_loc.native_device?.hash_prefix_length || 2} chars</span >{$ae_loc.native_device?.hash_prefix_length || 2} chars</span>
>
</div> </div>
</div> </div>
<div class="text-center"> <div class="text-center">
<p class="text-[8px] opacity-40 italic"> <p class="text-[8px] italic opacity-40">
Enable Edit Mode to adjust polling intervals. Enable Edit Mode to adjust polling intervals.
</p> </p>
</div> </div>
{/if} {/if}
</div> </div>
{:else} {:else}
<div class="text-center p-4 opacity-50 italic text-xs"> <div class="p-4 text-center text-xs italic opacity-50">
Device configuration not loaded. Device configuration not loaded.
</div> </div>
{/if} {/if}

View File

@@ -1,41 +1,48 @@
<script lang="ts"> <script lang="ts">
/** /**
* Launcher_Cfg_Template.svelte * Launcher_Cfg_Template.svelte
* A "Kitchen Sink" scaffold demonstrating all standard UI patterns * A "Kitchen Sink" scaffold demonstrating all standard UI patterns
* for the Launcher Configuration overhaul. * for the Launcher Configuration overhaul.
*/ */
import { ae_loc, ae_api, ae_sess } from '$lib/stores/ae_stores'; import { ae_loc, ae_api, ae_sess } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores'; import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte'; import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { AlertTriangle, Boxes, RefreshCw, Settings, Trash2, Zap } from '@lucide/svelte'; import {
interface Props { AlertTriangle,
on_expand?: () => void; Boxes,
} RefreshCw,
let { on_expand }: Props = $props(); Settings,
Trash2,
Zap
} from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
// --- 1. LOCAL STATE --- // --- 1. LOCAL STATE ---
let toggle_val = $state(false); let toggle_val = $state(false);
let text_input = $state(''); let text_input = $state('');
let number_input = $state(100); let number_input = $state(100);
let select_val = $state('option1'); let select_val = $state('option1');
let is_loading = $state(false); let is_loading = $state(false);
let action_status = $state(''); let action_status = $state('');
// --- 2. LOGIC HANDLERS --- // --- 2. LOGIC HANDLERS ---
async function handle_test_action(label: string) { async function handle_test_action(label: string) {
is_loading = true; is_loading = true;
action_status = `Executing ${label}...`; action_status = `Executing ${label}...`;
// Simulate async work // Simulate async work
await new Promise((r) => setTimeout(r, 1500)); await new Promise((r) => setTimeout(r, 1500));
is_loading = false; is_loading = false;
action_status = `Finished: ${label}`; action_status = `Finished: ${label}`;
setTimeout(() => (action_status = ''), 3000); setTimeout(() => (action_status = ''), 3000);
} }
// Modal state for destructive actions // Modal state for destructive actions
let show_confirm = $state(false); let show_confirm = $state(false);
</script> </script>
<Launcher_Cfg_Section <Launcher_Cfg_Section
@@ -43,100 +50,88 @@
icon={Boxes} icon={Boxes}
bind:state={$events_loc.launcher.section_state__template} bind:state={$events_loc.launcher.section_state__template}
{on_expand} {on_expand}
description="Kitchen Sink Scaffold | Demo Only" description="Kitchen Sink Scaffold | Demo Only">
>
<!-- A. TOP STATUS BAR (Optional) --> <!-- A. TOP STATUS BAR (Optional) -->
{#if action_status} {#if action_status}
<div <div
class="col-span-full text-[10px] text-center italic bg-primary-500/10 py-1 rounded animate-pulse text-primary-500 border border-primary-500/20" class="bg-primary-500/10 text-primary-500 border-primary-500/20 col-span-full animate-pulse rounded border py-1 text-center text-[10px] italic">
>
{action_status} {action_status}
</div> </div>
{/if} {/if}
<!-- B. COMMON GRID SECTION (Read Only / High Level) --> <!-- B. COMMON GRID SECTION (Read Only / High Level) -->
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1" <p class="ml-1 text-[9px] font-bold uppercase opacity-50">
>Standard Actions</p Standard Actions
> </p>
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="grid grid-cols-2 gap-1"> <div class="grid grid-cols-2 gap-1">
<button <button
type="button" type="button"
onclick={() => handle_test_action('Primary')} onclick={() => handle_test_action('Primary')}
class="btn btn-xs preset-tonal-primary hover:preset-filled-primary-500 justify-start" class="btn btn-xs preset-tonal-primary hover:preset-filled-primary-500 justify-start">
>
<Zap size="0.85em" class="mr-1 shrink-0" /> Primary <Zap size="0.85em" class="mr-1 shrink-0" /> Primary
</button> </button>
<button <button
type="button" type="button"
onclick={() => handle_test_action('Secondary')} onclick={() => handle_test_action('Secondary')}
class="btn btn-xs preset-tonal-secondary hover:preset-filled-secondary-500 justify-start" class="btn btn-xs preset-tonal-secondary hover:preset-filled-secondary-500 justify-start">
>
<Settings size="0.85em" class="mr-1 shrink-0" /> Secondary <Settings size="0.85em" class="mr-1 shrink-0" /> Secondary
</button> </button>
</div> </div>
<!-- Toggles & Checkboxes --> <!-- Toggles & Checkboxes -->
<div class="flex flex-col gap-1 mt-1 bg-surface-500/5 p-2 rounded"> <div class="bg-surface-500/5 mt-1 flex flex-col gap-1 rounded p-2">
<label class="flex items-center gap-2 cursor-pointer group"> <label class="group flex cursor-pointer items-center gap-2">
<input <input
type="checkbox" type="checkbox"
bind:checked={toggle_val} bind:checked={toggle_val}
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/>
<span <span
class="text-xs group-hover:text-primary-500 transition-colors" class="group-hover:text-primary-500 text-xs transition-colors"
>Toggle Feature Alpha</span >Toggle Feature Alpha</span>
>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer group"> <label class="group flex cursor-pointer items-center gap-2">
<input <input
type="radio" type="radio"
name="demo" name="demo"
value="a" value="a"
class="radio radio-sm" class="radio radio-sm" />
/>
<span <span
class="text-xs group-hover:text-primary-500 transition-colors" class="group-hover:text-primary-500 text-xs transition-colors"
>Mode A</span >Mode A</span>
>
</label> </label>
</div> </div>
</div> </div>
<!-- C. STATUS & GAUGES SECTION --> <!-- C. STATUS & GAUGES SECTION -->
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1" <p class="ml-1 text-[9px] font-bold uppercase opacity-50">
>Current Status</p Current Status
> </p>
<div <div
class="flex flex-col gap-2 p-2 border border-surface-500/10 rounded-lg" class="border-surface-500/10 flex flex-col gap-2 rounded-lg border p-2">
> <div class="flex items-center justify-between">
<div class="flex justify-between items-center">
<span class="text-[10px] font-medium">Engine Health</span> <span class="text-[10px] font-medium">Engine Health</span>
<span class="badge preset-filled-success text-[8px] uppercase" <span class="badge preset-filled-success text-[8px] uppercase"
>Stable</span >Stable</span>
>
</div> </div>
<!-- Progress / Gauge Example --> <!-- Progress / Gauge Example -->
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div <div
class="flex justify-between text-[8px] uppercase opacity-60" class="flex justify-between text-[8px] uppercase opacity-60">
>
<span>Processing Load</span> <span>Processing Load</span>
<span>45%</span> <span>45%</span>
</div> </div>
<div <div
class="w-full h-1.5 bg-surface-500/20 rounded-full overflow-hidden" class="bg-surface-500/20 h-1.5 w-full overflow-hidden rounded-full">
>
<div <div
class="h-full bg-success-500 transition-all duration-1000" class="bg-success-500 h-full transition-all duration-1000"
style="width: 45%" style="width: 45%">
></div> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -144,9 +139,10 @@
<button <button
type="button" type="button"
onclick={() => handle_test_action('Refresh')} onclick={() => handle_test_action('Refresh')}
class="btn btn-xs preset-outlined-surface-500 w-full text-[10px]" class="btn btn-xs preset-outlined-surface-500 w-full text-[10px]">
> <RefreshCw
<RefreshCw size="0.85em" class="mr-1 {is_loading ? 'animate-spin' : ''}" /> size="0.85em"
class="mr-1 {is_loading ? 'animate-spin' : ''}" />
Refresh State Refresh State
</button> </button>
</div> </div>
@@ -154,32 +150,28 @@
<!-- D. TECHNICAL SECTION (Edit Mode Only) --> <!-- D. TECHNICAL SECTION (Edit Mode Only) -->
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<div <div
class="col-span-full border-t border-surface-500/20 pt-3 mt-1 flex flex-col gap-3" class="border-surface-500/20 col-span-full mt-1 flex flex-col gap-3 border-t pt-3">
>
<!-- Dangerous Actions --> <!-- Dangerous Actions -->
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span <span
class="text-[9px] font-bold uppercase opacity-50 text-warning-500" class="text-warning-500 text-[9px] font-bold uppercase opacity-50"
>System Config</span >System Config</span>
>
<button <button
type="button" type="button"
onclick={() => (show_confirm = true)} onclick={() => (show_confirm = true)}
class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 justify-start" class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 justify-start">
> <AlertTriangle size="0.85em" class="mr-1 shrink-0" /> Reset
<AlertTriangle size="0.85em" class="mr-1 shrink-0" /> Reset All All
</button> </button>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span <span
class="text-[9px] font-bold uppercase opacity-50 text-error-500" class="text-error-500 text-[9px] font-bold uppercase opacity-50"
>Danger Zone</span >Danger Zone</span>
>
<button <button
type="button" type="button"
class="btn btn-xs preset-tonal-error hover:preset-filled-error-500 justify-start" class="btn btn-xs preset-tonal-error hover:preset-filled-error-500 justify-start">
>
<Trash2 size="0.85em" class="mr-1 shrink-0" /> Wipe Cache <Trash2 size="0.85em" class="mr-1 shrink-0" /> Wipe Cache
</button> </button>
</div> </div>
@@ -188,21 +180,17 @@
<!-- Form Inputs --> <!-- Form Inputs -->
<div class="grid grid-cols-1 gap-2"> <div class="grid grid-cols-1 gap-2">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span <span class="ml-1 text-[9px] font-bold uppercase opacity-50"
class="text-[9px] font-bold uppercase opacity-50 ml-1" >Raw Settings</span>
>Raw Settings</span
>
<div class="flex gap-1"> <div class="flex gap-1">
<input <input
type="text" type="text"
bind:value={text_input} bind:value={text_input}
placeholder="Enter string parameter..." placeholder="Enter string parameter..."
class="input input-sm grow text-[10px] preset-tonal-surface h-7" class="input input-sm preset-tonal-surface h-7 grow text-[10px]" />
/>
<select <select
bind:value={select_val} bind:value={select_val}
class="select select-sm h-7 py-0 text-[10px] w-24 preset-tonal-surface" class="select select-sm preset-tonal-surface h-7 w-24 py-0 text-[10px]">
>
<option value="option1">Global</option> <option value="option1">Global</option>
<option value="option2">Local</option> <option value="option2">Local</option>
</select> </select>
@@ -210,24 +198,22 @@
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60 ml-1" <span class="ml-1 text-[8px] opacity-60"
>Threshold (ms)</span >Threshold (ms)</span>
>
<input <input
type="number" type="number"
bind:value={number_input} bind:value={number_input}
class="input input-sm text-[10px] h-7 preset-tonal-surface" class="input input-sm preset-tonal-surface h-7 text-[10px]" />
/>
</div> </div>
</div> </div>
<!-- Terminal / Output Log --> <!-- Terminal / Output Log -->
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1" <p class="ml-1 text-[9px] font-bold uppercase opacity-50">
>Debug Output</p Debug Output
> </p>
<pre <pre
class="text-[8px] bg-black text-green-500 p-2 overflow-x-auto rounded border border-surface-500/50 max-h-24 shadow-inner"> class="border-surface-500/50 max-h-24 overflow-x-auto rounded border bg-black p-2 text-[8px] text-green-500 shadow-inner">
[LOG] System Initialized [LOG] System Initialized
[INFO] Store synced with IndexedDB [INFO] Store synced with IndexedDB
[DEBUG] active_tab: template [DEBUG] active_tab: template
@@ -241,13 +227,11 @@
<!-- Confirmation Modal Demo --> <!-- Confirmation Modal Demo -->
{#if show_confirm} {#if show_confirm}
<div <div
class="fixed inset-0 z-[1000] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4" class="fixed inset-0 z-[1000] flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm">
>
<div <div
class="card p-6 w-full max-w-sm preset-filled-surface-100-900 border border-warning-500 shadow-2xl animate-in zoom-in-95 duration-200" class="card preset-filled-surface-100-900 border-warning-500 animate-in zoom-in-95 w-full max-w-sm border p-6 shadow-2xl duration-200">
> <h4 class="h4 text-warning-500 mb-2 font-bold">Confirm Action</h4>
<h4 class="h4 text-warning-500 font-bold mb-2">Confirm Action</h4> <p class="mb-6 text-sm opacity-80">
<p class="text-sm opacity-80 mb-6">
Are you sure you want to perform this test operation? This Are you sure you want to perform this test operation? This
demonstrate the standard confirmation pattern. demonstrate the standard confirmation pattern.
</p> </p>
@@ -255,16 +239,14 @@
<button <button
type="button" type="button"
onclick={() => (show_confirm = false)} onclick={() => (show_confirm = false)}
class="btn btn-sm preset-tonal-surface">Cancel</button class="btn btn-sm preset-tonal-surface">Cancel</button>
>
<button <button
type="button" type="button"
onclick={() => { onclick={() => {
show_confirm = false; show_confirm = false;
handle_test_action('Confirm'); handle_test_action('Confirm');
}} }}
class="btn btn-sm preset-filled-warning" class="btn btn-sm preset-filled-warning">
>
Yes, Proceed Yes, Proceed
</button> </button>
</div> </div>

View File

@@ -1,57 +1,55 @@
<script lang="ts"> <script lang="ts">
import { ae_loc, ae_api, ae_sess } from '$lib/stores/ae_stores'; import { ae_loc, ae_api, ae_sess } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores'; import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import * as native from '$lib/electron/electron_relay'; import * as native from '$lib/electron/electron_relay';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte'; import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { CloudDownload, LoaderCircle, Search, Wand2 } from '@lucide/svelte'; import { CloudDownload, LoaderCircle, Search, Wand2 } from '@lucide/svelte';
interface Props { interface Props {
on_expand?: () => void; on_expand?: () => void;
} }
let { on_expand }: Props = $props(); let { on_expand }: Props = $props();
let update_source: 'url' | 'file' = $state('file'); let update_source: 'url' | 'file' = $state('file');
let update_path = $state( let update_path = $state(
'~/OSIT/Speaker Ready System/Admin Share/Custom Applications/osit_binaries/' '~/OSIT/Speaker Ready System/Admin Share/Custom Applications/osit_binaries/'
); );
let update_url = $state( let update_url = $state('https://dev-demo.oneskyit.com/updates/ae_native.zip');
'https://dev-demo.oneskyit.com/updates/ae_native.zip'
);
let update_status = $state(''); let update_status = $state('');
let is_checking = $state(false); let is_checking = $state(false);
let download_result = $state<any>(null); let download_result = $state<any>(null);
async function handle_check_update() { async function handle_check_update() {
is_checking = true; is_checking = true;
update_status = 'Checking for updates...'; update_status = 'Checking for updates...';
try { try {
const args = const args =
update_source === 'url' update_source === 'url'
? { source: 'url' as const, url: update_url } ? { source: 'url' as const, url: update_url }
: { source: 'file' as const, path: update_path }; : { source: 'file' as const, path: update_path };
const res = await native.update_app(args); const res = await native.update_app(args);
if (res.success) { if (res.success) {
download_result = res; download_result = res;
update_status = 'Update located/downloaded. Ready to install.'; update_status = 'Update located/downloaded. Ready to install.';
} else { } else {
update_status = `Failed: ${res.error}`; update_status = `Failed: ${res.error}`;
}
} catch (err: any) {
update_status = `Error: ${err.message}`;
} finally {
is_checking = false;
} }
} catch (err: any) {
update_status = `Error: ${err.message}`;
} finally {
is_checking = false;
} }
}
async function handle_install() { async function handle_install() {
update_status = 'Initiating installation...'; update_status = 'Initiating installation...';
alert( alert(
'Installation logic is OS-specific. This will typically swap the application bundle and restart.' 'Installation logic is OS-specific. This will typically swap the application bundle and restart.'
); );
} }
</script> </script>
<Launcher_Cfg_Section <Launcher_Cfg_Section
@@ -59,35 +57,31 @@
icon={CloudDownload} icon={CloudDownload}
bind:state={$events_loc.launcher.section_state__updates} bind:state={$events_loc.launcher.section_state__updates}
{on_expand} {on_expand}
description="v1.0.0 | Source: {update_source}" description="v1.0.0 | Source: {update_source}">
>
<!-- Content omitted for brevity, preserved in file --> <!-- Content omitted for brevity, preserved in file -->
<div class="col-span-full flex flex-col gap-2"> <div class="col-span-full flex flex-col gap-2">
<!-- TECHNICAL: Source Config (Edit Mode Only) --> <!-- TECHNICAL: Source Config (Edit Mode Only) -->
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<div <div
class="flex flex-col gap-2 bg-surface-500/5 p-2 rounded border border-surface-500/10 mb-1" class="bg-surface-500/5 border-surface-500/10 mb-1 flex flex-col gap-2 rounded border p-2">
> <div class="flex flex-row items-center justify-between px-1">
<div class="flex flex-row justify-between items-center px-1"> <p class="text-[9px] font-bold uppercase opacity-50">
<p class="text-[9px] font-bold uppercase opacity-50" Source Type
>Source Type</p </p>
>
<div class="flex gap-2"> <div class="flex gap-2">
<label class="flex items-center gap-1 text-[10px]"> <label class="flex items-center gap-1 text-[10px]">
<input <input
type="radio" type="radio"
bind:group={update_source} bind:group={update_source}
value="file" value="file"
class="radio radio-sm" class="radio radio-sm" /> Local
/> Local
</label> </label>
<label class="flex items-center gap-1 text-[10px]"> <label class="flex items-center gap-1 text-[10px]">
<input <input
type="radio" type="radio"
bind:group={update_source} bind:group={update_source}
value="url" value="url"
class="radio radio-sm" class="radio radio-sm" /> Web
/> Web
</label> </label>
</div> </div>
</div> </div>
@@ -97,15 +91,13 @@
type="text" type="text"
bind:value={update_path} bind:value={update_path}
placeholder="Path to update package" placeholder="Path to update package"
class="input input-sm text-[10px] preset-tonal-surface h-7 w-full" class="input input-sm preset-tonal-surface h-7 w-full text-[10px]" />
/>
{:else} {:else}
<input <input
type="text" type="text"
bind:value={update_url} bind:value={update_url}
placeholder="URL to update package" placeholder="URL to update package"
class="input input-sm text-[10px] preset-tonal-surface h-7 w-full" class="input input-sm preset-tonal-surface h-7 w-full text-[10px]" />
/>
{/if} {/if}
</div> </div>
{/if} {/if}
@@ -115,10 +107,9 @@
type="button" type="button"
onclick={handle_check_update} onclick={handle_check_update}
disabled={is_checking} disabled={is_checking}
class="btn btn-sm preset-filled-tertiary hover:preset-filled-primary-500 text-[10px] w-full" class="btn btn-sm preset-filled-tertiary hover:preset-filled-primary-500 w-full text-[10px]">
>
{#if is_checking} {#if is_checking}
<LoaderCircle size="0.85em" class="animate-spin mr-1" /> Checking... <LoaderCircle size="0.85em" class="mr-1 animate-spin" /> Checking...
{:else} {:else}
<Search size="0.85em" class="mr-1" /> Check for Updates <Search size="0.85em" class="mr-1" /> Check for Updates
{/if} {/if}
@@ -126,8 +117,7 @@
{#if update_status} {#if update_status}
<div <div
class="text-[9px] text-center italic p-1 border border-surface-500/20 rounded bg-surface-500/5 mt-1" class="border-surface-500/20 bg-surface-500/5 mt-1 rounded border p-1 text-center text-[9px] italic">
>
{update_status} {update_status}
</div> </div>
{/if} {/if}
@@ -136,8 +126,7 @@
<button <button
type="button" type="button"
onclick={handle_install} onclick={handle_install}
class="btn btn-sm preset-filled-success hover:preset-filled-primary-500 text-[10px] w-full animate-bounce mt-2 shadow-lg" class="btn btn-sm preset-filled-success hover:preset-filled-primary-500 mt-2 w-full animate-bounce text-[10px] shadow-lg">
>
<Wand2 size="0.85em" class="mr-1" /> Install & Relaunch <Wand2 size="0.85em" class="mr-1" /> Install & Relaunch
</button> </button>
{/if} {/if}

File diff suppressed because it is too large Load Diff

View File

@@ -1,54 +1,54 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
data: any; data: any;
} }
let { data }: Props = $props(); let { data }: Props = $props();
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { ae_api } from '$lib/stores/ae_stores'; import { ae_api } from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events'; import { db_events } from '$lib/ae_events/db_events';
import { events_func } from '$lib/ae_events/ae_events_functions'; import { events_func } from '$lib/ae_events/ae_events_functions';
// Magic redirect: when ?session_id= is present but no event_location_id is in the route // Magic redirect: when ?session_id= is present but no event_location_id is in the route
// (e.g. navigating here from the Presenter View which has no location context), // (e.g. navigating here from the Presenter View which has no location context),
// look up the session's location and redirect to the proper location-specific launcher URL. // look up the session's location and redirect to the proper location-specific launcher URL.
// The [event_location_id] page handles the normal case where location is already in the route. // The [event_location_id] page handles the normal case where location is already in the route.
$effect(() => { $effect(() => {
if (!browser) return; if (!browser) return;
const session_id = data.url.searchParams.get('session_id'); const session_id = data.url.searchParams.get('session_id');
const event_id = data.params.event_id; const event_id = data.params.event_id;
if (!session_id || !event_id) return; if (!session_id || !event_id) return;
// Snapshot API config to avoid making it a reactive dependency of this effect // Snapshot API config to avoid making it a reactive dependency of this effect
const api_cfg = $ae_api; const api_cfg = $ae_api;
(async () => { (async () => {
// Check Dexie cache first — sessions are typically cached from prior page visits // Check Dexie cache first — sessions are typically cached from prior page visits
// Typed as any: Dexie returns Session|undefined, API returns ae_EventSession|null — both duck-type fine // Typed as any: Dexie returns Session|undefined, API returns ae_EventSession|null — both duck-type fine
let session: any = await db_events.session.get(session_id); let session: any = await db_events.session.get(session_id);
if (!session) { if (!session) {
// Not cached — fetch from API and save to Dexie // Not cached — fetch from API and save to Dexie
session = await events_func.load_ae_obj_id__event_session({ session = await events_func.load_ae_obj_id__event_session({
api_cfg, api_cfg,
event_session_id: session_id, event_session_id: session_id,
try_cache: false, try_cache: false,
log_lvl: 0 log_lvl: 0
}); });
} }
if (session?.event_location_id) { if (session?.event_location_id) {
// Session has a location — redirect to the location-specific launcher URL. // Session has a location — redirect to the location-specific launcher URL.
// replaceState: true so the user doesn't need to hit back twice to leave the launcher. // replaceState: true so the user doesn't need to hit back twice to leave the launcher.
goto( goto(
`/events/${event_id}/launcher/${session.event_location_id}?session_id=${session_id}`, `/events/${event_id}/launcher/${session.event_location_id}?session_id=${session_id}`,
{ replaceState: true } { replaceState: true }
); );
} }
// If the session has no location set, the user stays on the base launcher page // If the session has no location set, the user stays on the base launcher page
// and the "Please select a location from the menu" prompt is shown by the layout. // and the "Please select a location from the menu" prompt is shown by the layout.
})(); })();
}); });
</script> </script>

View File

@@ -1,76 +1,75 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
/** @type {import('./$types').PageData} */ /** @type {import('./$types').PageData} */
data: any; data: any;
}
let { data }: Props = $props();
let log_lvl: number = $state(0);
// Imports
import { untrack } from 'svelte';
import { ae_loc, ae_sess, ae_api } from '$lib/stores/ae_stores';
import {
events_loc,
events_sess,
events_slct
} from '$lib/stores/ae_events_stores';
// NOTE: Derived from data.account_id (prop) instead of $slct.account_id (store)
// to prevent circular dependency loops during hydration.
let ae_acct = $derived(data[data.account_id]);
let url_event_id = $derived(data.params.event_id);
let url_event_location_id = $derived(data.params.event_location_id);
$effect(() => {
if (log_lvl > 1) {
console.log(`event_id: ${url_event_id}`);
console.log(`event_location_id: ${url_event_location_id}`);
} }
untrack(() => {
$events_slct.event_id = url_event_id;
$events_slct.event_location_id = url_event_location_id;
});
});
let { data }: Props = $props(); $effect(() => {
let log_lvl: number = $state(0); if (ae_acct) {
// Imports
import { untrack } from 'svelte';
import {
ae_loc,
ae_sess,
ae_api,
} from '$lib/stores/ae_stores';
import {
events_loc,
events_sess,
events_slct,
} from '$lib/stores/ae_events_stores';
// NOTE: Derived from data.account_id (prop) instead of $slct.account_id (store)
// to prevent circular dependency loops during hydration.
let ae_acct = $derived(data[data.account_id]);
let url_event_id = $derived(data.params.event_id);
let url_event_location_id = $derived(data.params.event_location_id);
$effect(() => {
if (log_lvl > 1) {
console.log(`event_id: ${url_event_id}`);
console.log(`event_location_id: ${url_event_location_id}`);
}
untrack(() => { untrack(() => {
$events_slct.event_id = url_event_id; $events_slct.event_location_obj_li = ae_acct.slct
$events_slct.event_location_id = url_event_location_id; .event_location_obj_li ?? [''];
$events_slct.id_li__event_location = ae_acct.slct
.id_li__event_location ?? [''];
$events_slct.event_session_obj_li = ae_acct.slct
.event_session_obj_li ?? [''];
}); });
});
$effect(() => {
if (ae_acct) {
untrack(() => {
$events_slct.event_location_obj_li = ae_acct.slct.event_location_obj_li ?? [''];
$events_slct.id_li__event_location = ae_acct.slct.id_li__event_location ?? [''];
$events_slct.event_session_obj_li = ae_acct.slct.event_session_obj_li ?? [''];
});
}
});
// Set localStorage defaults for launcher state
if (!$events_loc.launcher) {
$events_loc.launcher = {};
$events_loc.launcher.slct = { event_id: null };
$events_loc.launcher.show_content__session_code = true;
$events_loc.launcher.show_content__presentation_code = true;
$events_loc.launcher.show_content__presenter_code = true;
}
if (!$events_loc.launcher.slct) {
$events_loc.launcher.slct = {
event_id: null,
event_location_id: null,
event_session_id: null,
event_presentation_id: null,
event_presenter_id: null
};
} }
});
// Set session storage defaults // Set localStorage defaults for launcher state
if (!$events_sess.launcher) $events_sess.launcher = {}; if (!$events_loc.launcher) {
$events_sess.launcher.show_content__session_code = true; $events_loc.launcher = {};
$events_sess.launcher.show_content__presentation_code = true; $events_loc.launcher.slct = { event_id: null };
$events_sess.launcher.show_content__presenter_code = true; $events_loc.launcher.show_content__session_code = true;
$events_loc.launcher.show_content__presentation_code = true;
$events_loc.launcher.show_content__presenter_code = true;
}
if (!$events_loc.launcher.slct) {
$events_loc.launcher.slct = {
event_id: null,
event_location_id: null,
event_session_id: null,
event_presentation_id: null,
event_presenter_id: null
};
}
// Set session storage defaults
if (!$events_sess.launcher) $events_sess.launcher = {};
$events_sess.launcher.show_content__session_code = true;
$events_sess.launcher.show_content__presentation_code = true;
$events_sess.launcher.show_content__presenter_code = true;
</script> </script>
<div class="hidden">This is for forcing data loading.</div> <div class="hidden">This is for forcing data loading.</div>

View File

@@ -71,7 +71,8 @@ export async function load({ params, parent, url }) {
const session_id = url.searchParams.get('session_id'); const session_id = url.searchParams.get('session_id');
if (browser && session_id) { if (browser && session_id) {
if (log_lvl) console.log(`Triggering deep load for session_id: ${session_id}`); if (log_lvl)
console.log(`Triggering deep load for session_id: ${session_id}`);
events_func.load_ae_obj_id__event_session({ events_func.load_ae_obj_id__event_session({
api_cfg: ae_acct.api, api_cfg: ae_acct.api,
event_session_id: session_id, event_session_id: session_id,

View File

@@ -1,78 +1,84 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
} }
let { log_lvl = 0 }: Props = $props(); let { log_lvl = 0 }: Props = $props();
import { import {
ae_snip, ae_snip,
ae_loc, ae_loc,
ae_sess, ae_sess,
ae_api, ae_api,
ae_trig, ae_trig,
slct, slct,
slct_trigger, slct_trigger,
time time
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
import { import {
events_loc, events_loc,
events_sess, events_sess,
events_slct, events_slct,
events_trigger, events_trigger,
events_trig events_trig
} from '$lib/stores/ae_events_stores'; } from '$lib/stores/ae_events_stores';
// Sub-components // Sub-components
import Launcher_Cfg_Native_OS from './cfg_components/launcher_cfg_native_os.svelte'; import Launcher_Cfg_Native_OS from './cfg_components/launcher_cfg_native_os.svelte';
import Launcher_Cfg_Sync_Timers from './cfg_components/launcher_cfg_sync_timers.svelte'; import Launcher_Cfg_Sync_Timers from './cfg_components/launcher_cfg_sync_timers.svelte';
import Launcher_Cfg_Health from './cfg_components/launcher_cfg_health.svelte'; import Launcher_Cfg_Health from './cfg_components/launcher_cfg_health.svelte';
import Launcher_Cfg_Updates from './cfg_components/launcher_cfg_updates.svelte'; import Launcher_Cfg_Updates from './cfg_components/launcher_cfg_updates.svelte';
import Launcher_Cfg_Controller from './cfg_components/launcher_cfg_controller.svelte'; import Launcher_Cfg_Controller from './cfg_components/launcher_cfg_controller.svelte';
import Launcher_Cfg_Screen_Saver from './cfg_components/launcher_cfg_screen_saver.svelte'; import Launcher_Cfg_Screen_Saver from './cfg_components/launcher_cfg_screen_saver.svelte';
import Launcher_Cfg_App_Modes from './cfg_components/launcher_cfg_app_modes.svelte'; import Launcher_Cfg_App_Modes from './cfg_components/launcher_cfg_app_modes.svelte';
import Launcher_Cfg_Local_Actions from './cfg_components/launcher_cfg_local_actions.svelte'; import Launcher_Cfg_Local_Actions from './cfg_components/launcher_cfg_local_actions.svelte';
import { Bug, Code, Monitor, Pencil, RefreshCw, Settings, SlidersHorizontal, X } from '@lucide/svelte'; import {
// UI Tab State Bug,
// Tabs are audience-oriented: Code,
// setup — what every onsite operator needs (mode preset, display, WS, screen saver) Monitor,
// device — sync engine (all devices) + native/Electron OS controls (native or edit_mode) Pencil,
// dev — developer/debug tools; only useful when edit_mode is on RefreshCw,
let active_tab: 'setup' | 'device' | 'dev' = $state('setup'); Settings,
SlidersHorizontal,
X
} from '@lucide/svelte';
// UI Tab State
// Tabs are audience-oriented:
// setup — what every onsite operator needs (mode preset, display, WS, screen saver)
// device — sync engine (all devices) + native/Electron OS controls (native or edit_mode)
// dev — developer/debug tools; only useful when edit_mode is on
let active_tab: 'setup' | 'device' | 'dev' = $state('setup');
/** /**
* Auto-Collapse Coordinator * Auto-Collapse Coordinator
* When a section is opened in 'auto' mode, collapse all other 'auto' sections. * When a section is opened in 'auto' mode, collapse all other 'auto' sections.
* Pinned sections are ignored and remain open. * Pinned sections are ignored and remain open.
*/ */
function handle_section_expand(current_key: string) { function handle_section_expand(current_key: string) {
const launcher = $events_loc.launcher; const launcher = $events_loc.launcher;
Object.keys(launcher).forEach((key) => { Object.keys(launcher).forEach((key) => {
if ( if (
key.startsWith('section_state__') && key.startsWith('section_state__') &&
key !== `section_state__${current_key}` key !== `section_state__${current_key}`
) { ) {
if (launcher[key] === 'auto') { if (launcher[key] === 'auto') {
launcher[key] = 'collapsed'; launcher[key] = 'collapsed';
}
} }
}); }
$events_loc.launcher = launcher; // Trigger store update });
} $events_loc.launcher = launcher; // Trigger store update
}
</script> </script>
<div <div
class=" class="
w-full max-w-full flex w-full
flex flex-col gap-4 items-center justify-start max-w-full flex-col items-center justify-start gap-4
" ">
>
<div <div
class="w-full flex flex-row items-center justify-between border-b border-surface-500/20 pb-2" class="border-surface-500/20 flex w-full flex-row items-center justify-between border-b pb-2">
>
<h2 <h2
class="text-center text-lg font-bold text-gray-700 dark:text-gray-200" class="text-center text-lg font-bold text-gray-700 dark:text-gray-200">
>
<Settings size="1em" class="mr-2 opacity-50" /> <Settings size="1em" class="mr-2 opacity-50" />
Launcher Configuration Launcher Configuration
</h2> </h2>
@@ -88,8 +94,7 @@
class:text-primary-500={$ae_loc.edit_mode} class:text-primary-500={$ae_loc.edit_mode}
class:opacity-20={!$ae_loc.edit_mode} class:opacity-20={!$ae_loc.edit_mode}
class:hover:opacity-60={!$ae_loc.edit_mode} class:hover:opacity-60={!$ae_loc.edit_mode}
title="{$ae_loc.edit_mode ? 'Disable' : 'Enable'} Edit Mode" title="{$ae_loc.edit_mode ? 'Disable' : 'Enable'} Edit Mode">
>
<Pencil size="0.75em" /> <Pencil size="0.75em" />
<span class="sr-only">Toggle Edit Mode</span> <span class="sr-only">Toggle Edit Mode</span>
</button> </button>
@@ -97,8 +102,7 @@
<button <button
type="button" type="button"
onclick={() => ($events_loc.launcher.hide_drawer__cfg = true)} onclick={() => ($events_loc.launcher.hide_drawer__cfg = true)}
class="btn btn-icon dark:text-white hover:bg-surface-500/10 transition-colors" class="btn btn-icon hover:bg-surface-500/10 transition-colors dark:text-white">
>
<X size="1em" /> <X size="1em" />
<span class="sr-only">Close Config</span> <span class="sr-only">Close Config</span>
</button> </button>
@@ -110,88 +114,80 @@
for onsite operators who never need those tools. Edit Mode is toggled via for onsite operators who never need those tools. Edit Mode is toggled via
the pencil icon in the header above. --> the pencil icon in the header above. -->
<div <div
class="w-full gap-1 bg-surface-500/10 p-1 rounded-lg" class="bg-surface-500/10 w-full gap-1 rounded-lg p-1"
class:grid={true} class:grid={true}
class:grid-cols-2={!$ae_loc.edit_mode} class:grid-cols-2={!$ae_loc.edit_mode}
class:grid-cols-3={$ae_loc.edit_mode} class:grid-cols-3={$ae_loc.edit_mode}>
>
<button <button
type="button" type="button"
onclick={() => (active_tab = 'setup')} onclick={() => (active_tab = 'setup')}
class="btn btn-sm text-[10px] uppercase font-bold transition-all" class="btn btn-sm text-[10px] font-bold uppercase transition-all"
class:preset-filled-primary={active_tab === 'setup'} class:preset-filled-primary={active_tab === 'setup'}
class:preset-tonal-surface={active_tab !== 'setup'} class:preset-tonal-surface={active_tab !== 'setup'}
title="Display presets, interface toggles, WS controller, screen saver" title="Display presets, interface toggles, WS controller, screen saver">
>
<SlidersHorizontal size="0.85em" class="mr-1" /> Setup <SlidersHorizontal size="0.85em" class="mr-1" /> Setup
</button> </button>
<button <button
type="button" type="button"
onclick={() => (active_tab = 'device')} onclick={() => (active_tab = 'device')}
class="btn btn-sm text-[10px] uppercase font-bold transition-all" class="btn btn-sm text-[10px] font-bold uppercase transition-all"
class:preset-filled-primary={active_tab === 'device'} class:preset-filled-primary={active_tab === 'device'}
class:preset-tonal-surface={active_tab !== 'device'} class:preset-tonal-surface={active_tab !== 'device'}
title="Sync engine, device health &amp; native OS controls" title="Sync engine, device health &amp; native OS controls">
>
<Monitor size="0.85em" class="mr-1" /> Device <Monitor size="0.85em" class="mr-1" /> Device
</button> </button>
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<button <button
type="button" type="button"
onclick={() => (active_tab = 'dev')} onclick={() => (active_tab = 'dev')}
class="btn btn-sm text-[10px] uppercase font-bold transition-all" class="btn btn-sm text-[10px] font-bold uppercase transition-all"
class:preset-filled-warning={active_tab === 'dev'} class:preset-filled-warning={active_tab === 'dev'}
class:preset-tonal-surface={active_tab !== 'dev'} class:preset-tonal-surface={active_tab !== 'dev'}
title="Developer &amp; debug tools" title="Developer &amp; debug tools">
>
<Code size="0.85em" class="mr-1" /> Dev <Code size="0.85em" class="mr-1" /> Dev
</button> </button>
{/if} {/if}
</div> </div>
<!-- Tab Content --> <!-- Tab Content -->
<div class="w-full flex flex-col gap-2 min-h-[400px]"> <div class="flex min-h-[400px] w-full flex-col gap-2">
<!-- SETUP: everything onsite operators need day-to-day --> <!-- SETUP: everything onsite operators need day-to-day -->
{#if active_tab === 'setup'} {#if active_tab === 'setup'}
<div class="animate-in fade-in slide-in-from-left-2 duration-300 flex flex-col gap-2"> <div
class="animate-in fade-in slide-in-from-left-2 flex flex-col gap-2 duration-300">
<!-- Mode preset is the #1 onsite action — give it prominent placement --> <!-- Mode preset is the #1 onsite action — give it prominent placement -->
<Launcher_Cfg_App_Modes <Launcher_Cfg_App_Modes
on_expand={() => handle_section_expand('app_modes')} on_expand={() => handle_section_expand('app_modes')} />
/>
<Launcher_Cfg_Controller <Launcher_Cfg_Controller
on_expand={() => handle_section_expand('controller')} on_expand={() => handle_section_expand('controller')} />
/>
<Launcher_Cfg_Screen_Saver <Launcher_Cfg_Screen_Saver
on_expand={() => handle_section_expand('screen_saver')} on_expand={() => handle_section_expand('screen_saver')} />
/>
</div> </div>
{/if} {/if}
<!-- DEVICE: sync engine first (all devices) + native OS controls (native or edit_mode preview) --> <!-- DEVICE: sync engine first (all devices) + native OS controls (native or edit_mode preview) -->
{#if active_tab === 'device'} {#if active_tab === 'device'}
<div class="animate-in fade-in slide-in-from-bottom-2 duration-300 flex flex-col gap-2"> <div
class="animate-in fade-in slide-in-from-bottom-2 flex flex-col gap-2 duration-300">
<!-- Sync pause/timers — relevant to every device, not just native --> <!-- Sync pause/timers — relevant to every device, not just native -->
<Launcher_Cfg_Sync_Timers <Launcher_Cfg_Sync_Timers
on_expand={() => handle_section_expand('sync_timers')} on_expand={() => handle_section_expand('sync_timers')} />
/>
<!-- Native sections: always in Electron; visible in edit_mode for dev preview. <!-- Native sections: always in Electron; visible in edit_mode for dev preview.
electron_relay.ts guards all calls — safe to import/render without Electron. --> electron_relay.ts guards all calls — safe to import/render without Electron. -->
{#if $ae_loc.is_native || $ae_loc.edit_mode} {#if $ae_loc.is_native || $ae_loc.edit_mode}
<Launcher_Cfg_Health <Launcher_Cfg_Health
on_expand={() => handle_section_expand('health')} on_expand={() => handle_section_expand('health')} />
/>
<Launcher_Cfg_Native_OS <Launcher_Cfg_Native_OS
on_expand={() => handle_section_expand('native_os')} on_expand={() => handle_section_expand('native_os')} />
/>
{#if $ae_loc.is_native} {#if $ae_loc.is_native}
<Launcher_Cfg_Updates <Launcher_Cfg_Updates
on_expand={() => handle_section_expand('updates')} on_expand={() =>
/> handle_section_expand('updates')} />
{/if} {/if}
{:else} {:else}
<div class="py-3 text-center opacity-40 italic text-xs flex flex-col gap-1 items-center"> <div
class="flex flex-col items-center gap-1 py-3 text-center text-xs italic opacity-40">
<Monitor size="1.2em" class="opacity-30" /> <Monitor size="1.2em" class="opacity-30" />
<p>Native OS controls available in Aether Desktop.</p> <p>Native OS controls available in Aether Desktop.</p>
<p class="text-[9px]">Enable Edit Mode to preview.</p> <p class="text-[9px]">Enable Edit Mode to preview.</p>
@@ -202,27 +198,24 @@
<!-- DEV: developer/debug tools — only reachable when Edit Mode is on --> <!-- DEV: developer/debug tools — only reachable when Edit Mode is on -->
{#if active_tab === 'dev' && $ae_loc.edit_mode} {#if active_tab === 'dev' && $ae_loc.edit_mode}
<div class="animate-in fade-in slide-in-from-right-2 duration-300 flex flex-col gap-2"> <div
class="animate-in fade-in slide-in-from-right-2 flex flex-col gap-2 duration-300">
<Launcher_Cfg_Local_Actions <Launcher_Cfg_Local_Actions
on_expand={() => handle_section_expand('local_actions')} on_expand={() => handle_section_expand('local_actions')} />
/>
</div> </div>
{/if} {/if}
</div> </div>
<!-- Global Actions Footer --> <!-- Global Actions Footer -->
<div <div
class="w-full flex flex-col gap-2 border-t border-surface-500/20 pt-4 mt-auto" class="border-surface-500/20 mt-auto flex w-full flex-col gap-2 border-t pt-4">
>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<!-- Close button — always visible in lower-left as a second dismissal point. <!-- Close button — always visible in lower-left as a second dismissal point.
Useful in kiosk/iframe mode where the top-right close btn may scroll out of view. --> Useful in kiosk/iframe mode where the top-right close btn may scroll out of view. -->
<button <button
type="button" type="button"
onclick={() => ($events_loc.launcher.hide_drawer__cfg = true)} onclick={() => ($events_loc.launcher.hide_drawer__cfg = true)}
class="btn btn-sm preset-tonal-surface hover:preset-filled-surface-500 transition-all" class="btn btn-sm preset-tonal-surface hover:preset-filled-surface-500 transition-all">
>
<X size="0.85em" class="mr-1" /> <X size="0.85em" class="mr-1" />
Close Close
</button> </button>
@@ -230,8 +223,7 @@
<button <button
type="button" type="button"
onclick={() => location.reload()} onclick={() => location.reload()}
class="btn btn-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition-all" class="btn btn-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition-all">
>
<RefreshCw size="0.85em" class="mr-1" /> <RefreshCw size="0.85em" class="mr-1" />
Reload Reload
</button> </button>
@@ -242,16 +234,14 @@
type="button" type="button"
onclick={() => onclick={() =>
($events_loc.launcher.hide_drawer__debug = false)} ($events_loc.launcher.hide_drawer__debug = false)}
class="btn btn-sm preset-tonal-warning hover:preset-filled-warning-500 transition-all w-full" class="btn btn-sm preset-tonal-warning hover:preset-filled-warning-500 w-full transition-all">
>
<Bug size="0.85em" class="mr-1" /> <Bug size="0.85em" class="mr-1" />
Debug Panel Debug Panel
</button> </button>
{/if} {/if}
<p <p
class="text-[9px] text-center opacity-40 uppercase font-bold tracking-widest mt-2" class="mt-2 text-center text-[9px] font-bold tracking-widest uppercase opacity-40">
>
Aether Platform &bull; Events Launcher v3.0 Aether Platform &bull; Events Launcher v3.0
</p> </p>
</div> </div>

View File

@@ -1,224 +1,235 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
event_file_id: string; event_file_id: string;
event_file_obj: any; event_file_obj: any;
max_filename_length?: number; max_filename_length?: number;
hide_launch_icon?: boolean; hide_launch_icon?: boolean;
hide_meta?: boolean; hide_meta?: boolean;
hide_created_on?: boolean; hide_created_on?: boolean;
hide_os?: boolean; hide_os?: boolean;
hide_size?: boolean; hide_size?: boolean;
hide_draft?: boolean; hide_draft?: boolean;
show_bak_download?: boolean; show_bak_download?: boolean;
btn_size?: string; btn_size?: string;
btn_text_align?: string; btn_text_align?: string;
text_size?: string; text_size?: string;
text_size_md?: string; text_size_md?: string;
session_type?: string; session_type?: string;
open_method?: null | string; open_method?: null | string;
modal_title?: string; modal_title?: string;
modal__title?: any; modal__title?: any;
modal__open_event_file_id?: any; modal__open_event_file_id?: any;
modal__event_file_obj?: any; modal__event_file_obj?: any;
} }
let { let {
log_lvl = $bindable(0), log_lvl = $bindable(0),
event_file_id, event_file_id,
event_file_obj = $bindable({}), event_file_obj = $bindable({}),
max_filename_length = $bindable(50), max_filename_length = $bindable(50),
hide_launch_icon = $bindable(false), hide_launch_icon = $bindable(false),
hide_meta = $bindable(false), hide_meta = $bindable(false),
hide_created_on = $bindable(false), hide_created_on = $bindable(false),
hide_os = $bindable(false), hide_os = $bindable(false),
hide_size = $bindable(false), hide_size = $bindable(false),
hide_draft = $bindable(false), hide_draft = $bindable(false),
show_bak_download = false, show_bak_download = false,
btn_size = $bindable('btn-sm'), btn_size = $bindable('btn-sm'),
btn_text_align = $bindable('text-left'), btn_text_align = $bindable('text-left'),
text_size = $bindable('text-sm'), text_size = $bindable('text-sm'),
text_size_md = $bindable('md:text-base'), text_size_md = $bindable('md:text-base'),
session_type = $bindable('oral'), session_type = $bindable('oral'),
open_method = $bindable('download'), open_method = $bindable('download'),
modal_title = $bindable(''), modal_title = $bindable(''),
modal__title = $bindable(''), modal__title = $bindable(''),
modal__open_event_file_id = $bindable(null), modal__open_event_file_id = $bindable(null),
modal__event_file_obj = $bindable(null) modal__event_file_obj = $bindable(null)
}: Props = $props(); }: Props = $props();
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
import { api } from '$lib/api/api'; import { api } from '$lib/api/api';
import { ae_loc, ae_api, ae_sess, slct } from '$lib/stores/ae_stores'; import { ae_loc, ae_api, ae_sess, slct } from '$lib/stores/ae_stores';
import { core_func } from '$lib/ae_core/ae_core_functions'; import { core_func } from '$lib/ae_core/ae_core_functions';
import { import {
events_loc, events_loc,
events_sess, events_sess,
events_slct events_slct
} from '$lib/stores/ae_events_stores'; } from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions'; import { events_func } from '$lib/ae_events/ae_events_functions';
import { AlertCircle, AlertTriangle, BarChart2, CalendarDays, FolderOpen, Laptop, LoaderCircle, Monitor, Save, Send } from '@lucide/svelte'; import {
import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte'; AlertCircle,
AlertTriangle,
BarChart2,
CalendarDays,
FolderOpen,
Laptop,
LoaderCircle,
Monitor,
Save,
Send
} from '@lucide/svelte';
import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte';
// Import the relay // Import the relay
import * as native from '$lib/electron/electron_relay'; import * as native from '$lib/electron/electron_relay';
let ae_promises: key_val = $state({}); let ae_promises: key_val = $state({});
let open_file_clicked: null | boolean = $state(null); let open_file_clicked: null | boolean = $state(null);
let open_file_status: null | string = $state(null); let open_file_status: null | string = $state(null);
let open_file_status_message: null | string = $state(null); let open_file_status_message: null | string = $state(null);
let screen_saver_exts = ['jpg', 'png', 'PNG', 'webp']; let screen_saver_exts = ['jpg', 'png', 'PNG', 'webp'];
onMount(() => { onMount(() => {
if (screen_saver_exts.includes(event_file_obj.extension)) { if (screen_saver_exts.includes(event_file_obj.extension)) {
if (!$events_loc.launcher.screen_saver_img_kv) if (!$events_loc.launcher.screen_saver_img_kv)
$events_loc.launcher.screen_saver_img_kv = {}; $events_loc.launcher.screen_saver_img_kv = {};
$events_loc.launcher.screen_saver_img_kv[event_file_id] = { $events_loc.launcher.screen_saver_img_kv[event_file_id] = {
...event_file_obj ...event_file_obj
};
}
});
async function handle_open_file() {
if (log_lvl) console.log('*** handle_open_file() ***');
if (open_file_clicked) return; // Hard Guard: Already processing
$events_slct.event_file_id = event_file_id;
$events_slct.event_file_obj = event_file_obj;
// 1. NATIVE MODE (Electron)
if ($ae_loc.is_native && $events_loc.launcher.app_mode === 'native') {
const cache_root = $ae_loc.local_file_cache_path;
const temp_root = $ae_loc.host_file_temp_path;
open_file_clicked = true;
open_file_status = 'checking_cache';
open_file_status_message = 'Checking local cache...';
const exists = await native.check_hash_file_cache({
cache_root,
hash: event_file_obj.hash_sha256,
verify_hash: true // Hardened: Trust No One!
});
if (!exists) {
open_file_status = 'downloading_file';
open_file_status_message = 'Downloading file to cache...';
// Use the PROVEN endpoint path from api.ts that is known to work in Default Mode.
const url = `${$ae_api.base_url}/v3/action/hosted_file/${event_file_obj.hosted_file_id}/download?return_file=true&filename=${encodeURIComponent(event_file_obj.filename)}&key=${$ae_api.account_id}`;
const dl_result = await native.download_to_cache({
url,
cache_root,
hash: event_file_obj.hash_sha256,
api_key: $ae_api.api_secret_key,
account_id: $ae_api.account_id
});
if (!dl_result.success) {
open_file_status = 'error';
open_file_status_message = `Download failed: ${dl_result.error}`;
setTimeout(() => (open_file_clicked = false), 5000);
return false;
}
}
open_file_status = 'opening_file';
open_file_status_message = 'Opening Application';
// Phase 2/5: Use the atomic copy-and-launch operation.
// The main process handler (file_handlers.ts) now handles the
// specialized LibreOffice/AppleScript logic internally after copying.
const launch_result = await native.launch_from_cache({
cache_root,
hash: event_file_obj.hash_sha256,
temp_root,
filename: event_file_obj.filename
});
if (!launch_result.success) {
open_file_status = 'error';
open_file_status_message = `Failed to open: ${launch_result.error}`;
}
setTimeout(() => (open_file_clicked = false), 5000);
return launch_result.success;
}
// 2. ONSITE MODE (Browser with Modified Extensions)
else if ($events_loc.launcher.app_mode === 'onsite') {
open_file_clicked = true;
open_file_status = 'downloading_onsite';
open_file_status_message = 'Downloading (Onsite Mode)...';
let filename = event_file_obj.filename;
if (
(event_file_obj.extension === 'ppt' ||
event_file_obj.extension === 'pptx') &&
event_file_obj.open_in_os === 'win'
) {
filename = event_file_obj.filename + 'win';
}
const dl_promise = api.get_object({
api_cfg: $ae_api,
endpoint: `/v3/action/hosted_file/${event_file_obj.hosted_file_id}/download`,
params: {
filename: filename,
x_no_account_id_token: 'direct-download'
},
filename: filename,
return_blob: true,
auto_download: true,
log_lvl: 1
});
setTimeout(() => (open_file_clicked = false), 5000);
return dl_promise;
}
// 3. DEFAULT MODE (Standard Browser)
else {
open_file_clicked = true;
open_file_status = 'downloading_default';
open_file_status_message = 'Downloading...';
const dl_promise = api.get_object({
api_cfg: $ae_api,
endpoint: `/v3/action/hosted_file/${event_file_obj.hosted_file_id}/download`,
params: {
filename: event_file_obj.filename,
x_no_account_id_token: 'direct-download'
},
filename: event_file_obj.filename,
return_blob: true,
auto_download: true,
log_lvl: 1
});
if ($events_loc.launcher.controller == 'local_push') {
$events_sess.launcher.controller_cmd = `ae_download:hosted_file=${event_file_obj.hosted_file_id}:${event_file_obj.filename}:${event_file_obj.extension}`;
$events_sess.launcher.controller_trigger_send = true;
}
setTimeout(() => (open_file_clicked = false), 5000);
return dl_promise;
}
}
function prevent_default<T extends Event>(fn: (event: T) => void) {
return function (event: T) {
event.preventDefault();
fn(event);
}; };
} }
});
async function handle_open_file() {
if (log_lvl) console.log('*** handle_open_file() ***');
if (open_file_clicked) return; // Hard Guard: Already processing
$events_slct.event_file_id = event_file_id;
$events_slct.event_file_obj = event_file_obj;
// 1. NATIVE MODE (Electron)
if ($ae_loc.is_native && $events_loc.launcher.app_mode === 'native') {
const cache_root = $ae_loc.local_file_cache_path;
const temp_root = $ae_loc.host_file_temp_path;
open_file_clicked = true;
open_file_status = 'checking_cache';
open_file_status_message = 'Checking local cache...';
const exists = await native.check_hash_file_cache({
cache_root,
hash: event_file_obj.hash_sha256,
verify_hash: true // Hardened: Trust No One!
});
if (!exists) {
open_file_status = 'downloading_file';
open_file_status_message = 'Downloading file to cache...';
// Use the PROVEN endpoint path from api.ts that is known to work in Default Mode.
const url = `${$ae_api.base_url}/v3/action/hosted_file/${event_file_obj.hosted_file_id}/download?return_file=true&filename=${encodeURIComponent(event_file_obj.filename)}&key=${$ae_api.account_id}`;
const dl_result = await native.download_to_cache({
url,
cache_root,
hash: event_file_obj.hash_sha256,
api_key: $ae_api.api_secret_key,
account_id: $ae_api.account_id
});
if (!dl_result.success) {
open_file_status = 'error';
open_file_status_message = `Download failed: ${dl_result.error}`;
setTimeout(() => (open_file_clicked = false), 5000);
return false;
}
}
open_file_status = 'opening_file';
open_file_status_message = 'Opening Application';
// Phase 2/5: Use the atomic copy-and-launch operation.
// The main process handler (file_handlers.ts) now handles the
// specialized LibreOffice/AppleScript logic internally after copying.
const launch_result = await native.launch_from_cache({
cache_root,
hash: event_file_obj.hash_sha256,
temp_root,
filename: event_file_obj.filename
});
if (!launch_result.success) {
open_file_status = 'error';
open_file_status_message = `Failed to open: ${launch_result.error}`;
}
setTimeout(() => (open_file_clicked = false), 5000);
return launch_result.success;
}
// 2. ONSITE MODE (Browser with Modified Extensions)
else if ($events_loc.launcher.app_mode === 'onsite') {
open_file_clicked = true;
open_file_status = 'downloading_onsite';
open_file_status_message = 'Downloading (Onsite Mode)...';
let filename = event_file_obj.filename;
if (
(event_file_obj.extension === 'ppt' ||
event_file_obj.extension === 'pptx') &&
event_file_obj.open_in_os === 'win'
) {
filename = event_file_obj.filename + 'win';
}
const dl_promise = api.get_object({
api_cfg: $ae_api,
endpoint: `/v3/action/hosted_file/${event_file_obj.hosted_file_id}/download`,
params: {
filename: filename,
x_no_account_id_token: 'direct-download'
},
filename: filename,
return_blob: true,
auto_download: true,
log_lvl: 1
});
setTimeout(() => (open_file_clicked = false), 5000);
return dl_promise;
}
// 3. DEFAULT MODE (Standard Browser)
else {
open_file_clicked = true;
open_file_status = 'downloading_default';
open_file_status_message = 'Downloading...';
const dl_promise = api.get_object({
api_cfg: $ae_api,
endpoint: `/v3/action/hosted_file/${event_file_obj.hosted_file_id}/download`,
params: {
filename: event_file_obj.filename,
x_no_account_id_token: 'direct-download'
},
filename: event_file_obj.filename,
return_blob: true,
auto_download: true,
log_lvl: 1
});
if ($events_loc.launcher.controller == 'local_push') {
$events_sess.launcher.controller_cmd = `ae_download:hosted_file=${event_file_obj.hosted_file_id}:${event_file_obj.filename}:${event_file_obj.extension}`;
$events_sess.launcher.controller_trigger_send = true;
}
setTimeout(() => (open_file_clicked = false), 5000);
return dl_promise;
}
}
function prevent_default<T extends Event>(fn: (event: T) => void) {
return function (event: T) {
event.preventDefault();
fn(event);
};
}
</script> </script>
<div <div
@@ -227,24 +238,22 @@
class:hidden={hide_draft && class:hidden={hide_draft &&
(event_file_obj.file_purpose == 'outline' || (event_file_obj.file_purpose == 'outline' ||
event_file_obj.file_purpose == 'draft')} event_file_obj.file_purpose == 'draft')}
class="event_launcher_file_cont grow flex flex-col md:flex-row flex-wrap gap-1 items-center justify-center max-w-full transition-all" class="event_launcher_file_cont flex max-w-full grow flex-col flex-wrap items-center justify-center gap-1 transition-all md:flex-row">
>
{#if open_file_clicked} {#if open_file_clicked}
<div <div
class="open_file_clicked alert" class="open_file_clicked alert"
in:fade={{ duration: 250 }} in:fade={{ duration: 250 }}
out:fade={{ duration: 2000 }} out:fade={{ duration: 2000 }}>
>
<div class="alert_msg_pulse"> <div class="alert_msg_pulse">
<strong <strong
>*** {open_file_status_message || >*** {open_file_status_message ||
'Please wait while this file downloads...'} ***</strong 'Please wait while this file downloads...'} ***</strong>
>
</div> </div>
{#if $ae_loc.is_native && $events_loc.launcher.app_mode === 'native'} {#if $ae_loc.is_native && $events_loc.launcher.app_mode === 'native'}
<p>Most files will automatically be opened full screen.</p> <p>Most files will automatically be opened full screen.</p>
<p> <p>
PowerPoint or KeyNote will attempt to display in presenter view. PowerPoint or KeyNote will attempt to display in presenter
view.
</p> </p>
<p>Please close the file when finished.</p> <p>Please close the file when finished.</p>
{/if} {/if}
@@ -252,8 +261,7 @@
{/if} {/if}
<span <span
class="event_file_action grow max-w-full flex flex-row flex-wrap gap-1 items-center justify-center" class="event_file_action flex max-w-full grow flex-row flex-wrap items-center justify-center gap-1">
>
{#if session_type == 'poster' || open_method == 'modal'} {#if session_type == 'poster' || open_method == 'modal'}
<AE_Comp_Hosted_Files_Download_Button <AE_Comp_Hosted_Files_Download_Button
hosted_file_id={event_file_id} hosted_file_id={event_file_id}
@@ -267,17 +275,24 @@
$events_slct.event_file_id = event_file_id; $events_slct.event_file_id = event_file_id;
$events_slct.event_file_obj = event_file_obj; $events_slct.event_file_obj = event_file_obj;
// Push the open command to the remote display when in local_push mode // Push the open command to the remote display when in local_push mode
if ($events_loc.launcher.controller == 'local_push' && $events_sess.launcher.ws_connect_status == 'connected') { if (
$events_loc.launcher.controller == 'local_push' &&
$events_sess.launcher.ws_connect_status == 'connected'
) {
$events_sess.launcher.controller_cmd = `ae_open:event_file=${event_file_id}`; $events_sess.launcher.controller_cmd = `ae_open:event_file=${event_file_id}`;
$events_sess.launcher.controller_trigger_send = true; $events_sess.launcher.controller_trigger_send = true;
} }
}} }}>
>
{#snippet label()} {#snippet label()}
{#if screen_saver_exts.includes(event_file_obj.extension)} {#if screen_saver_exts.includes(event_file_obj.extension)}
<BarChart2 size="1em" class="{hide_launch_icon ? 'hidden' : ''} m-1" /> Open Poster <BarChart2
size="1em"
class="{hide_launch_icon ? 'hidden' : ''} m-1" /> Open
Poster
{:else} {:else}
<Send size="1em" class="{hide_launch_icon ? 'hidden' : ''} m-1" /> <Send
size="1em"
class="{hide_launch_icon ? 'hidden' : ''} m-1" />
{ae_util.shorten_filename({ {ae_util.shorten_filename({
filename: event_file_obj.filename, filename: event_file_obj.filename,
max_length: max_filename_length max_length: max_filename_length
@@ -291,13 +306,14 @@
hosted_file_obj={event_file_obj} hosted_file_obj={event_file_obj}
require_auth={false} require_auth={false}
classes="btn {btn_size} gap-1 justify-between min-w-full w-full max-w-96 preset-tonal-primary border border-primary-500" classes="btn {btn_size} gap-1 justify-between min-w-full w-full max-w-96 preset-tonal-primary border border-primary-500"
click={handle_open_file} click={handle_open_file}>
>
{#snippet label()} {#snippet label()}
{@const file_id = event_file_obj.hosted_file_id} {@const file_id = event_file_obj.hosted_file_id}
<span class="shrink text-xs border-r border-gray-400 pr-1"> <span class="shrink border-r border-gray-400 pr-1 text-xs">
{#await ae_promises[event_file_id]} {#await ae_promises[event_file_id]}
<LoaderCircle size="1em" class="inline animate-spin mx-0.5" /> <LoaderCircle
size="1em"
class="mx-0.5 inline animate-spin" />
<span> <span>
{#if $ae_sess.api_download_kv[file_id]} {#if $ae_sess.api_download_kv[file_id]}
{$ae_sess.api_download_kv[file_id] {$ae_sess.api_download_kv[file_id]
@@ -307,24 +323,28 @@
{/if} {/if}
</span> </span>
{:then result} {:then result}
{@const FileIcon = ae_util.file_extension_icon_lucide(event_file_obj.extension)} {@const FileIcon =
<FileIcon size="1em" class="inline mx-0.5" /> ae_util.file_extension_icon_lucide(
event_file_obj.extension
)}
<FileIcon size="1em" class="mx-0.5 inline" />
{event_file_obj.extension} {event_file_obj.extension}
{#if result === null || result === false} {#if result === null || result === false}
<span class="text-error-500" <span class="text-error-500"
><AlertTriangle size="1em" class="inline mx-1" />Failed!</span ><AlertTriangle
> size="1em"
class="mx-1 inline" />Failed!</span>
{/if} {/if}
{:catch error} {:catch error}
<span class="text-error-500" title={error?.message} <span class="text-error-500" title={error?.message}
><AlertCircle size="1em" class="inline mx-0.5" />Error!</span ><AlertCircle
> size="1em"
class="mx-0.5 inline" />Error!</span>
{/await} {/await}
</span> </span>
<span <span
class="grow {text_size} {text_size_md} w-full max-w-full overflow-hidden text-ellipsis {btn_text_align}" class="grow {text_size} {text_size_md} w-full max-w-full overflow-hidden text-ellipsis {btn_text_align}">
>
{ae_util.shorten_string({ {ae_util.shorten_string({
string: event_file_obj.filename_no_ext, string: event_file_obj.filename_no_ext,
begin_length: 45, begin_length: 45,
@@ -333,9 +353,8 @@
</span> </span>
<span <span
class="badge my-0 py-0.5 preset-tonal-success hover:preset-filled-success-500 text-xs xl:text-sm" class="badge preset-tonal-success hover:preset-filled-success-500 my-0 py-0.5 text-xs xl:text-sm"
class:hidden={!event_file_obj.file_purpose} class:hidden={!event_file_obj.file_purpose}>
>
{event_file_obj.file_purpose} {event_file_obj.file_purpose}
</span> </span>
{/snippet} {/snippet}
@@ -344,9 +363,8 @@
</span> </span>
<span <span
class="event_file_meta grow text-sm text-gray-500 flex flex-col sm:flex-row gap-1 wrap items-center justify-between w-64 max-w-80 font-mono" class="event_file_meta wrap flex w-64 max-w-80 grow flex-col items-center justify-between gap-1 font-mono text-sm text-gray-500 sm:flex-row"
class:hidden={hide_meta} class:hidden={hide_meta}>
>
<button <button
type="button" type="button"
onclick={async () => { onclick={async () => {
@@ -366,33 +384,33 @@
log_lvl log_lvl
}); });
}} }}
class="btn btn-sm transition-all group" class="btn btn-sm group transition-all"
class:preset-tonal-success={event_file_obj?.open_in_os == 'win'} class:preset-tonal-success={event_file_obj?.open_in_os == 'win'}
class:preset-tonal-warning={event_file_obj?.open_in_os == 'mac'} class:preset-tonal-warning={event_file_obj?.open_in_os == 'mac'}
disabled={!$ae_loc.trusted_access} disabled={!$ae_loc.trusted_access}>
> {#if event_file_obj?.open_in_os == 'win'}<Monitor
{#if event_file_obj?.open_in_os == 'win'}<Monitor size="1em" class="m-1" /> size="1em"
{:else if event_file_obj?.open_in_os == 'mac'}<Laptop size="1em" class="m-1" /> class="m-1" />
{:else if event_file_obj?.open_in_os == 'mac'}<Laptop
size="1em"
class="m-1" />
{:else}<FolderOpen size="1em" class="m-1" />{/if} {:else}<FolderOpen size="1em" class="m-1" />{/if}
</button> </button>
<span <span
class="event_file_created_on text-xs text-center flex flex-row gap-1 items-center justify-end w-24 md:w-44 preset-filled-surface-100-900 rounded px-1 py-0.5" class="event_file_created_on preset-filled-surface-100-900 flex w-24 flex-row items-center justify-end gap-1 rounded px-1 py-0.5 text-center text-xs md:w-44"
class:hidden={hide_created_on} class:hidden={hide_created_on}>
>
<CalendarDays size="0.85em" class="inline" /> <CalendarDays size="0.85em" class="inline" />
<span class="w-18" <span class="w-18"
>{ae_util.iso_datetime_formatter( >{ae_util.iso_datetime_formatter(
event_file_obj.created_on, event_file_obj.created_on,
'date_short' 'date_short'
)}</span )}</span>
>
</span> </span>
<span <span
class="event_file_size text-xs text-center flex flex-row gap-1 items-center justify-end preset-filled-surface-100-900 w-22 max-w-28 rounded py-0.5" class="event_file_size preset-filled-surface-100-900 flex w-22 max-w-28 flex-row items-center justify-end gap-1 rounded py-0.5 text-center text-xs"
class:hidden={hide_size} class:hidden={hide_size}>
>
<Save size="0.85em" class="inline" /> <Save size="0.85em" class="inline" />
{#if event_file_obj.file_size}{ae_util.format_bytes( {#if event_file_obj.file_size}{ae_util.format_bytes(
event_file_obj.file_size event_file_obj.file_size

View File

@@ -1,160 +1,159 @@
<script lang="ts"> <script lang="ts">
/** /**
* launcher_menu.svelte — Aether Launcher: Sidebar Menu Container * launcher_menu.svelte — Aether Launcher: Sidebar Menu Container
* *
* PURPOSE: * PURPOSE:
* The main sidebar panel for the Aether Events Launcher. Composes all menu * The main sidebar panel for the Aether Events Launcher. Composes all menu
* sub-components into a single vertical column and passes data down from the * sub-components into a single vertical column and passes data down from the
* parent layout. This is intentionally a thin coordinator — business logic lives * parent layout. This is intentionally a thin coordinator — business logic lives
* in the individual sub-components and the layout's liveQuery / effect layer. * in the individual sub-components and the layout's liveQuery / effect layer.
* *
* STRUCTURE (top to bottom): * STRUCTURE (top to bottom):
* 1. Event-level files (lq__event_event_file_obj_li) * 1. Event-level files (lq__event_event_file_obj_li)
* Files attached directly to the event (e.g. a shared opening slide deck). * Files attached directly to the event (e.g. a shared opening slide deck).
* Shown regardless of which room/session is selected. * Shown regardless of which room/session is selected.
* *
* 2. Location selector (Menu_location_list_menu — edit mode only) * 2. Location selector (Menu_location_list_menu — edit mode only)
* Dropdown that lets operators switch the active room. Triggers a session * Dropdown that lets operators switch the active room. Triggers a session
* list reload and navigates the URL to /launcher/{location_id}. * list reload and navigates the URL to /launcher/{location_id}.
* *
* 3. Location-level files (lq__location_event_file_obj_li) * 3. Location-level files (lq__location_event_file_obj_li)
* Files attached to the selected room — e.g. A/V setup guides, room schedules. * Files attached to the selected room — e.g. A/V setup guides, room schedules.
* Hidden until a room is selected. * Hidden until a room is selected.
* *
* 4. Session list (Menu_session_list_menu) * 4. Session list (Menu_session_list_menu)
* Compact button list of all sessions in the selected room. Operators click * Compact button list of all sessions in the selected room. Operators click
* to switch the active session; hover pre-loads after a delay timer. * to switch the active session; hover pre-loads after a delay timer.
* *
* 5. Launcher controls (Menu_launcher_controls) * 5. Launcher controls (Menu_launcher_controls)
* Bottom bar for accessibility and visibility settings: font size cycler, * Bottom bar for accessibility and visibility settings: font size cycler,
* light/dark toggle, and (edit mode only) show/hide draft files/sessions. * light/dark toggle, and (edit mode only) show/hide draft files/sessions.
* *
* DATA FLOW: * DATA FLOW:
* All liveQuery stores (lq__*) are passed in from +layout.svelte and originate * All liveQuery stores (lq__*) are passed in from +layout.svelte and originate
* from Dexie IndexedDB — never fetched directly here. Selection state is * from Dexie IndexedDB — never fetched directly here. Selection state is
* coordinated via $events_slct / $events_loc stores. * coordinated via $events_slct / $events_loc stores.
*/ */
interface Props { interface Props {
lq__event_obj: any; lq__event_obj: any;
lq__event_event_file_obj_li: any; lq__event_event_file_obj_li: any;
lq__location_event_file_obj_li: any; lq__location_event_file_obj_li: any;
slct__event_file_id?: string | null; slct__event_file_id?: string | null;
lq__event_location_obj_li: any; lq__event_location_obj_li: any;
lq__event_location_obj?: any; lq__event_location_obj?: any;
slct__event_location_id?: string | null; slct__event_location_id?: string | null;
loading__session_li_status?: null | boolean | string; loading__session_li_status?: null | boolean | string;
lq__event_session_obj_li: any; lq__event_session_obj_li: any;
loading__session_id_status?: null | boolean | string; loading__session_id_status?: null | boolean | string;
lq__event_session_obj?: any; lq__event_session_obj?: any;
slct__event_session_id?: string | null; slct__event_session_id?: string | null;
trigger_reload__event_obj_id?: boolean | null | string; trigger_reload__event_obj_id?: boolean | null | string;
trigger_reload__event_session_obj_id?: boolean | null | string; trigger_reload__event_session_obj_id?: boolean | null | string;
trigger_reload__event_session_obj_li?: boolean; trigger_reload__event_session_obj_li?: boolean;
trigger_reload__event_location_obj_id?: boolean | null | string; trigger_reload__event_location_obj_id?: boolean | null | string;
trigger_reload__event_location_obj_li?: boolean; trigger_reload__event_location_obj_li?: boolean;
log_lvl?: number; log_lvl?: number;
} }
let { let {
lq__event_obj, lq__event_obj,
lq__event_event_file_obj_li, lq__event_event_file_obj_li,
lq__location_event_file_obj_li, lq__location_event_file_obj_li,
slct__event_file_id = $bindable(null), slct__event_file_id = $bindable(null),
lq__event_location_obj_li, lq__event_location_obj_li,
lq__event_location_obj, lq__event_location_obj,
slct__event_location_id = $bindable(null), slct__event_location_id = $bindable(null),
loading__session_li_status = $bindable(null), loading__session_li_status = $bindable(null),
lq__event_session_obj_li, lq__event_session_obj_li,
loading__session_id_status = $bindable(null), loading__session_id_status = $bindable(null),
lq__event_session_obj, lq__event_session_obj,
slct__event_session_id = $bindable(null), slct__event_session_id = $bindable(null),
trigger_reload__event_obj_id = $bindable(false), trigger_reload__event_obj_id = $bindable(false),
trigger_reload__event_session_obj_id = $bindable(false), trigger_reload__event_session_obj_id = $bindable(false),
trigger_reload__event_session_obj_li = $bindable(false), trigger_reload__event_session_obj_li = $bindable(false),
trigger_reload__event_location_obj_id = $bindable(false), trigger_reload__event_location_obj_id = $bindable(false),
trigger_reload__event_location_obj_li = $bindable(false), trigger_reload__event_location_obj_li = $bindable(false),
log_lvl = $bindable(0) log_lvl = $bindable(0)
}: Props = $props(); }: Props = $props();
// *** Import Svelte specific // *** Import Svelte specific
// import { goto } from '$app/navigation'; // import { goto } from '$app/navigation';
// *** Import other supporting libraries // *** Import other supporting libraries
// import { liveQuery } from "dexie"; // import { liveQuery } from "dexie";
// *** Import Aether specific variables and functions // *** Import Aether specific variables and functions
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
import { api } from '$lib/api/api'; import { api } from '$lib/api/api';
// import Element_data_store from '$lib/element_data_store.svelte'; // import Element_data_store from '$lib/element_data_store.svelte';
import { import {
ae_snip, ae_snip,
ae_loc, ae_loc,
ae_sess, ae_sess,
ae_api, ae_api,
ae_trig, ae_trig,
slct, slct,
slct_trigger slct_trigger
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
// import { db_events } from "$lib/ae_events/db_events"; // import { db_events } from "$lib/ae_events/db_events";
import { import {
events_loc, events_loc,
events_sess, events_sess,
events_slct, events_slct,
events_trigger events_trigger
} from '$lib/stores/ae_events_stores'; } from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions'; import { events_func } from '$lib/ae_events/ae_events_functions';
import Event_launcher_file_cont from './launcher_file_cont.svelte'; import Event_launcher_file_cont from './launcher_file_cont.svelte';
import Menu_location_list_menu from './menu_location_list.svelte'; import Menu_location_list_menu from './menu_location_list.svelte';
import Menu_session_list_menu from './menu_session_list.svelte'; import Menu_session_list_menu from './menu_session_list.svelte';
import Menu_launcher_controls from './menu_launcher_controls.svelte'; import Menu_launcher_controls from './menu_launcher_controls.svelte';
// *** Functions and Logic // *** Functions and Logic
$events_trigger = null; $events_trigger = null;
let qry__enabled = 'enabled'; let qry__enabled = 'enabled';
let qry__hidden = 'not_hidden'; let qry__hidden = 'not_hidden';
if ($ae_loc.administrator_access) { if ($ae_loc.administrator_access) {
qry__enabled = 'all'; qry__enabled = 'all';
qry__hidden = 'all'; qry__hidden = 'all';
} else if ($ae_loc.trusted_access) { } else if ($ae_loc.trusted_access) {
qry__enabled = 'enabled'; qry__enabled = 'enabled';
qry__hidden = 'all'; qry__hidden = 'all';
} else { } else {
qry__enabled = 'enabled'; qry__enabled = 'enabled';
qry__hidden = 'not_hidden'; qry__hidden = 'not_hidden';
} }
let ae_promises: key_val = $state({ let ae_promises: key_val = $state({
get_li__event_file: null get_li__event_file: null
}); });
</script> </script>
<div <div
class=" class="
event_launcher_menu event_launcher_menu
shrink h-full w-full max-w-full flex h-full w-full max-w-full
flex flex-col flex-wrap gap-1 items-center justify-start shrink flex-col flex-wrap items-center justify-start gap-1
" ">
>
<!-- overflow-x-clip --> <!-- overflow-x-clip -->
{#if $lq__event_event_file_obj_li} {#if $lq__event_event_file_obj_li}
<div class="w-full flex flex-col gap-0.5 overflow-y-auto"> <div class="flex w-full flex-col gap-0.5 overflow-y-auto">
<!-- <div class="text-xs text-neutral-800/80"> <!-- <div class="text-xs text-neutral-800/80">
<strong> <strong>
Event Files: Event Files:
@@ -191,8 +190,7 @@
} }
bind:modal__event_file_obj={ bind:modal__event_file_obj={
$events_sess.launcher.modal__event_file_obj $events_sess.launcher.modal__event_file_obj
} } />
/>
{/each} {/each}
</div> </div>
{/if} {/if}
@@ -201,12 +199,11 @@
<Menu_location_list_menu <Menu_location_list_menu
{lq__event_location_obj_li} {lq__event_location_obj_li}
slct_event_location_id={$events_slct.event_location_id} slct_event_location_id={$events_slct.event_location_id}
bind:loading__session_li_status bind:loading__session_li_status />
/>
{/if} {/if}
{#if $lq__location_event_file_obj_li} {#if $lq__location_event_file_obj_li}
<div class="w-full flex flex-col gap-0.5"> <div class="flex w-full flex-col gap-0.5">
{#each $lq__location_event_file_obj_li as event_file_obj, index (event_file_obj.event_file_id)} {#each $lq__location_event_file_obj_li as event_file_obj, index (event_file_obj.event_file_id)}
<Event_launcher_file_cont <Event_launcher_file_cont
event_file_id={event_file_obj.event_file_id} event_file_id={event_file_obj.event_file_id}
@@ -235,8 +232,7 @@
} }
bind:modal__event_file_obj={ bind:modal__event_file_obj={
$events_sess.launcher.modal__event_file_obj $events_sess.launcher.modal__event_file_obj
} } />
/>
{/each} {/each}
</div> </div>
{/if} {/if}
@@ -246,8 +242,7 @@
bind:slct__event_session_id bind:slct__event_session_id
bind:loading__session_id_status bind:loading__session_id_status
{lq__event_session_obj_li} {lq__event_session_obj_li}
bind:trigger_reload__event_session_obj_id bind:trigger_reload__event_session_obj_id />
/>
{/if} {/if}
<Menu_launcher_controls /> <Menu_launcher_controls />

View File

@@ -1,69 +1,67 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
lq__event_presentation_obj: any; lq__event_presentation_obj: any;
session_type?: string; session_type?: string;
}
let { lq__event_presentation_obj, session_type = '' }: Props = $props();
import { liveQuery } from 'dexie';
import { db_events } from '$lib/ae_events/db_events';
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import Event_launcher_file_cont from './launcher_file_cont.svelte';
// Staggered Load: Trigger deep fetch only when the presentation ID changes.
// WHY: The SWR pattern in load_ae_obj_id__event_presentation always fires a
// background API call that writes to Dexie. That Dexie write triggers the
// liveQuery upstream, which updates the lq__event_presentation_obj prop,
// which re-runs this $effect — creating an infinite loop that crashes the tab.
// Guarding on last_loaded_id breaks the loop: the effect only makes an API
// call when we see a new presentation ID, not on every downstream prop update.
let last_loaded_id: string | null = null;
$effect(() => {
const id = lq__event_presentation_obj?.event_presentation_id;
if (id && id !== last_loaded_id) {
last_loaded_id = id;
events_func.load_ae_obj_id__event_presentation({
api_cfg: $ae_api,
event_presentation_id: id,
inc_file_li: true,
inc_presenter_li: true,
try_cache: true,
log_lvl: 0
});
} }
});
let { lq__event_presentation_obj, session_type = '' }: Props = $props(); // Event File (Directly linked to presentation)
let lq__event_file_obj_li = $derived(
import { liveQuery } from 'dexie'; liveQuery(async () => {
import { db_events } from '$lib/ae_events/db_events'; if (!lq__event_presentation_obj?.event_presentation_id) return [];
import { ae_loc, ae_api } from '$lib/stores/ae_stores'; let results = await db_events.file
import { events_loc, events_sess } from '$lib/stores/ae_events_stores'; .where('for_id')
import { events_func } from '$lib/ae_events/ae_events_functions'; .equals(lq__event_presentation_obj.event_presentation_id)
import Event_launcher_file_cont from './launcher_file_cont.svelte'; .reverse()
.sortBy('created_on');
// Staggered Load: Trigger deep fetch only when the presentation ID changes. return results;
// WHY: The SWR pattern in load_ae_obj_id__event_presentation always fires a })
// background API call that writes to Dexie. That Dexie write triggers the );
// liveQuery upstream, which updates the lq__event_presentation_obj prop,
// which re-runs this $effect — creating an infinite loop that crashes the tab.
// Guarding on last_loaded_id breaks the loop: the effect only makes an API
// call when we see a new presentation ID, not on every downstream prop update.
let last_loaded_id: string | null = null;
$effect(() => {
const id = lq__event_presentation_obj?.event_presentation_id;
if (id && id !== last_loaded_id) {
last_loaded_id = id;
events_func.load_ae_obj_id__event_presentation({
api_cfg: $ae_api,
event_presentation_id: id,
inc_file_li: true,
inc_presenter_li: true,
try_cache: true,
log_lvl: 0
});
}
});
// Event File (Directly linked to presentation)
let lq__event_file_obj_li = $derived(
liveQuery(async () => {
if (!lq__event_presentation_obj?.event_presentation_id) return [];
let results = await db_events.file
.where('for_id')
.equals(lq__event_presentation_obj.event_presentation_id)
.reverse()
.sortBy('created_on');
return results;
})
);
</script> </script>
{#if $lq__event_file_obj_li && $lq__event_file_obj_li.length} {#if $lq__event_file_obj_li && $lq__event_file_obj_li.length}
<section class="event_presentation_file_list my-1"> <section class="event_presentation_file_list my-1">
<div <div
class="text-[10px] text-surface-600-400 uppercase font-bold tracking-wider opacity-70" class="text-surface-600-400 text-[10px] font-bold tracking-wider uppercase opacity-70">
>
Presentation Files: Presentation Files:
</div> </div>
<ul class="space-y-1"> <ul class="space-y-1">
{#each $lq__event_file_obj_li as event_file_obj (event_file_obj.event_file_id)} {#each $lq__event_file_obj_li as event_file_obj (event_file_obj.event_file_id)}
<li <li
class="flex flex-col md:flex-row flex-wrap gap-1 items-center justify-start" class="flex flex-col flex-wrap items-center justify-start gap-1 md:flex-row"
class:hidden={!$events_loc.launcher class:hidden={!$events_loc.launcher
.show_content__hidden_files && event_file_obj.hide} .show_content__hidden_files && event_file_obj.hide}>
>
<Event_launcher_file_cont <Event_launcher_file_cont
event_file_id={event_file_obj.event_file_id} event_file_id={event_file_obj.event_file_id}
{event_file_obj} {event_file_obj}
@@ -82,8 +80,7 @@
} }
bind:modal__event_file_obj={ bind:modal__event_file_obj={
$events_sess.launcher.modal__event_file_obj $events_sess.launcher.modal__event_file_obj
} } />
/>
</li> </li>
{/each} {/each}
</ul> </ul>

View File

@@ -1,56 +1,56 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
// export let slct_event_presenter_id: string; // export let slct_event_presenter_id: string;
lq__event_presenter_obj: any; // This is not actually the LiveQuery object. This was pulled from the list of presenters for a presentation. With Svelte 5 this should not matter. lq__event_presenter_obj: any; // This is not actually the LiveQuery object. This was pulled from the list of presenters for a presentation. With Svelte 5 this should not matter.
session_type?: string; session_type?: string;
} }
let { lq__event_presenter_obj, session_type = 'oral' }: Props = $props(); let { lq__event_presenter_obj, session_type = 'oral' }: Props = $props();
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
// import { ae_util } from '$lib/ae_utils/ae_utils'; // import { ae_util } from '$lib/ae_utils/ae_utils';
// import { api } from '$lib/api'; // import { api } from '$lib/api';
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
import { import {
ae_snip, ae_snip,
ae_loc, ae_loc,
ae_sess, ae_sess,
ae_api, ae_api,
ae_trig, ae_trig,
slct, slct,
slct_trigger slct_trigger
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events'; import { db_events } from '$lib/ae_events/db_events';
import { import {
events_loc, events_loc,
events_sess, events_sess,
events_slct, events_slct,
events_trigger events_trigger
} from '$lib/stores/ae_events_stores'; } from '$lib/stores/ae_events_stores';
// import { events_func } from '$lib/ae_events/ae_events_functions'; // import { events_func } from '$lib/ae_events/ae_events_functions';
import Event_launcher_file_cont from './launcher_file_cont.svelte'; import Event_launcher_file_cont from './launcher_file_cont.svelte';
import { Archive, User, Users } from '@lucide/svelte'; import { Archive, User, Users } from '@lucide/svelte';
// export let slct_event_presentation_id: string; // export let slct_event_presentation_id: string;
let ae_promises: key_val = { let ae_promises: key_val = {
get_li__event_file: null get_li__event_file: null
}; };
// Event File // Event File
let lq__event_file_obj_li = $derived( let lq__event_file_obj_li = $derived(
liveQuery(async () => { liveQuery(async () => {
let results = await db_events.file let results = await db_events.file
// .where('event_session_id') // .where('event_session_id')
.where('for_id') .where('for_id')
.equals(lq__event_presenter_obj?.event_presenter_id) .equals(lq__event_presenter_obj?.event_presenter_id)
.reverse() .reverse()
.sortBy('created_on'); .sortBy('created_on');
return results; return results;
}) })
); );
</script> </script>
<strong> <strong>
@@ -66,7 +66,7 @@
</strong> </strong>
{#if !lq__event_presenter_obj?.file_count} {#if !lq__event_presenter_obj?.file_count}
<p class="text-sm text-center text-gray-400"> <p class="text-center text-sm text-gray-400">
<!-- <span class="fas fa-exclamation"></span> --> <!-- <span class="fas fa-exclamation"></span> -->
No files to show for this presenter at this time. No files to show for this presenter at this time.
<!-- <span class="fas fa-exclamation"></span> --> <!-- <span class="fas fa-exclamation"></span> -->
@@ -76,7 +76,7 @@
{#if $lq__event_file_obj_li && $lq__event_file_obj_li.length} {#if $lq__event_file_obj_li && $lq__event_file_obj_li.length}
<section class="event_session_file_list"> <section class="event_session_file_list">
<div> <div>
<div class="text-xs text-surface-600-400"> <div class="text-surface-600-400 text-xs">
<strong> <strong>
<Archive size="1em" class="inline" /> <Archive size="1em" class="inline" />
Presenter Files: Presenter Files:
@@ -89,10 +89,9 @@
<ul class="space-y-1"> <ul class="space-y-1">
{#each $lq__event_file_obj_li as event_file_obj, index (event_file_obj.event_file_id)} {#each $lq__event_file_obj_li as event_file_obj, index (event_file_obj.event_file_id)}
<li <li
class="flex flex-col md:flex-row flex-wrap gap-1 items-center justify-start" class="flex flex-col flex-wrap items-center justify-start gap-1 md:flex-row"
class:hidden={!$events_loc.launcher class:hidden={!$events_loc.launcher
.show_content__hidden_files && event_file_obj.hide} .show_content__hidden_files && event_file_obj.hide}>
>
<Event_launcher_file_cont <Event_launcher_file_cont
event_file_id={event_file_obj.event_file_id} event_file_id={event_file_obj.event_file_id}
{event_file_obj} {event_file_obj}
@@ -110,8 +109,7 @@
} }
bind:modal__event_file_obj={ bind:modal__event_file_obj={
$events_sess.launcher.modal__event_file_obj $events_sess.launcher.modal__event_file_obj
} } />
/>
</li> </li>
{/each} {/each}
</ul> </ul>

View File

@@ -1,56 +1,56 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
// export let slct_event_presenter_id: string; // export let slct_event_presenter_id: string;
lq__event_presenter_obj: any; // This is not actually the LiveQuery object. This was pulled from the list of presenters for a presentation. With Svelte 5 this should not matter. lq__event_presenter_obj: any; // This is not actually the LiveQuery object. This was pulled from the list of presenters for a presentation. With Svelte 5 this should not matter.
hide_name?: boolean; hide_name?: boolean;
} }
let { lq__event_presenter_obj, hide_name = false }: Props = $props(); let { lq__event_presenter_obj, hide_name = false }: Props = $props();
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
// import { ae_util } from '$lib/ae_utils/ae_utils'; // import { ae_util } from '$lib/ae_utils/ae_utils';
// import { api } from '$lib/api'; // import { api } from '$lib/api';
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
import { import {
ae_snip, ae_snip,
ae_loc, ae_loc,
ae_sess, ae_sess,
ae_api, ae_api,
ae_trig, ae_trig,
slct, slct,
slct_trigger slct_trigger
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events'; import { db_events } from '$lib/ae_events/db_events';
import { import {
events_loc, events_loc,
events_sess, events_sess,
events_slct, events_slct,
events_trigger events_trigger
} from '$lib/stores/ae_events_stores'; } from '$lib/stores/ae_events_stores';
// import { events_func } from '$lib/ae_events/ae_events_functions'; // import { events_func } from '$lib/ae_events/ae_events_functions';
import Event_launcher_file_cont from './launcher_file_cont.svelte'; import Event_launcher_file_cont from './launcher_file_cont.svelte';
import { Archive, User, Users } from '@lucide/svelte'; import { Archive, User, Users } from '@lucide/svelte';
// export let slct_event_presentation_id: string; // export let slct_event_presentation_id: string;
let ae_promises: key_val = { let ae_promises: key_val = {
get_li__event_file: null get_li__event_file: null
}; };
// Event File // Event File
let lq__event_file_obj_li = $derived( let lq__event_file_obj_li = $derived(
liveQuery(async () => { liveQuery(async () => {
let results = await db_events.file let results = await db_events.file
// .where('event_session_id') // .where('event_session_id')
.where('for_id') .where('for_id')
.equals(lq__event_presenter_obj?.event_presenter_id) .equals(lq__event_presenter_obj?.event_presenter_id)
.reverse() .reverse()
.sortBy('created_on'); .sortBy('created_on');
return results; return results;
}) })
); );
</script> </script>
<strong class:hidden={hide_name}> <strong class:hidden={hide_name}>
@@ -83,10 +83,9 @@
<ul class="space-y-1"> <ul class="space-y-1">
{#each $lq__event_file_obj_li as event_file_obj, index (event_file_obj.event_file_id)} {#each $lq__event_file_obj_li as event_file_obj, index (event_file_obj.event_file_id)}
<li <li
class="flex flex-col md:flex-row wrap gap items-center justify-center" class="wrap gap flex flex-col items-center justify-center md:flex-row"
class:hidden={!$events_loc.launcher class:hidden={!$events_loc.launcher
.show_content__hidden_files && event_file_obj.hide} .show_content__hidden_files && event_file_obj.hide}>
>
<Event_launcher_file_cont <Event_launcher_file_cont
event_file_id={event_file_obj.event_file_id} event_file_id={event_file_obj.event_file_id}
{event_file_obj} {event_file_obj}
@@ -102,8 +101,7 @@
} }
bind:modal__event_file_obj={ bind:modal__event_file_obj={
$events_sess.launcher.modal__event_file_obj $events_sess.launcher.modal__event_file_obj
} } />
/>
</li> </li>
{/each} {/each}
</ul> </ul>

View File

@@ -1,160 +1,174 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
slct__event_session_id?: string | null; slct__event_session_id?: string | null;
log_lvl?: number; log_lvl?: number;
} }
let { let {
slct__event_session_id = $bindable(null), slct__event_session_id = $bindable(null),
log_lvl = $bindable(1) log_lvl = $bindable(1)
}: Props = $props(); }: Props = $props();
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
import Event_launcher_file_cont from './launcher_file_cont.svelte'; import Event_launcher_file_cont from './launcher_file_cont.svelte';
import Launcher_presentation_view from './launcher_presentation_view.svelte'; import Launcher_presentation_view from './launcher_presentation_view.svelte';
import Launcher_presenter_view from './launcher_presenter_view.svelte'; import Launcher_presenter_view from './launcher_presenter_view.svelte';
import Launcher_presenter_view_posters from './launcher_presenter_view_posters.svelte'; import Launcher_presenter_view_posters from './launcher_presenter_view_posters.svelte';
// WHY: Poster sessions get a dedicated card-grid view optimised for touch/PWA use. // WHY: Poster sessions get a dedicated card-grid view optimised for touch/PWA use.
import Launcher_session_view_posters from './launcher_session_view_posters.svelte'; import Launcher_session_view_posters from './launcher_session_view_posters.svelte';
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
// import { core_func } from '$lib/ae_core_functions'; // import { core_func } from '$lib/ae_core_functions';
// import { db_core } from "$lib/db_core"; // import { db_core } from "$lib/db_core";
import { import {
ae_snip, ae_snip,
ae_loc, ae_loc,
ae_sess, ae_sess,
ae_api, ae_api,
ae_trig, ae_trig,
slct, slct,
slct_trigger slct_trigger
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events'; import { db_events } from '$lib/ae_events/db_events';
import { import {
events_loc, events_loc,
events_sess, events_sess,
events_slct, events_slct,
events_trigger events_trigger
} from '$lib/stores/ae_events_stores'; } from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions'; import { events_func } from '$lib/ae_events/ae_events_functions';
import { AlertTriangle, Archive, Barcode, Image, LoaderCircle, Monitor, User, Users } from '@lucide/svelte'; import {
// Event Session (Main View Trigger) AlertTriangle,
// WHY: We use a simple derived observable. The template handles the $ prefix. Archive,
let lq__event_session_obj = $derived( Barcode,
liveQuery(() => db_events.session.get(slct__event_session_id)) Image,
); LoaderCircle,
Monitor,
User,
Users
} from '@lucide/svelte';
// 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. // 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 // 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 // code paths were silently dead. Deriving it here from the session object
// ensures it always reflects the current session. // ensures it always reflects the current session.
let type_code = $derived($lq__event_session_obj?.type_code ?? ''); let type_code = $derived($lq__event_session_obj?.type_code ?? '');
// Event File (for a Session) // Event File (for a Session)
// WHY: Pure data retrieval. Side effects (updating global stores) are removed // WHY: Pure data retrieval. Side effects (updating global stores) are removed
// to prevent circular reactivity loops during rapid navigation. // to prevent circular reactivity loops during rapid navigation.
let lq__event_file_obj_li = $derived( let lq__event_file_obj_li = $derived(
liveQuery(async () => { liveQuery(async () => {
if (!slct__event_session_id) return []; if (!slct__event_session_id) return [];
if (log_lvl > 1) { if (log_lvl > 1) {
console.log(`[LQ] Fetching files for session: ${slct__event_session_id}`); console.log(
} `[LQ] Fetching files for session: ${slct__event_session_id}`
);
}
return await db_events.file return await db_events.file
.where('for_id') .where('for_id')
.equals(slct__event_session_id) .equals(slct__event_session_id)
.reverse() .reverse()
.sortBy('created_on'); .sortBy('created_on');
}) })
); );
// Event Presentation // Event Presentation
let lq__event_presentation_obj_li = $derived( let lq__event_presentation_obj_li = $derived(
liveQuery(async () => { liveQuery(async () => {
if (!slct__event_session_id) return []; if (!slct__event_session_id) return [];
if (log_lvl > 1) { if (log_lvl > 1) {
console.log(`[LQ] Fetching presentations for session: ${slct__event_session_id}`); console.log(
} `[LQ] Fetching presentations for session: ${slct__event_session_id}`
);
}
let sort_by = 'start_datetime'; let sort_by = 'start_datetime';
if (type_code == 'poster') { if (type_code == 'poster') {
sort_by = 'name'; sort_by = 'name';
} }
return await db_events.presentation return await db_events.presentation
.where('event_session_id') .where('event_session_id')
.equals(slct__event_session_id) .equals(slct__event_session_id)
.sortBy(sort_by); .sortBy(sort_by);
}) })
); );
// Event Presenter // Event Presenter
let lq__event_presenter_obj_li = $derived( let lq__event_presenter_obj_li = $derived(
liveQuery(async () => { liveQuery(async () => {
if (!slct__event_session_id) return []; if (!slct__event_session_id) return [];
if (log_lvl > 1) { if (log_lvl > 1) {
console.log(`[LQ] Fetching presenters for session: ${slct__event_session_id}`); console.log(
} `[LQ] Fetching presenters for session: ${slct__event_session_id}`
);
}
return await db_events.presenter return await db_events.presenter
.where('event_session_id') .where('event_session_id')
.equals(slct__event_session_id) .equals(slct__event_session_id)
.sortBy('full_name'); .sortBy('full_name');
}) })
); );
// let show_modal_upload_files: boolean = false; // let show_modal_upload_files: boolean = false;
// let link_to_type: null|string = null; // let link_to_type: null|string = null;
// let link_to_id: null|string = null; // let link_to_id: null|string = null;
let ae_promises: key_val = $state({}); let ae_promises: key_val = $state({});
// $events_slct.id_li__event_presenter = []; // $events_slct.id_li__event_presenter = [];
// await tick(); // await tick();
// ae_promises[slct__event_session_id] = events_func.load_ae_obj_li__event_presenter({ // ae_promises[slct__event_session_id] = events_func.load_ae_obj_li__event_presenter({
// api_cfg: $ae_api, // api_cfg: $ae_api,
// for_obj_type: 'event_session', // for_obj_type: 'event_session',
// for_obj_id: slct__event_session_id, // for_obj_id: slct__event_session_id,
// // inc_file_li: false, // // inc_file_li: false,
// params: {qry__enabled: 'enabled', qry__limit: 550}, // params: {qry__enabled: 'enabled', qry__limit: 550},
// try_cache: true, // try_cache: true,
// log_lvl: 1, // log_lvl: 1,
// }) // })
// .then(async function (load_results) { // .then(async function (load_results) {
// console.log(`load_results = `, load_results); // console.log(`load_results = `, load_results);
// // let event_presenter_id_li = []; // // let event_presenter_id_li = [];
// // let tmp_li = []; // This is to prevent the array from constantly updating and triggering the liveQuery. // // 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++) { // // for (let i = 0; i < load_results.length; i++) {
// // let event_presenter_obj = load_results[i]; // // let event_presenter_obj = load_results[i];
// // let event_presenter_id = event_presenter_obj.event_presenter_id; // // let event_presenter_id = event_presenter_obj.event_presenter_id;
// // tmp_li.push(event_presenter_id); // // tmp_li.push(event_presenter_id);
// // } // // }
// // event_presenter_id_li = tmp_li; // // event_presenter_id_li = tmp_li;
// // console.log(`event_presenter_id_li:`, event_presenter_id_li); // // console.log(`event_presenter_id_li:`, event_presenter_id_li);
// // $events_slct.id_li__event_presenter = event_presenter_id_li; // // $events_slct.id_li__event_presenter = event_presenter_id_li;
// return load_results; // return load_results;
// }); // });
</script> </script>
<div <div
class=" class="
event_launcher_session_view event_launcher_session_view
grow h-full w-full relative h-full w-full
grow
space-y-1 space-y-1
relative ">
"
>
<!-- <slot name="event_session_message">event session message</slot> --> <!-- <slot name="event_session_message">event session message</slot> -->
{#if $events_sess.launcher.loading__session_id_status} {#if $events_sess.launcher.loading__session_id_status}
<span class="absolute top-0 right-0 text-sm text-center text-gray-400"> <span class="absolute top-0 right-0 text-center text-sm text-gray-400">
<LoaderCircle size="1em" class="inline animate-spin" /> <LoaderCircle size="1em" class="inline animate-spin" />
Loading session information... Loading session information...
</span> </span>
@@ -169,157 +183,151 @@
<!-- WHY: Poster sessions use a dedicated touch-first card-grid layout. --> <!-- WHY: Poster sessions use a dedicated touch-first card-grid layout. -->
<Launcher_session_view_posters {slct__event_session_id} {log_lvl} /> <Launcher_session_view_posters {slct__event_session_id} {log_lvl} />
{:else} {:else}
<!-- <!--
Session header: flex-col keeps datetime and name on separate rows so Session header: flex-col keeps datetime and name on separate rows so
the header height is predictable regardless of session name length. the header height is predictable regardless of session name length.
Long names (300+ chars) are clamped to 2 lines; short names never Long names (300+ chars) are clamped to 2 lines; short names never
collapse the header below that height. Zero layout shift between sessions. collapse the header below that height. Zero layout shift between sessions.
--> -->
<header <header
class="event_session_about border-b-2 border-gray-400 dark:border-gray-600 flex flex-col gap-0.5 items-stretch" class="event_session_about flex flex-col items-stretch gap-0.5 border-b-2 border-gray-400 dark:border-gray-600">
> <h3
<h3 class:hidden={!$lq__event_session_obj?.start_datetime ||
class:hidden={!$lq__event_session_obj?.start_datetime || $events_loc.launcher.hide__session_datetimes}
$events_loc.launcher.hide__session_datetimes} class="event_session_datetimes text-center text-sm">
class="event_session_datetimes text-sm text-center" <button
> type="button"
<button onclick={() => {
type="button" if (
onclick={() => { $events_loc.launcher.time_format ==
if ( 'time_12_short'
$events_loc.launcher.time_format == 'time_12_short' ) {
) { // $events_loc.launcher.datetime_format = 'datetime_long';
// $events_loc.launcher.datetime_format = 'datetime_long'; $events_loc.launcher.time_format = 'time_short';
$events_loc.launcher.time_format = 'time_short'; $events_loc.launcher.time_hours = 24;
$events_loc.launcher.time_hours = 24; } else {
} else { $events_loc.launcher.time_format =
$events_loc.launcher.time_format = 'time_12_short'; 'time_12_short';
// $events_loc.launcher.datetime_format = 'datetime_12_long'; // $events_loc.launcher.datetime_format = 'datetime_12_long';
$events_loc.launcher.time_hours = 12; $events_loc.launcher.time_hours = 12;
} }
}} }}>
> <strong
<strong >{ae_util.iso_datetime_formatter(
>{ae_util.iso_datetime_formatter( $lq__event_session_obj.start_datetime,
$lq__event_session_obj.start_datetime, 'week_long'
'week_long' )}</strong>
)}</strong <span class="font-normal">
> {ae_util.iso_datetime_formatter(
<span class="font-normal"> $lq__event_session_obj.start_datetime,
{ae_util.iso_datetime_formatter( 'date_long_month_day'
$lq__event_session_obj.start_datetime, )}
'date_long_month_day' </span>
)} <strong
</span> >{ae_util.iso_datetime_formatter(
<strong $lq__event_session_obj.start_datetime,
>{ae_util.iso_datetime_formatter( $events_loc.launcher.time_format
$lq__event_session_obj.start_datetime, )}</strong>
$events_loc.launcher.time_format <span class="font-normal">
)}</strong
> {ae_util.iso_datetime_formatter(
<span class="font-normal"> $lq__event_session_obj.end_datetime,
$events_loc.launcher.time_format
{ae_util.iso_datetime_formatter( )}
$lq__event_session_obj.end_datetime, </span>
$events_loc.launcher.time_format </button>
)} </h3>
</span>
</button>
</h3>
<span <span
class="w-full flex flex-row gap-2 items-center justify-between" class="flex w-full flex-row items-center justify-between gap-2">
> <!-- grow + line-clamp-2 = stable 2-line max; title provides full text for screen readers + hover -->
<!-- grow + line-clamp-2 = stable 2-line max; title provides full text for screen readers + hover --> <h2
<h2 class="line-clamp-2 min-w-0 grow text-xl"
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)}`}>
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>
{$lq__event_session_obj?.name} {#if $lq__event_session_obj?.code}
</h2> <!-- shrink-0: code badge never gets squeezed by a long name -->
{#if $lq__event_session_obj?.code} <span
<!-- shrink-0: code badge never gets squeezed by a long name --> class="shrink-0 p-1 text-base font-normal text-gray-500"
<span title="Session code {$lq__event_session_obj.code}">
class="shrink-0 text-base text-gray-500 font-normal p-1" <Barcode size="1em" class="inline" />
title="Session code {$lq__event_session_obj.code}" {$lq__event_session_obj?.code}
> </span>
<Barcode size="1em" class="inline" /> {/if}
{$lq__event_session_obj?.code} </span>
</span> </header>
{/if}
</span>
</header>
<!-- <section class="event_session_description text-xs" class:d_none="{hide_description}"> <!-- <section class="event_session_description text-xs" class:d_none="{hide_description}">
{@html $lq__event_session_obj.description} {@html $lq__event_session_obj.description}
</section> --> </section> -->
{#if $lq__event_session_obj?.file_count_all === 0} {#if $lq__event_session_obj?.file_count_all === 0}
<p class="text-2xl text-center text-red-500 font-bold"> <p class="text-center text-2xl font-bold text-red-500">
<AlertTriangle size="1em" class="inline" /> <AlertTriangle size="1em" class="inline" />
Warning Warning
<AlertTriangle size="1em" class="inline" /> <AlertTriangle size="1em" class="inline" />
<br /> <br />
No files available show for this session. No files available show for this session.
</p> </p>
{/if} {/if}
{#if $lq__event_file_obj_li && $lq__event_file_obj_li.length} {#if $lq__event_file_obj_li && $lq__event_file_obj_li.length}
<section class="event_session_file_list"> <section class="event_session_file_list">
<div> <div>
<div class="text-xs text-surface-600-400"> <div class="text-surface-600-400 text-xs">
<strong> <strong>
<Archive size="1em" class="inline" /> <Archive size="1em" class="inline" />
Session Files: Session Files:
<span <span
class:hidden={!$ae_loc.trusted_access || class:hidden={!$ae_loc.trusted_access ||
!$ae_loc.edit_mode} !$ae_loc.edit_mode}>
> ({$lq__event_file_obj_li?.length}&times;)
({$lq__event_file_obj_li?.length}&times;) </span>
</span> </strong>
</strong> </div>
</div> <!-- {#if $ae_loc.trusted_access || $events_loc.launcher.trusted_access}
<!-- {#if $ae_loc.trusted_access || $events_loc.launcher.trusted_access}
<button type="button" <button type="button"
type="button" class="ae_btn btn_outline_warning btn_xs" title="Upload updated or additional files" 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) <span class="fas fa-upload"></span> Upload Session File(s)
</button> </button>
{/if} --> {/if} -->
</div> </div>
<ul class="space-y-1"> <ul class="space-y-1">
{#each $lq__event_file_obj_li as event_file_obj (event_file_obj.event_file_id)} {#each $lq__event_file_obj_li as event_file_obj (event_file_obj.event_file_id)}
<li <li
class="flex flex-row flex-wrap gap-1 items-center justify-center" class="flex flex-row flex-wrap items-center justify-center gap-1"
class:hidden={!$events_loc.launcher class:hidden={!$events_loc.launcher
.show_content__hidden_files && .show_content__hidden_files &&
event_file_obj.hide} event_file_obj.hide}>
> <Event_launcher_file_cont
<Event_launcher_file_cont event_file_id={event_file_obj.event_file_id}
event_file_id={event_file_obj.event_file_id} {event_file_obj}
{event_file_obj} hide_created_on={true}
hide_created_on={true} show_bak_download={$ae_loc.trusted_access &&
show_bak_download={$ae_loc.trusted_access && $ae_loc.edit_mode}
$ae_loc.edit_mode} session_type={type_code || 'oral'}
session_type={type_code || 'oral'} open_method={type_code == 'poster'
open_method={type_code == 'poster' ? 'modal' : null} ? 'modal'
modal_title={$lq__event_session_obj?.name} : null}
bind:modal__title={ modal_title={$lq__event_session_obj?.name}
$events_sess.launcher.modal__title bind:modal__title={
} $events_sess.launcher.modal__title
bind:modal__open_event_file_id={ }
$events_sess.launcher bind:modal__open_event_file_id={
.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 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} /> --> <!-- <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 <!-- <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}" 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="btn btn-sm variant-soft-secondary m-0.5 *:hover:inline"
class:hidden={!ae_tmp.show__direct_download} class:hidden={!ae_tmp.show__direct_download}
@@ -330,94 +338,93 @@
Download Download
</div> </div>
</a> --> </a> -->
</li> </li>
{/each} {/each}
</ul> </ul>
</section> </section>
{/if} {/if}
<!-- <hr class="w-full border border-gray-200" /> --> <!-- <hr class="w-full border border-gray-200" /> -->
<section class="event_presentation_list"> <section class="event_presentation_list">
<!-- {$lq__event_session_obj?.event_presentation_li?.length ?? 'loading...?'} --> <!-- {$lq__event_session_obj?.event_presentation_li?.length ?? 'loading...?'} -->
{#if $lq__event_presentation_obj_li} {#if $lq__event_presentation_obj_li}
<div class="text-xs text-surface-600-400"> <div class="text-surface-600-400 text-xs">
<strong> <strong>
{#if type_code == 'poster'} {#if type_code == 'poster'}
<Image size="1em" class="inline" /> <Image size="1em" class="inline" />
Posters: Posters:
{:else} {:else}
<Monitor size="1em" class="inline" /> <Monitor size="1em" class="inline" />
Presentations: Presentations:
{/if} {/if}
{#if $ae_loc.administrator_access && $ae_loc.edit_mode} {#if $ae_loc.administrator_access && $ae_loc.edit_mode}
({$lq__event_presentation_obj_li?.length}&times;) ({$lq__event_presentation_obj_li?.length}&times;)
{/if} {/if}
</strong> </strong>
</div> </div>
<!-- Maybe set max with? max-w-(--breakpoint-md) --> <!-- Maybe set max with? max-w-(--breakpoint-md) -->
<ul class="event_presentation_list max-w-full space-y-2"> <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)} {#each $lq__event_presentation_obj_li as event_presentation_obj (event_presentation_obj.event_presentation_id)}
<li <li
class="border-b-2 border-gray-300 dark:border-gray-700 my-1 py-1 text-center md:text-left" class="my-1 border-b-2 border-gray-300 py-1 text-center md:text-left dark:border-gray-700">
> <!-- The presentation information -->
<!-- The presentation information --> <div
<div class="event_presentation_datetime_name flex flex-row justify-evenly gap-4">
class="event_presentation_datetime_name flex flex-row justify-evenly gap-4" <!-- <div class="event_presentation_datetime_name"> -->
> {#if event_presentation_obj?.start_datetime}
<!-- <div class="event_presentation_datetime_name"> --> <span
{#if event_presentation_obj?.start_datetime} class="event_presentation_datetime"
<span class="event_presentation_datetime" ><strong
><strong >{ae_util.iso_datetime_formatter(
>{ae_util.iso_datetime_formatter( event_presentation_obj?.start_datetime,
event_presentation_obj?.start_datetime, 'time_12_short_no_leading'
'time_12_short_no_leading' )}</strong
)}</strong ></span>
></span {/if}
>
{/if}
<span class="event_presentation_name grow" <span class="event_presentation_name grow"
>{event_presentation_obj?.name}</span >{event_presentation_obj?.name}</span>
> <!-- </div> -->
<!-- </div> -->
<!-- Yes, this is kind of inefficient, but it works for now. --> <!-- Yes, this is kind of inefficient, but it works for now. -->
{#if $lq__event_presenter_obj_li && type_code == 'poster'} {#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)} {#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} {#if event_presenter_obj.event_presentation_id == event_presentation_obj.event_presentation_id}
<span <span
class="event_presentation_single_presenter italic text-sm text-gray-500" class="event_presentation_single_presenter text-sm text-gray-500 italic">
> {#if $lq__event_presenter_obj_li[index]?.given_name && $lq__event_presenter_obj_li[index]?.given_name != 'Group'}
{#if $lq__event_presenter_obj_li[index]?.given_name && $lq__event_presenter_obj_li[index]?.given_name != 'Group'} <User
<User size="0.85em" class="inline" /> size="0.85em"
{$lq__event_presenter_obj_li[ class="inline" />
index {$lq__event_presenter_obj_li[
]?.full_name} index
{:else if $lq__event_presenter_obj_li[index]?.given_name == 'Group'} ]?.full_name}
<Users size="0.85em" class="inline" /> {:else if $lq__event_presenter_obj_li[index]?.given_name == 'Group'}
{$lq__event_presenter_obj_li[ <Users
index size="0.85em"
]?.affiliations} class="inline" />
{:else} {$lq__event_presenter_obj_li[
--not set-- index
{/if} ]?.affiliations}
</span> {:else}
{/if} --not set--
{/each} {/if}
{/if} </span>
</div> {/if}
{/each}
{/if}
</div>
<!-- Presentation-level files --> <!-- Presentation-level files -->
<Launcher_presentation_view <Launcher_presentation_view
lq__event_presentation_obj={event_presentation_obj} lq__event_presentation_obj={event_presentation_obj}
session_type={type_code} session_type={type_code} />
/>
<!-- The presenter list --> <!-- The presenter list -->
<!-- WHY: In poster mode, presenter names are already shown inline <!-- WHY: In poster mode, presenter names are already shown inline
in the presentation header above, so hide_name=true. in the presentation header above, so hide_name=true.
We still render Launcher_presenter_view_posters here because We still render Launcher_presenter_view_posters here because
some events store files at the PRESENTER level (for_id=event_presenter_id) some events store files at the PRESENTER level (for_id=event_presenter_id)
@@ -425,43 +432,41 @@
The component renders nothing if there are no presenter-level files, The component renders nothing if there are no presenter-level files,
so this has no visual cost for events that use presentation-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} {#if $lq__event_presenter_obj_li && $lq__event_presenter_obj_li.length}
<ul class="event_presentation_presenter_list"> <ul
{#each $lq__event_presenter_obj_li as event_presenter_obj (event_presenter_obj.event_presenter_id)} class="event_presentation_presenter_list">
{#if event_presenter_obj.event_presentation_id == event_presentation_obj.event_presentation_id} {#each $lq__event_presenter_obj_li as event_presenter_obj (event_presenter_obj.event_presenter_id)}
<li {#if event_presenter_obj.event_presentation_id == event_presentation_obj.event_presentation_id}
class=" <li
border border-transparent class="
hover:bg-surface-100-900 hover:border-surface-400-600
rounded-lg rounded-lg
hover:bg-surface-100-900 border
hover:border-surface-400-600 border-transparent
p-1 p-1
transition-all transition-all
" ">
> {#if type_code == 'poster'}
{#if type_code == 'poster'} <Launcher_presenter_view_posters
<Launcher_presenter_view_posters lq__event_presenter_obj={event_presenter_obj}
lq__event_presenter_obj={event_presenter_obj} hide_name={true} />
hide_name={true} {:else}
/> <Launcher_presenter_view
{:else} lq__event_presenter_obj={event_presenter_obj}
<Launcher_presenter_view session_type={type_code} />
lq__event_presenter_obj={event_presenter_obj} {/if}
session_type={type_code} </li>
/> {/if}
{/if} {/each}
</li> </ul>
{/if} {/if}
{/each} </li>
</ul> {/each}
{/if} </ul>
</li> {:else}
{/each} <p>No presentations available to display.</p>
</ul> {/if}
{:else} </section>
<p>No presentations available to display.</p>
{/if}
</section>
{/if}<!-- end type_code !== 'poster' --> {/if}<!-- end type_code !== 'poster' -->
{:else} {:else}
<LoaderCircle size="1em" class="inline animate-spin" /> <LoaderCircle size="1em" class="inline animate-spin" />

View File

@@ -1,78 +1,78 @@
<script lang="ts"> <script lang="ts">
/** /**
* launcher_session_view_posters.svelte * launcher_session_view_posters.svelte
* *
* WHY: Digital Poster sessions need a dedicated card-grid layout optimised for * WHY: Digital Poster sessions need a dedicated card-grid layout optimised for
* touch-first PWA use on tablets and phones. Keeping this separate from the oral * touch-first PWA use on tablets and phones. Keeping this separate from the oral
* session view avoids cluttering that template with a growing set of poster-specific * session view avoids cluttering that template with a growing set of poster-specific
* branches and lets each view own its own layout concerns cleanly. * branches and lets each view own its own layout concerns cleanly.
* *
* The menu will typically be hidden and the page may be in iframe mode * The menu will typically be hidden and the page may be in iframe mode
* ($ae_loc.iframe == true), so this view fills the full available width. * ($ae_loc.iframe == true), so this view fills the full available width.
* *
* Deployment context: operator tablet / phone PWA, also works on desktop. * Deployment context: operator tablet / phone PWA, also works on desktop.
*/ */
interface Props { interface Props {
slct__event_session_id?: string | null; slct__event_session_id?: string | null;
log_lvl?: number; log_lvl?: number;
} }
let { let {
slct__event_session_id = $bindable(null), slct__event_session_id = $bindable(null),
log_lvl = $bindable(1) log_lvl = $bindable(1)
}: Props = $props(); }: Props = $props();
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
import { ae_loc } from '$lib/stores/ae_stores'; import { ae_loc } from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events'; import { db_events } from '$lib/ae_events/db_events';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores'; import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import Event_launcher_file_cont from './launcher_file_cont.svelte'; import Event_launcher_file_cont from './launcher_file_cont.svelte';
import { Image, Images, LoaderCircle, User, Users } from '@lucide/svelte'; import { Image, Images, LoaderCircle, User, Users } from '@lucide/svelte';
import Launcher_presentation_view from './launcher_presentation_view.svelte'; import Launcher_presentation_view from './launcher_presentation_view.svelte';
import Launcher_presenter_view_posters from './launcher_presenter_view_posters.svelte'; import Launcher_presenter_view_posters from './launcher_presenter_view_posters.svelte';
// Session object // Session object
let lq__event_session_obj = $derived( let lq__event_session_obj = $derived(
liveQuery(() => db_events.session.get(slct__event_session_id)) liveQuery(() => db_events.session.get(slct__event_session_id))
); );
// Session-level files (rare — program PDFs, flyers, etc.) // Session-level files (rare — program PDFs, flyers, etc.)
let lq__event_file_obj_li = $derived( let lq__event_file_obj_li = $derived(
liveQuery(async () => { liveQuery(async () => {
if (!slct__event_session_id) return []; if (!slct__event_session_id) return [];
return await db_events.file return await db_events.file
.where('for_id') .where('for_id')
.equals(slct__event_session_id) .equals(slct__event_session_id)
.reverse() .reverse()
.sortBy('created_on'); .sortBy('created_on');
}) })
); );
// Presentations sorted alphabetically — typical for poster sessions // Presentations sorted alphabetically — typical for poster sessions
let lq__event_presentation_obj_li = $derived( let lq__event_presentation_obj_li = $derived(
liveQuery(async () => { liveQuery(async () => {
if (!slct__event_session_id) return []; if (!slct__event_session_id) return [];
return await db_events.presentation return await db_events.presentation
.where('event_session_id') .where('event_session_id')
.equals(slct__event_session_id) .equals(slct__event_session_id)
.sortBy('name'); .sortBy('name');
}) })
); );
// All presenters for this session; filtered per card in the template // All presenters for this session; filtered per card in the template
let lq__event_presenter_obj_li = $derived( let lq__event_presenter_obj_li = $derived(
liveQuery(async () => { liveQuery(async () => {
if (!slct__event_session_id) return []; if (!slct__event_session_id) return [];
return await db_events.presenter return await db_events.presenter
.where('event_session_id') .where('event_session_id')
.equals(slct__event_session_id) .equals(slct__event_session_id)
.sortBy('full_name'); .sortBy('full_name');
}) })
); );
// Poster count for the session header badge // Poster count for the session header badge
let poster_count = $derived($lq__event_presentation_obj_li?.length ?? 0); let poster_count = $derived($lq__event_presentation_obj_li?.length ?? 0);
</script> </script>
<!-- <!--
@@ -80,43 +80,42 @@
The outer div mirrors the grow/h-full/w-full contract expected by The outer div mirrors the grow/h-full/w-full contract expected by
+layout.svelte so this slots in as a drop-in replacement for the oral view. +layout.svelte so this slots in as a drop-in replacement for the oral view.
--> -->
<div class="poster_session_view flex flex-col gap-0 w-full h-full overflow-hidden"> <div
class="poster_session_view flex h-full w-full flex-col gap-0 overflow-hidden">
{#if $events_sess.launcher?.loading__session_id_status} {#if $events_sess.launcher?.loading__session_id_status}
<span class="absolute top-0 right-0 text-sm text-gray-400 flex items-center gap-1 p-1 z-10"> <span
class="absolute top-0 right-0 z-10 flex items-center gap-1 p-1 text-sm text-gray-400">
<LoaderCircle size="1em" class="animate-spin" /> <LoaderCircle size="1em" class="animate-spin" />
Loading... Loading...
</span> </span>
{/if} {/if}
{#if $lq__event_session_obj && $lq__event_session_obj.event_session_id} {#if $lq__event_session_obj && $lq__event_session_obj.event_session_id}
<!-- ── Compact session identity strip ──────────────────────────────── --> <!-- ── Compact session identity strip ──────────────────────────────── -->
<header <header
class=" class="
poster_session_header poster_session_header
flex flex-row gap-2 items-center justify-between border-surface-300 dark:border-surface-600 bg-surface-100/60 dark:bg-surface-800/60 flex
px-2 py-1.5 shrink-0 flex-row
border-b-2 border-surface-300 dark:border-surface-600 items-center justify-between gap-2
bg-surface-100/60 dark:bg-surface-800/60 border-b-2 px-2
shrink-0 py-1.5
" ">
>
<h2 <h2
class="text-base font-bold line-clamp-1 grow min-w-0" class="line-clamp-1 min-w-0 grow text-base font-bold"
title={$lq__event_session_obj.name} title={$lq__event_session_obj.name}>
> <Images
<Images size="1em" class="inline mr-1.5 text-primary-500 opacity-70" /> size="1em"
class="text-primary-500 mr-1.5 inline opacity-70" />
{$lq__event_session_obj.name} {$lq__event_session_obj.name}
</h2> </h2>
<span class="flex flex-row gap-1.5 items-center shrink-0"> <span class="flex shrink-0 flex-row items-center gap-1.5">
<!-- Poster count badge --> <!-- Poster count badge -->
{#if poster_count > 0} {#if poster_count > 0}
<span <span
class="text-xs font-mono font-semibold text-surface-500 bg-surface-200 dark:bg-surface-700 px-2 py-0.5 rounded-full" class="text-surface-500 bg-surface-200 dark:bg-surface-700 rounded-full px-2 py-0.5 font-mono text-xs font-semibold"
title="Number of posters in this session" title="Number of posters in this session">
>
{poster_count}&times; {poster_count}&times;
</span> </span>
{/if} {/if}
@@ -124,9 +123,8 @@
<!-- Session code --> <!-- Session code -->
{#if $lq__event_session_obj.code} {#if $lq__event_session_obj.code}
<span <span
class="text-xs font-mono font-bold text-surface-400 bg-surface-100 dark:bg-surface-800 px-2 py-0.5 rounded border border-surface-300 dark:border-surface-600" class="text-surface-400 bg-surface-100 dark:bg-surface-800 border-surface-300 dark:border-surface-600 rounded border px-2 py-0.5 font-mono text-xs font-bold"
title="Session code: {$lq__event_session_obj.code}" title="Session code: {$lq__event_session_obj.code}">
>
{$lq__event_session_obj.code} {$lq__event_session_obj.code}
</span> </span>
{/if} {/if}
@@ -135,16 +133,18 @@
<!-- ── Session-level files (rarely present — program, schedule, etc.) ── --> <!-- ── Session-level files (rarely present — program, schedule, etc.) ── -->
{#if $lq__event_file_obj_li && $lq__event_file_obj_li.length} {#if $lq__event_file_obj_li && $lq__event_file_obj_li.length}
<section class="session_resources_strip px-2 pb-2 pt-1 border-b border-surface-200 dark:border-surface-700 shrink-0"> <section
<p class="text-[10px] text-surface-500 uppercase font-bold tracking-wider mb-1 opacity-60"> class="session_resources_strip border-surface-200 dark:border-surface-700 shrink-0 border-b px-2 pt-1 pb-2">
<p
class="text-surface-500 mb-1 text-[10px] font-bold tracking-wider uppercase opacity-60">
Session Resources: Session Resources:
</p> </p>
<ul class="flex flex-row flex-wrap gap-2"> <ul class="flex flex-row flex-wrap gap-2">
{#each $lq__event_file_obj_li as event_file_obj (event_file_obj.event_file_id)} {#each $lq__event_file_obj_li as event_file_obj (event_file_obj.event_file_id)}
<li <li
class:hidden={!$events_loc.launcher.show_content__hidden_files && class:hidden={!$events_loc.launcher
event_file_obj.hide} .show_content__hidden_files &&
> event_file_obj.hide}>
<Event_launcher_file_cont <Event_launcher_file_cont
event_file_id={event_file_obj.event_file_id} event_file_id={event_file_obj.event_file_id}
{event_file_obj} {event_file_obj}
@@ -163,8 +163,7 @@
} }
bind:modal__event_file_obj={ bind:modal__event_file_obj={
$events_sess.launcher.modal__event_file_obj $events_sess.launcher.modal__event_file_obj
} } />
/>
</li> </li>
{/each} {/each}
</ul> </ul>
@@ -174,18 +173,20 @@
<!-- ── Poster card grid ────────────────────────────────────────────── --> <!-- ── Poster card grid ────────────────────────────────────────────── -->
{#if $lq__event_presentation_obj_li === undefined} {#if $lq__event_presentation_obj_li === undefined}
<!-- Still resolving from Dexie --> <!-- Still resolving from Dexie -->
<div class="flex items-center justify-center gap-2 p-10 opacity-40 grow"> <div
class="flex grow items-center justify-center gap-2 p-10 opacity-40">
<LoaderCircle size="2em" class="animate-spin" /> <LoaderCircle size="2em" class="animate-spin" />
<span>Loading posters…</span> <span>Loading posters…</span>
</div> </div>
{:else if $lq__event_presentation_obj_li.length === 0} {:else if $lq__event_presentation_obj_li.length === 0}
<!-- Loaded but empty --> <!-- Loaded but empty -->
<div class="flex flex-col items-center justify-center gap-3 p-12 opacity-40 text-center grow"> <div
class="flex grow flex-col items-center justify-center gap-3 p-12 text-center opacity-40">
<Image size="3em" /> <Image size="3em" />
<p class="text-lg font-medium">No posters in this session yet.</p> <p class="text-lg font-medium">
No posters in this session yet.
</p>
</div> </div>
{:else} {:else}
<!-- <!--
Grid: 1 col on phone, 2 on tablet (sm), 3 on large desktop (xl). Grid: 1 col on phone, 2 on tablet (sm), 3 on large desktop (xl).
@@ -197,15 +198,14 @@
class=" class="
poster_card_grid poster_card_grid
grid grid
grow
grid-cols-1 grid-cols-1
gap-3
overflow-y-auto
p-3
sm:grid-cols-2 sm:grid-cols-2
xl:grid-cols-3 xl:grid-cols-3
gap-3 ">
p-3
overflow-y-auto
grow
"
>
{#each $lq__event_presentation_obj_li as presentation, i (presentation.event_presentation_id)} {#each $lq__event_presentation_obj_li as presentation, i (presentation.event_presentation_id)}
{@const presenters_for_this = ( {@const presenters_for_this = (
$lq__event_presenter_obj_li ?? [] $lq__event_presenter_obj_li ?? []
@@ -223,18 +223,17 @@
<li <li
class=" class="
poster_card poster_card
relative flex flex-col gap-2 border-surface-200 dark:border-surface-700 dark:bg-surface-900 hover:border-primary-400
rounded-xl dark:hover:border-primary-500
border border-surface-200 dark:border-surface-700 relative flex min-h-40
bg-white dark:bg-surface-900 flex-col gap-2
hover:border-primary-400 dark:hover:border-primary-500 rounded-xl border
active:scale-[0.98] active:shadow-sm bg-white p-3
transition-all duration-150 shadow-sm transition-all
shadow-sm hover:shadow-md duration-150 hover:shadow-md
p-3 active:scale-[0.98]
min-h-40 active:shadow-sm
" ">
>
<!-- <!--
Top-right badge: prefer the presentation code (e.g. "P-042") Top-right badge: prefer the presentation code (e.g. "P-042")
as it matches physical poster board numbers; fall back to as it matches physical poster board numbers; fall back to
@@ -242,18 +241,17 @@
--> -->
<span <span
class=" class="
absolute top-2 right-2 text-primary-600 dark:text-primary-400 bg-primary-50
text-xs font-mono font-bold leading-tight dark:bg-primary-950/60 border-primary-200 dark:border-primary-800 absolute
text-primary-600 dark:text-primary-400 top-2 right-2
bg-primary-50 dark:bg-primary-950/60 rounded-full border
border border-primary-200 dark:border-primary-800 px-2 py-0.5 font-mono
px-2 py-0.5 text-xs leading-tight
rounded-full font-bold
" "
title="{presentation.code title={presentation.code
? 'Poster code: ' + presentation.code ? 'Poster code: ' + presentation.code
: 'Poster #' + (i + 1)}" : 'Poster #' + (i + 1)}>
>
{presentation.code || '#' + (i + 1)} {presentation.code || '#' + (i + 1)}
</span> </span>
@@ -266,15 +264,14 @@
<h3 <h3
class=" class="
poster_title poster_title
text-base md:text-lg
font-bold leading-snug
line-clamp-3
text-surface-950 dark:text-surface-50 text-surface-950 dark:text-surface-50
line-clamp-3 grow
pr-14 pr-14
grow text-base leading-snug
font-bold
md:text-lg
" "
title={presentation.name} title={presentation.name}>
>
{presentation.name} {presentation.name}
</h3> </h3>
@@ -286,37 +283,38 @@
"Group" presenter whose full name is stored in affiliations. "Group" presenter whose full name is stored in affiliations.
--> -->
{#if presenters_for_this.length} {#if presenters_for_this.length}
<div class="presenter_info space-y-0.5 shrink-0"> <div class="presenter_info shrink-0 space-y-0.5">
{#each presenters_for_this as presenter (presenter.event_presenter_id)} {#each presenters_for_this as presenter (presenter.event_presenter_id)}
<p <p
class=" class="
flex flex-row flex-wrap items-baseline gap-x-1.5 text-surface-500 dark:text-surface-400 flex flex-row flex-wrap
text-sm text-surface-500 dark:text-surface-400 items-baseline gap-x-1.5 text-sm
leading-snug leading-snug
" ">
>
{#if presenter.given_name && presenter.given_name !== 'Group'} {#if presenter.given_name && presenter.given_name !== 'Group'}
<User size="0.7em" class="opacity-50 shrink-0 mt-px" /> <User
size="0.7em"
class="mt-px shrink-0 opacity-50" />
<span <span
class="font-medium text-surface-700 dark:text-surface-300" class="text-surface-700 dark:text-surface-300 font-medium"
>{presenter.full_name}</span >{presenter.full_name}</span>
>
{#if presenter.affiliations} {#if presenter.affiliations}
<span <span
class="italic text-xs opacity-70 line-clamp-1 min-w-0" class="line-clamp-1 min-w-0 text-xs italic opacity-70"
title={presenter.affiliations} title={presenter.affiliations}>
>
{presenter.affiliations} {presenter.affiliations}
</span> </span>
{/if} {/if}
{:else if presenter.given_name === 'Group'} {:else if presenter.given_name === 'Group'}
<Users size="0.7em" class="opacity-50 shrink-0 mt-px" /> <Users
size="0.7em"
class="mt-px shrink-0 opacity-50" />
<span <span
class="font-medium text-surface-700 dark:text-surface-300" class="text-surface-700 dark:text-surface-300 font-medium"
>{presenter.affiliations}</span >{presenter.affiliations}</span>
>
{:else} {:else}
<span class="opacity-40 text-xs"></span> <span class="text-xs opacity-40"
></span>
{/if} {/if}
</p> </p>
{/each} {/each}
@@ -329,12 +327,12 @@
presenter level, or both — render both sub-components so presenter level, or both — render both sub-components so
neither source is missed. neither source is missed.
--> -->
<div class="poster_actions flex flex-col gap-1 mt-auto pt-1 shrink-0"> <div
class="poster_actions mt-auto flex shrink-0 flex-col gap-1 pt-1">
<!-- Presentation-level files (the most common attachment point) --> <!-- Presentation-level files (the most common attachment point) -->
<Launcher_presentation_view <Launcher_presentation_view
lq__event_presentation_obj={presentation} lq__event_presentation_obj={presentation}
session_type="poster" session_type="poster" />
/>
<!-- <!--
Presenter-level files (some events attach the PDF here instead). Presenter-level files (some events attach the PDF here instead).
@@ -344,18 +342,16 @@
{#each presenters_for_this as presenter (presenter.event_presenter_id)} {#each presenters_for_this as presenter (presenter.event_presenter_id)}
<Launcher_presenter_view_posters <Launcher_presenter_view_posters
lq__event_presenter_obj={presenter} lq__event_presenter_obj={presenter}
hide_name={true} hide_name={true} />
/>
{/each} {/each}
</div> </div>
</li> </li>
{/each} {/each}
</ul> </ul>
{/if} {/if}
{:else} {:else}
<!-- No session selected or still loading --> <!-- No session selected or still loading -->
<div class="flex items-center justify-center gap-2 p-8 opacity-40 grow"> <div class="flex grow items-center justify-center gap-2 p-8 opacity-40">
<LoaderCircle size="1em" class="animate-spin" /> <LoaderCircle size="1em" class="animate-spin" />
<span>No session selected</span> <span>No session selected</span>
</div> </div>

View File

@@ -1,39 +1,40 @@
<script lang="ts"> <script lang="ts">
/** /**
* menu_launcher_controls.svelte — Aether Launcher: Bottom Control Bar * menu_launcher_controls.svelte — Aether Launcher: Bottom Control Bar
* *
* PURPOSE: * PURPOSE:
* Accessibility and visibility controls for the Launcher sidebar. Placed at the * Accessibility and visibility controls for the Launcher sidebar. Placed at the
* bottom of launcher_menu.svelte so presenters and operators can adjust display * bottom of launcher_menu.svelte so presenters and operators can adjust display
* settings without needing access to the global AE System Menu (which may be * settings without needing access to the global AE System Menu (which may be
* unavailable in kiosk, locked, or podium setups). * unavailable in kiosk, locked, or podium setups).
* *
* SECTIONS: * SECTIONS:
* 1. Visibility toggles (edit mode only) — show/hide draft files and hidden sessions * 1. Visibility toggles (edit mode only) — show/hide draft files and hidden sessions
* 2. Accessibility controls (always visible) — font size cycler and light/dark toggle * 2. Accessibility controls (always visible) — font size cycler and light/dark toggle
* *
* WHY ALWAYS-VISIBLE ACCESSIBILITY CONTROLS: * WHY ALWAYS-VISIBLE ACCESSIBILITY CONTROLS:
* Projector-connected screens, tablets, and phones at conference venues vary wildly * Projector-connected screens, tablets, and phones at conference venues vary wildly
* in lighting. Operators and presenters need quick one-tap access to font size and * in lighting. Operators and presenters need quick one-tap access to font size and
* theme mode without hunting through the system menu or requiring admin access. * theme mode without hunting through the system menu or requiring admin access.
*/ */
import { Moon, Sun, Eye, EyeOff } from '@lucide/svelte'; import { Moon, Sun, Eye, EyeOff } from '@lucide/svelte';
import { ae_loc } from '$lib/stores/ae_stores'; import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores'; import { events_loc } from '$lib/stores/ae_events_stores';
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
} }
let { log_lvl = $bindable(0) }: Props = $props(); let { log_lvl = $bindable(0) }: Props = $props();
</script> </script>
<div class="w-full max-w-full flex flex-col gap-1 items-center justify-center"> <div class="flex w-full max-w-full flex-col items-center justify-center gap-1">
<!-- ── Visibility toggles — edit mode only ── --> <!-- ── Visibility toggles — edit mode only ── -->
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<div class="w-full max-w-full flex flex-row gap-1 items-center justify-center"> <div
class="flex w-full max-w-full flex-row items-center justify-center gap-1">
<button <button
type="button" type="button"
onclick={() => { onclick={() => {
@@ -46,13 +47,12 @@
} }
}} }}
class=" class="
btn btn-sm text-xs btn btn-sm preset-tonal-tertiary
w-1/2 max-w-1/2 hover:preset-filled-tertiary-500 w-1/2
preset-tonal-tertiary hover:preset-filled-tertiary-500 max-w-1/2 text-xs
transition-all transition-all
" "
title="Toggle visibility of hidden and draft files in the launcher file list." title="Toggle visibility of hidden and draft files in the launcher file list.">
>
{#if $events_loc.launcher.show_content__hidden_files} {#if $events_loc.launcher.show_content__hidden_files}
<EyeOff size="0.85em" class="m-1 text-neutral-800/80" /> <EyeOff size="0.85em" class="m-1 text-neutral-800/80" />
Hide Files Hide Files
@@ -69,13 +69,12 @@
!$events_loc.launcher.show_content__hidden_sessions; !$events_loc.launcher.show_content__hidden_sessions;
}} }}
class=" class="
btn btn-sm text-xs btn btn-sm preset-tonal-tertiary
w-1/2 max-w-1/2 hover:preset-filled-tertiary-500 w-1/2
preset-tonal-tertiary hover:preset-filled-tertiary-500 max-w-1/2 text-xs
transition-all transition-all
" "
title="Toggle visibility of hidden and cancelled sessions in the launcher session list." title="Toggle visibility of hidden and cancelled sessions in the launcher session list.">
>
{#if $events_loc.launcher.show_content__hidden_sessions} {#if $events_loc.launcher.show_content__hidden_sessions}
<EyeOff size="0.85em" class="m-1 text-neutral-800/80" /> <EyeOff size="0.85em" class="m-1 text-neutral-800/80" />
Hide Sessions Hide Sessions
@@ -88,7 +87,8 @@
{/if} {/if}
<!-- ── Accessibility controls — always visible ── --> <!-- ── Accessibility controls — always visible ── -->
<div class="w-full max-w-full flex flex-row gap-1 items-center justify-center"> <div
class="flex w-full max-w-full flex-row items-center justify-center gap-1">
<!-- Font size cycler: default → larger → smaller → default --> <!-- Font size cycler: default → larger → smaller → default -->
<button <button
type="button" type="button"
@@ -103,22 +103,28 @@
} }
}} }}
class=" class="
btn btn-sm text-xs btn btn-sm preset-tonal-tertiary
hover:preset-filled-tertiary-500 group
w-1/2 max-w-1/2 w-1/2 max-w-1/2
preset-tonal-tertiary hover:preset-filled-tertiary-500 text-xs transition-all
transition-all group
" "
title="Cycle font size (default → larger → smaller). Current: {$ae_loc.font_size_mode ?? 'default'}" title="Cycle font size (default → larger → smaller). Current: {$ae_loc.font_size_mode ??
> 'default'}">
{#if !$ae_loc.font_size_mode || $ae_loc.font_size_mode === 'default'} {#if !$ae_loc.font_size_mode || $ae_loc.font_size_mode === 'default'}
<span class="font-bold text-sm font-mono leading-none m-1">A</span> <span class="m-1 font-mono text-sm leading-none font-bold"
<span class="hidden group-hover:inline-block text-xs">Font: Normal</span> >A</span>
<span class="hidden text-xs group-hover:inline-block"
>Font: Normal</span>
{:else if $ae_loc.font_size_mode === 'larger'} {:else if $ae_loc.font_size_mode === 'larger'}
<span class="font-bold text-base font-mono leading-none m-1">A+</span> <span class="m-1 font-mono text-base leading-none font-bold"
<span class="hidden group-hover:inline-block text-xs">Font: Larger</span> >A+</span>
<span class="hidden text-xs group-hover:inline-block"
>Font: Larger</span>
{:else} {:else}
<span class="font-bold text-xs font-mono leading-none m-1">A</span> <span class="m-1 font-mono text-xs leading-none font-bold"
<span class="hidden group-hover:inline-block text-xs">Font: Smaller</span> >A</span>
<span class="hidden text-xs group-hover:inline-block"
>Font: Smaller</span>
{/if} {/if}
</button> </button>
@@ -126,16 +132,17 @@
<button <button
type="button" type="button"
onclick={() => { onclick={() => {
$ae_loc.theme_mode = $ae_loc.theme_mode === 'dark' ? 'light' : 'dark'; $ae_loc.theme_mode =
$ae_loc.theme_mode === 'dark' ? 'light' : 'dark';
}} }}
class=" class="
btn btn-sm text-xs btn btn-sm preset-tonal-tertiary
hover:preset-filled-tertiary-500 group
w-1/2 max-w-1/2 w-1/2 max-w-1/2
preset-tonal-tertiary hover:preset-filled-tertiary-500 text-xs transition-all
transition-all group
" "
title="Toggle light/dark display mode. Current: {$ae_loc.theme_mode ?? 'light'}" title="Toggle light/dark display mode. Current: {$ae_loc.theme_mode ??
> 'light'}">
{#if $ae_loc.theme_mode === 'dark'} {#if $ae_loc.theme_mode === 'dark'}
<Moon class="m-1 inline-block" size="1em" /> <Moon class="m-1 inline-block" size="1em" />
<span class="hidden group-hover:inline-block">Dark Mode</span> <span class="hidden group-hover:inline-block">Dark Mode</span>

View File

@@ -1,170 +1,171 @@
<script lang="ts"> <script lang="ts">
/** /**
* menu_location_list.svelte — Aether Launcher: Room / Location Selector * menu_location_list.svelte — Aether Launcher: Room / Location Selector
* *
* PURPOSE: * PURPOSE:
* Provides a dropdown for operators to switch between venue rooms (event_location * Provides a dropdown for operators to switch between venue rooms (event_location
* objects). Selecting a room triggers a session list reload for that room and * objects). Selecting a room triggers a session list reload for that room and
* navigates the URL to /launcher/{location_id} so the back button works correctly. * navigates the URL to /launcher/{location_id} so the back button works correctly.
* *
* VISIBILITY: * VISIBILITY:
* Rendered only when $ae_loc.edit_mode is true (operator-level access). Regular * Rendered only when $ae_loc.edit_mode is true (operator-level access). Regular
* attendees and kiosk displays do not see the room selector — they are taken * attendees and kiosk displays do not see the room selector — they are taken
* directly to a URL-encoded room and session without UI controls. * directly to a URL-encoded room and session without UI controls.
* *
* DATA FLOW: * DATA FLOW:
* lq__event_location_obj_li (Dexie liveQuery, passed from launcher_menu.svelte) * lq__event_location_obj_li (Dexie liveQuery, passed from launcher_menu.svelte)
* → rendered as <select> options * → rendered as <select> options
* → onchange writes to $events_slct.event_location_id and $events_loc.launcher.slct * → onchange writes to $events_slct.event_location_id and $events_loc.launcher.slct
* → calls handle_load_ae_obj_li__event_session() to fetch sessions for the new room * → calls handle_load_ae_obj_li__event_session() to fetch sessions for the new room
* → navigates to /launcher/{location_id} via goto() * → navigates to /launcher/{location_id} via goto()
* *
* NOTE — slct_event_location_id binding: * NOTE — slct_event_location_id binding:
* Declared $bindable() so the parent can pass the initial selected value via bind:. * Declared $bindable() so the parent can pass the initial selected value via bind:.
* The onchange handler also writes directly to the canonical stores * The onchange handler also writes directly to the canonical stores
* ($events_slct.event_location_id) to keep other parts of the launcher in sync. * ($events_slct.event_location_id) to keep other parts of the launcher in sync.
*/ */
interface Props { interface Props {
loading__session_li_status?: null | boolean | string; loading__session_li_status?: null | boolean | string;
lq__event_location_obj_li: any; lq__event_location_obj_li: any;
slct_event_location_id?: string | null; slct_event_location_id?: string | null;
trigger_reload__event_session_obj_li?: boolean; trigger_reload__event_session_obj_li?: boolean;
trigger_reload__event_location_obj_li?: boolean; trigger_reload__event_location_obj_li?: boolean;
log_lvl?: number; log_lvl?: number;
}
let {
loading__session_li_status = $bindable(null),
lq__event_location_obj_li,
slct_event_location_id = $bindable(null),
trigger_reload__event_session_obj_li = $bindable(false),
trigger_reload__event_location_obj_li = $bindable(false),
log_lvl = $bindable(0)
}: Props = $props();
// *** Import Svelte specific
import { page } from '$app/state';
import { goto } from '$app/navigation';
// import { liveQuery } from "dexie";
// import { tick } from 'svelte';
// *** Import other supporting libraries
// *** Import Aether specific variables and functions
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
// import { db_events } from "$lib/db_events";
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import { Check, LoaderCircle } from '@lucide/svelte';
// export let slct_event_session_id: any;
// *** Functions and Logic
let ae_promises: key_val = $state({
slct_event_location_id: null
});
// let hover_timer_wait = 1000;
// let hover_timer: any = $state(null);
function handle_load_ae_obj_li__event_session(event_location_id: string) {
if (log_lvl) {
console.log(
`handle_load_ae_obj_li__event_session: event_location_id = ${event_location_id}`
);
}
if (!event_location_id) {
console.warn(
`handle_load_ae_obj_li__event_session: No event_location_id provided.`
);
return;
} }
let { loading__session_li_status = true;
loading__session_li_status = $bindable(null),
lq__event_location_obj_li,
slct_event_location_id = $bindable(null),
trigger_reload__event_session_obj_li = $bindable(false), ae_promises[event_location_id] = events_func
trigger_reload__event_location_obj_li = $bindable(false), .load_ae_obj_li__event_session({
api_cfg: $ae_api,
for_obj_type: 'event_location',
for_obj_id: event_location_id,
inc_file_li: true, // Only include files directly under the session?
inc_all_file_li: false, // Also include files under presentations and presenters as well?
inc_presentation_li: true,
inc_presenter_li: true,
enabled: $events_loc.launcher.show_content__enabled_sessions
? 'all'
: 'enabled',
hidden: $events_loc.launcher.show_content__hidden_sessions
? 'all'
: 'not_hidden',
limit: 49,
try_cache: true,
log_lvl: 1
})
.then(async function (load_results) {
console.log(`load_results = `, load_results);
log_lvl = $bindable(0) let event_session_id_li = [];
}: Props = $props();
// *** Import Svelte specific let tmp_li = []; // This is to prevent the array from constantly updating and triggering the liveQuery.
import { page } from '$app/state';
import { goto } from '$app/navigation';
// import { liveQuery } from "dexie";
// import { tick } from 'svelte';
// *** Import other supporting libraries for (let i = 0; i < load_results.length; i++) {
let event_session_obj = load_results[i];
let event_session_id = event_session_obj.event_session_id;
tmp_li.push(event_session_id);
}
event_session_id_li = tmp_li;
console.log(`event_session_id_li:`, event_session_id_li);
// $events_slct.id_li__event_session = event_session_id_li;
// *** Import Aether specific variables and functions loading__session_li_status = false;
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
// import { db_events } from "$lib/db_events";
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import { Check, LoaderCircle } from '@lucide/svelte';
// export let slct_event_session_id: any;
// *** Functions and Logic return load_results;
});
let ae_promises: key_val = $state({ return ae_promises[event_location_id];
slct_event_location_id: null }
});
// let hover_timer_wait = 1000;
// let hover_timer: any = $state(null);
function handle_load_ae_obj_li__event_session(event_location_id: string) {
if (log_lvl) {
console.log(
`handle_load_ae_obj_li__event_session: event_location_id = ${event_location_id}`
);
}
if (!event_location_id) {
console.warn(
`handle_load_ae_obj_li__event_session: No event_location_id provided.`
);
return;
}
loading__session_li_status = true;
ae_promises[event_location_id] = events_func
.load_ae_obj_li__event_session({
api_cfg: $ae_api,
for_obj_type: 'event_location',
for_obj_id: event_location_id,
inc_file_li: true, // Only include files directly under the session?
inc_all_file_li: false, // Also include files under presentations and presenters as well?
inc_presentation_li: true,
inc_presenter_li: true,
enabled: $events_loc.launcher.show_content__enabled_sessions
? 'all'
: 'enabled',
hidden: $events_loc.launcher.show_content__hidden_sessions
? 'all'
: 'not_hidden',
limit: 49,
try_cache: true,
log_lvl: 1
})
.then(async function (load_results) {
console.log(`load_results = `, load_results);
let event_session_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_session_obj = load_results[i];
let event_session_id = event_session_obj.event_session_id;
tmp_li.push(event_session_id);
}
event_session_id_li = tmp_li;
console.log(`event_session_id_li:`, event_session_id_li);
// $events_slct.id_li__event_session = event_session_id_li;
loading__session_li_status = false;
return load_results;
});
return ae_promises[event_location_id];
}
</script> </script>
<!-- text-neutral-800/80 --> <!-- text-neutral-800/80 -->
<div <div
class=" class="
w-full max-w-full flex w-full
flex flex-col md:flex-row flex-wrap gap-1 items-center justify-center max-w-full flex-col flex-wrap items-center justify-center gap-1 md:flex-row
" ">
>
{#if $lq__event_location_obj_li && $lq__event_location_obj_li.length > 0} {#if $lq__event_location_obj_li && $lq__event_location_obj_li.length > 0}
<div class="text-xs text-surface-600-400"> <div class="text-surface-600-400 text-xs">
<strong> <strong>
Location: Location:
<span <span
class:hidden={!$ae_loc.trusted_access || !$ae_loc.edit_mode} class:hidden={!$ae_loc.trusted_access ||
> !$ae_loc.edit_mode}>
({$lq__event_location_obj_li?.length}&times;) ({$lq__event_location_obj_li?.length}&times;)
</span> </span>
<!-- This should fade out once the data is loaded. --> <!-- This should fade out once the data is loaded. -->
{#await ae_promises[slct_event_location_id ?? '']} {#await ae_promises[slct_event_location_id ?? '']}
<LoaderCircle size="0.85em" class="inline animate-spin text-blue-500" /> <LoaderCircle
size="0.85em"
class="inline animate-spin text-blue-500" />
{:then result} {:then result}
<Check size="0.85em" class="inline text-green-500/80" /> <Check size="0.85em" class="inline text-green-500/80" />
{/await} {/await}
@@ -172,7 +173,7 @@
</div> </div>
<select <select
class="select text-xs p-1 max-w-42" class="select max-w-42 p-1 text-xs"
bind:value={slct_event_location_id} bind:value={slct_event_location_id}
onchange={async () => { onchange={async () => {
// console.log(`slct_event_location_id:`, slct_event_location_id); // console.log(`slct_event_location_id:`, slct_event_location_id);
@@ -222,9 +223,8 @@
loading__session_li_status = null; loading__session_li_status = null;
// goto(new_url, {replaceState: true}); // Updates the URL without reloading the page // goto(new_url, {replaceState: true}); // Updates the URL without reloading the page
goto(new_url, { replaceState: false }); // Updates the URL history without reloading the page goto(new_url, { replaceState: false }); // Updates the URL history without reloading the page
}} }}>
> <option value="" class="text-surface-800-200 italic">
<option value="" class="italic text-surface-800-200">
-- select -- -- select --
</option> </option>
{#each $lq__event_location_obj_li as event_location_obj (event_location_obj.event_location_id)} {#each $lq__event_location_obj_li as event_location_obj (event_location_obj.event_location_id)}

View File

@@ -1,224 +1,236 @@
<script lang="ts"> <script lang="ts">
/** /**
* menu_session_list.svelte — Aether Launcher: Session Selector * menu_session_list.svelte — Aether Launcher: Session Selector
* *
* PURPOSE: * PURPOSE:
* This is the primary navigation control for conference operators using * This is the primary navigation control for conference operators using
* the Aether Events Launcher. It lists all sessions in the selected room * the Aether Events Launcher. It lists all sessions in the selected room
* (event_location) and lets the operator switch the room's active session. * (event_location) and lets the operator switch the room's active session.
* *
* ENVIRONMENT: * ENVIRONMENT:
* The Launcher runs on operator laptops, dedicated podium/kiosk tablets, * The Launcher runs on operator laptops, dedicated podium/kiosk tablets,
* projector-connected desktops, and occasionally phones in breakout rooms. * projector-connected desktops, and occasionally phones in breakout rooms.
* Users range from tech-savvy AV staff to volunteers with limited computer * Users range from tech-savvy AV staff to volunteers with limited computer
* experience. Some users have motor impairments or shaky hands (e.g. older * experience. Some users have motor impairments or shaky hands (e.g. older
* members common at IDAA and similar events). * members common at IDAA and similar events).
* *
* KEY DESIGN CONSTRAINTS: * KEY DESIGN CONSTRAINTS:
* - Must show 020 sessions without scrolling (compact fixed-height rows) * - Must show 020 sessions without scrolling (compact fixed-height rows)
* - Session names can be extremely long (~300 chars) — must truncate at * - Session names can be extremely long (~300 chars) — must truncate at
* rest but reveal fully on hover without pushing other rows around * rest but reveal fully on hover without pushing other rows around
* - Hover-to-switch fires after a delay timer (not instantly) to prevent * - Hover-to-switch fires after a delay timer (not instantly) to prevent
* accidental session changes from casual cursor movement * accidental session changes from casual cursor movement
* - Strongly prefer click-to-confirm over hair-trigger hover activation * - Strongly prefer click-to-confirm over hair-trigger hover activation
* - Works in light and dark mode; projector-safe high-contrast overlay * - Works in light and dark mode; projector-safe high-contrast overlay
* *
* DATA FLOW: * DATA FLOW:
* lq__event_session_obj_li (Dexie liveQuery, passed from launcher/+layout.svelte) * lq__event_session_obj_li (Dexie liveQuery, passed from launcher/+layout.svelte)
* → rendered here as buttons * → rendered here as buttons
* → click / hover-timer sets trigger_reload__event_session_obj_id * → click / hover-timer sets trigger_reload__event_session_obj_id
* → $effect fires load + URL navigation + optional WS remote-control push * → $effect fires load + URL navigation + optional WS remote-control push
* *
* SESSION VISIBILITY (operator toggle — show_content__hidden_sessions): * SESSION VISIBILITY (operator toggle — show_content__hidden_sessions):
* Normal sessions: always visible * Normal sessions: always visible
* hide_event_launcher = true: hidden from list by default (launcher-specific * hide_event_launcher = true: hidden from list by default (launcher-specific
* suppression, e.g. overflow/backup sessions) * suppression, e.g. overflow/backup sessions)
* hide = true: globally hidden across all views (draft/cancelled) * hide = true: globally hidden across all views (draft/cancelled)
* *
* Both hidden states are fetched into Dexie with hidden:'all' by the background * Both hidden states are fetched into Dexie with hidden:'all' by the background
* sync so the operator can reveal them via the "All Sessions" menu toggle. * sync so the operator can reveal them via the "All Sessions" menu toggle.
* When revealed they appear dimmed (opacity-40) with an eye-slash indicator. * When revealed they appear dimmed (opacity-40) with an eye-slash indicator.
*/ */
interface Props { interface Props {
slct__event_session_id?: null | boolean | string; slct__event_session_id?: null | boolean | string;
loading__session_id_status?: null | boolean | string; loading__session_id_status?: null | boolean | string;
// export let lq__event_session_obj: any; // export let lq__event_session_obj: any;
lq__event_session_obj_li: any; lq__event_session_obj_li: any;
trigger_reload__event_session_obj_id?: boolean | null | string; trigger_reload__event_session_obj_id?: boolean | null | string;
// trigger_reload__event_session_obj_li?: boolean; // trigger_reload__event_session_obj_li?: boolean;
log_lvl?: number; log_lvl?: number;
} }
let { let {
slct__event_session_id = $bindable(null), slct__event_session_id = $bindable(null),
loading__session_id_status = $bindable(null), loading__session_id_status = $bindable(null),
lq__event_session_obj_li, lq__event_session_obj_li,
trigger_reload__event_session_obj_id = $bindable(false), trigger_reload__event_session_obj_id = $bindable(false),
// trigger_reload__event_session_obj_li = $bindable(false), // trigger_reload__event_session_obj_li = $bindable(false),
log_lvl = $bindable(1) log_lvl = $bindable(1)
}: Props = $props(); }: Props = $props();
// *** Import Svelte specific // *** Import Svelte specific
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { page } from '$app/state'; import { page } from '$app/state';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
// import { liveQuery } from "dexie"; // import { liveQuery } from "dexie";
// *** Import other supporting libraries // *** Import other supporting libraries
// *** Import Aether specific variables and functions // *** Import Aether specific variables and functions
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
// import { db_events } from "$lib/db_events"; // import { db_events } from "$lib/db_events";
import { import {
ae_snip, ae_snip,
ae_loc, ae_loc,
ae_sess, ae_sess,
ae_api, ae_api,
ae_trig, ae_trig,
slct, slct,
slct_trigger slct_trigger
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
import { import {
events_loc, events_loc,
events_sess, events_sess,
events_slct, events_slct,
events_trigger events_trigger
} from '$lib/stores/ae_events_stores'; } from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions'; import { events_func } from '$lib/ae_events/ae_events_functions';
import { CalendarCheck, CalendarDays, Check, EyeOff, Image, LoaderCircle } from '@lucide/svelte'; import {
// export let slct__event_session_id: any; CalendarCheck,
CalendarDays,
Check,
EyeOff,
Image,
LoaderCircle
} from '@lucide/svelte';
// export let slct__event_session_id: any;
// *** Functions and Logic // *** Functions and Logic
let ae_promises: key_val = $state({ let ae_promises: key_val = $state({
slct__event_session_id: null, slct__event_session_id: null,
slct__event_presentation_li: null slct__event_presentation_li: null
}); });
// WHY 1200ms: Aether Launcher is used at conferences by operators of all ages and // WHY 1200ms: Aether Launcher is used at conferences by operators of all ages and
// motor abilities — shaky hands, imprecise trackpads, and fat-finger tablet taps are // motor abilities — shaky hands, imprecise trackpads, and fat-finger tablet taps are
// routine. 750ms (the previous value) triggered accidental session changes when the // routine. 750ms (the previous value) triggered accidental session changes when the
// cursor drifted across the list. 1200ms means the operator must deliberately hold // cursor drifted across the list. 1200ms means the operator must deliberately hold
// focus on a row for over a second before it fires — still fast for intentional use. // focus on a row for over a second before it fires — still fast for intentional use.
// NOTE: hover-timer only triggers a data PRE-LOAD (preview). The session does not // NOTE: hover-timer only triggers a data PRE-LOAD (preview). The session does not
// actually switch until the operator clicks. See onclick handler below. // actually switch until the operator clicks. See onclick handler below.
let hover_timer_wait = 1200; let hover_timer_wait = 1200;
let hover_timer: any = $state(null); let hover_timer: any = $state(null);
// Navigation Shield Pattern (Refactored 2026-02-11) // Navigation Shield Pattern (Refactored 2026-02-11)
// WHY: We use untrack for store updates to prevent circular reactivity loops // WHY: We use untrack for store updates to prevent circular reactivity loops
// with the layout's sync effect. Standardizing on page.url ensures // with the layout's sync effect. Standardizing on page.url ensures
// the back button and browser history work correctly. // the back button and browser history work correctly.
$effect(() => { $effect(() => {
if (trigger_reload__event_session_obj_id) { if (trigger_reload__event_session_obj_id) {
const start = performance.now();
const event_session_id = String(trigger_reload__event_session_obj_id);
if (log_lvl) {
console.log(`[UI Trace] trigger_reload changed to: ${event_session_id}`);
}
untrack(() => {
// 1. Reset trigger immediately
trigger_reload__event_session_obj_id = false;
// 2. Local State Updates (Sync store for instant LiveQuery reaction)
if (slct__event_session_id !== event_session_id) {
slct__event_session_id = event_session_id;
}
// 3. Background Data Fetch
handle_load_ae_obj_id__event_session(event_session_id);
// 4. Remote Control Sync
if ($events_loc.launcher.controller == 'local_push') {
$events_sess.launcher.controller_cmd = `ae_load:event_session=${event_session_id}`;
$events_sess.launcher.controller_trigger_send = true;
}
// 5. URL Navigation
let new_url_obj = new URL(page.url);
new_url_obj.searchParams.set('session_id', event_session_id);
if (log_lvl) console.log(`[UI Trace] Initiating SvelteKit goto...`);
goto(new_url_obj.toString(), {
replaceState: false,
noScroll: true,
keepFocus: true
}).then(() => {
if (log_lvl)
console.log(`🏁 [Trace] Navigation Roundtrip: ${(performance.now() - start).toFixed(2)}ms`);
});
});
}
});
function handle_load_ae_obj_id__event_session(event_session_id: any) {
const start = performance.now(); const start = performance.now();
const event_session_id = String(trigger_reload__event_session_obj_id);
if (log_lvl) { if (log_lvl) {
console.log( console.log(
`[UI Trace] handle_load_ae_obj_id__event_session: Calling library for id=${event_session_id}` `[UI Trace] trigger_reload changed to: ${event_session_id}`
); );
} }
loading__session_id_status = 'loading'; untrack(() => {
// 1. Reset trigger immediately
trigger_reload__event_session_obj_id = false;
ae_promises.slct__event_session_id = events_func // 2. Local State Updates (Sync store for instant LiveQuery reaction)
.load_ae_obj_id__event_session({ if (slct__event_session_id !== event_session_id) {
api_cfg: $ae_api, slct__event_session_id = event_session_id;
event_session_id: event_session_id, }
inc_file_li: true,
inc_all_file_li: true, // 3. Background Data Fetch
inc_presentation_li: true, handle_load_ae_obj_id__event_session(event_session_id);
inc_presenter_li: true,
log_lvl: log_lvl // 4. Remote Control Sync
}) if ($events_loc.launcher.controller == 'local_push') {
.then(async (load_results) => { $events_sess.launcher.controller_cmd = `ae_load:event_session=${event_session_id}`;
$events_sess.launcher.controller_trigger_send = true;
}
// 5. URL Navigation
let new_url_obj = new URL(page.url);
new_url_obj.searchParams.set('session_id', event_session_id);
if (log_lvl) console.log(`[UI Trace] Initiating SvelteKit goto...`);
goto(new_url_obj.toString(), {
replaceState: false,
noScroll: true,
keepFocus: true
}).then(() => {
if (log_lvl) if (log_lvl)
console.log( console.log(
`[UI Trace] handle_load_ae_obj_id: Library returned results in ${(performance.now() - start).toFixed(2)}ms.` `🏁 [Trace] Navigation Roundtrip: ${(performance.now() - start).toFixed(2)}ms`
); );
if (load_results) {
$events_slct.event_session_obj = load_results;
$events_slct.event_file_obj_li =
load_results.event_file_li ?? [];
$events_slct.event_presentation_obj_li =
load_results.event_presentation_li ?? [];
}
})
.finally(() => {
loading__session_id_status = false;
}); });
});
} }
});
function handle_load_ae_obj_id__event_session(event_session_id: any) {
const start = performance.now();
if (log_lvl) {
console.log(
`[UI Trace] handle_load_ae_obj_id__event_session: Calling library for id=${event_session_id}`
);
}
loading__session_id_status = 'loading';
ae_promises.slct__event_session_id = events_func
.load_ae_obj_id__event_session({
api_cfg: $ae_api,
event_session_id: event_session_id,
inc_file_li: true,
inc_all_file_li: true,
inc_presentation_li: true,
inc_presenter_li: true,
log_lvl: log_lvl
})
.then(async (load_results) => {
if (log_lvl)
console.log(
`[UI Trace] handle_load_ae_obj_id: Library returned results in ${(performance.now() - start).toFixed(2)}ms.`
);
if (load_results) {
$events_slct.event_session_obj = load_results;
$events_slct.event_file_obj_li =
load_results.event_file_li ?? [];
$events_slct.event_presentation_obj_li =
load_results.event_presentation_li ?? [];
}
})
.finally(() => {
loading__session_id_status = false;
});
}
</script> </script>
<div <div
class=" class="
w-full max-w-80 flex w-full
flex flex-col flex-wrap gap-1 items-center justify-start md:justify-center max-w-80 flex-col flex-wrap items-center justify-start gap-1 md:justify-center
" ">
>
{#if $lq__event_session_obj_li && $lq__event_session_obj_li.length > 0} {#if $lq__event_session_obj_li && $lq__event_session_obj_li.length > 0}
<div class="text-xs text-surface-600-400"> <div class="text-surface-600-400 text-xs">
<strong> <strong>
Sessions: Sessions:
<span <span
class:hidden={!$ae_loc.trusted_access || !$ae_loc.edit_mode} class:hidden={!$ae_loc.trusted_access ||
> !$ae_loc.edit_mode}>
({$lq__event_session_obj_li?.length}&times;) ({$lq__event_session_obj_li?.length}&times;)
</span> </span>
<!-- This should fade out once the data is loaded. --> <!-- This should fade out once the data is loaded. -->
{#await ae_promises.slct__event_session_id} {#await ae_promises.slct__event_session_id}
<LoaderCircle size="0.85em" class="inline animate-spin text-blue-500" /> <LoaderCircle
size="0.85em"
class="inline animate-spin text-blue-500" />
{:then result} {:then result}
<Check size="0.85em" class="inline text-green-500/80" /> <Check size="0.85em" class="inline text-green-500/80" />
{/await} {/await}
@@ -227,22 +239,20 @@
<ul <ul
class=" class="
m-0 flex
w-full max-w-full w-full max-w-full
p-0 m-0 flex-col items-start justify-start gap-0 p-0
flex flex-col gap-0 items-start justify-start ">
"
>
{#each $lq__event_session_obj_li as event_session_obj (event_session_obj.event_session_id)} {#each $lq__event_session_obj_li as event_session_obj (event_session_obj.event_session_id)}
<li <li
class=" class="
session-item session-item
relative relative
p-0 m-0 m-0 w-full
w-full max-w-full max-w-full p-0
" "
class:session-active={slct__event_session_id === class:session-active={slct__event_session_id ===
event_session_obj?.id} event_session_obj?.id}>
>
<button <button
type="button" type="button"
onmouseenter={() => { onmouseenter={() => {
@@ -272,17 +282,17 @@
class=" class="
session-btn session-btn
btn btn-sm btn btn-sm
focus-visible:ring-2 focus-visible:ring-primary-400 focus-visible:ring-offset-1 focus-visible:ring-primary-400 m-0 flex
text-sm w-full
w-full max-w-full max-w-full flex-row
text-left items-center
m-0 justify-start
px-1.5 py-1 rounded-md px-1.5
rounded-md py-1
flex flex-row items-center justify-start text-left text-sm transition-colors duration-200
transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-offset-1
" "
class:preset-filled-primary={slct__event_session_id === class:preset-filled-primary={slct__event_session_id ===
event_session_obj?.id} event_session_obj?.id}
@@ -298,8 +308,7 @@
event_session_obj?.hide_event_launcher)} event_session_obj?.hide_event_launcher)}
class:opacity-40={event_session_obj?.hide || class:opacity-40={event_session_obj?.hide ||
event_session_obj?.hide_event_launcher} event_session_obj?.hide_event_launcher}
title={`Session: ${event_session_obj?.name}\nID: ${event_session_obj?.id} | ${ae_util.iso_datetime_formatter(event_session_obj?.start_datetime, $events_loc.launcher.time_format)}`} title={`Session: ${event_session_obj?.name}\nID: ${event_session_obj?.id} | ${ae_util.iso_datetime_formatter(event_session_obj?.start_datetime, $events_loc.launcher.time_format)}`}>
>
<!-- Session row layout: [date column | session name] <!-- Session row layout: [date column | session name]
Date column is fixed-width (shrink-0) so name column always Date column is fixed-width (shrink-0) so name column always
gets consistent space regardless of date string length. gets consistent space regardless of date string length.
@@ -311,8 +320,7 @@
When revealed, dimmed (opacity-40) with eye-slash icon. --> When revealed, dimmed (opacity-40) with eye-slash icon. -->
<span <span
class="border-r border-surface-400-600 pr-1 min-w-20 shrink-0" class="border-surface-400-600 min-w-20 shrink-0 border-r pr-1">
>
{#if slct__event_session_id === event_session_obj?.id} {#if slct__event_session_id === event_session_obj?.id}
<CalendarCheck size="0.85em" class="inline" /> <CalendarCheck size="0.85em" class="inline" />
{:else} {:else}
@@ -321,8 +329,7 @@
<span <span
class="text-xs" class="text-xs"
class:hidden={slct__event_session_id === class:hidden={slct__event_session_id ===
event_session_obj?.id} event_session_obj?.id}>
>
{ae_util.iso_datetime_formatter( {ae_util.iso_datetime_formatter(
event_session_obj?.start_datetime, event_session_obj?.start_datetime,
'week_medium' 'week_medium'
@@ -339,20 +346,28 @@
<span <span
class=" class="
session-name session-name
grow text-sm min-w-0 grow
min-w-0 text-sm
" ">
>
{#if event_session_obj?.type_code == 'poster'} {#if event_session_obj?.type_code == 'poster'}
<span title="Digital Poster Session"><Image size="0.85em" class="inline mr-1 text-primary-500" /></span> <span title="Digital Poster Session"
><Image
size="0.85em"
class="text-primary-500 mr-1 inline" /></span>
{/if} {/if}
<!-- Distinct icon styles distinguish the two hidden states: <!-- Distinct icon styles distinguish the two hidden states:
amber = hide (globally hidden — draft, cancelled, or admin-only) amber = hide (globally hidden — draft, cancelled, or admin-only)
muted = hide_event_launcher (suppressed in Launcher view only) --> muted = hide_event_launcher (suppressed in Launcher view only) -->
{#if event_session_obj?.hide} {#if event_session_obj?.hide}
<span title="Hidden session"><EyeOff size="0.85em" class="inline mr-1 text-warning-600" /></span> <span title="Hidden session"
><EyeOff
size="0.85em"
class="text-warning-600 mr-1 inline" /></span>
{:else if event_session_obj?.hide_event_launcher} {:else if event_session_obj?.hide_event_launcher}
<span title="Hidden from Launcher"><EyeOff size="0.85em" class="inline mr-1 opacity-60" /></span> <span title="Hidden from Launcher"
><EyeOff
size="0.85em"
class="mr-1 inline opacity-60" /></span>
{/if} {/if}
{event_session_obj?.name} {event_session_obj?.name}
</span> </span>
@@ -366,7 +381,7 @@
</div> </div>
<style> <style>
/* /*
* ═══════════════════════════════════════════════════════════════════ * ═══════════════════════════════════════════════════════════════════
* Aether Launcher — Compact Session List Styles * Aether Launcher — Compact Session List Styles
* One Sky IT — specialized for conference operator use * One Sky IT — specialized for conference operator use
@@ -413,76 +428,80 @@
* can instantly see which row is being previewed. * can instantly see which row is being previewed.
*/ */
/* ── Inactive row: fixed compact height ── */ /* ── Inactive row: fixed compact height ── */
.session-item { .session-item {
height: 2rem; height: 2rem;
} }
/* ── Active row: always fully visible in flow ── */ /* ── Active row: always fully visible in flow ── */
.session-item.session-active { .session-item.session-active {
height: auto; height: auto;
min-height: 2rem; min-height: 2rem;
} }
/* Clip inactive button content to row height */ /* Clip inactive button content to row height */
.session-btn { .session-btn {
overflow: hidden; overflow: hidden;
} }
/* Active button: never clip — operator can always read it */ /* Active button: never clip — operator can always read it */
.session-item.session-active .session-btn { .session-item.session-active .session-btn {
overflow: visible; overflow: visible;
} }
/* /*
* Inactive hover/focus: pop button out as an overlay panel. * Inactive hover/focus: pop button out as an overlay panel.
* - position:absolute keeps the <li> placeholder at 2rem (no layout shift) * - position:absolute keeps the <li> placeholder at 2rem (no layout shift)
* - Solid opaque background prevents tonal transparency from showing through * - Solid opaque background prevents tonal transparency from showing through
* - Left border accent gives a clear "you are here" cue * - Left border accent gives a clear "you are here" cue
*/ */
.session-item:not(.session-active):hover .session-btn, .session-item:not(.session-active):hover .session-btn,
.session-item:not(.session-active):focus-within .session-btn { .session-item:not(.session-active):focus-within .session-btn {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: auto; height: auto;
z-index: 20; z-index: 20;
overflow: visible; overflow: visible;
opacity: 1; opacity: 1;
background-color: #f1f5f9; /* slate-100 — solid light surface */ background-color: #f1f5f9; /* slate-100 — solid light surface */
border-left: 3px solid rgb(var(--color-primary-500, 99 102 241)); border-left: 3px solid rgb(var(--color-primary-500, 99 102 241));
border-radius: 0.375rem; border-radius: 0.375rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15), 0 1px 4px rgba(0, 0, 0, 0.08); box-shadow:
} 0 4px 20px rgba(0, 0, 0, 0.15),
0 1px 4px rgba(0, 0, 0, 0.08);
}
/* Dark mode overlay — solid dark surface, light readable text */ /* Dark mode overlay — solid dark surface, light readable text */
:global(.dark) .session-item:not(.session-active):hover .session-btn, :global(.dark) .session-item:not(.session-active):hover .session-btn,
:global(.dark) .session-item:not(.session-active):focus-within .session-btn { :global(.dark) .session-item:not(.session-active):focus-within .session-btn {
background-color: #1e293b; /* slate-800 */ background-color: #1e293b; /* slate-800 */
color: #f1f5f9; /* slate-100 */ color: #f1f5f9; /* slate-100 */
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.6), 0 1px 4px rgba(0, 0, 0, 0.4); box-shadow:
} 0 4px 24px rgba(0, 0, 0, 0.6),
0 1px 4px rgba(0, 0, 0, 0.4);
}
/* ── Session name: single-line truncated at rest ── */ /* ── Session name: single-line truncated at rest ── */
.session-name { .session-name {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/* Active session: name always wraps fully */ /* Active session: name always wraps fully */
.session-item.session-active .session-name { .session-item.session-active .session-name {
white-space: normal; white-space: normal;
overflow: visible; overflow: visible;
text-overflow: unset; text-overflow: unset;
} }
/* Overlay: name wraps fully (300-char titles readable) */ /* Overlay: name wraps fully (300-char titles readable) */
.session-item:not(.session-active):hover .session-name, .session-item:not(.session-active):hover .session-name,
.session-item:not(.session-active):focus-within .session-name { .session-item:not(.session-active):focus-within .session-name {
white-space: normal; white-space: normal;
overflow: visible; overflow: visible;
text-overflow: unset; text-overflow: unset;
} }
</style> </style>