style(launcher): accessibility, session list UX, and preset-* token fixes
Style token fixes: - launcher_cfg.svelte: tab buttons preset-filled-primary-500 -> preset-filled-primary; opacity-50 inactive -> preset-tonal-surface - launcher_cfg_app_modes.svelte: same fix for app mode buttons (opacity-40) - launcher_cfg_controller.svelte: variant-filled-success/error -> preset-filled-* - launcher_cfg_template.svelte: variant-filled-success -> preset-filled-success - launcher_session_view.svelte: add dark:border-gray-600/700 to bare border-gray-* Session list (menu_session_list.svelte) -- full accessibility + UX pass: - fix: background sync fetches hidden:all so All Sessions toggle works - fix: hide_event_launcher respected in class:hidden and class:opacity-40 - fix: overlay uses explicit opaque backgrounds (slate-100/slate-800) to prevent preset-tonal-secondary transparency bleed-through in light and dark mode - feat: compact fixed 2rem row height; position:absolute overlay on hover/focus reveals full session name (300-char) without any layout shift (no sibling movement) - feat: active session always fully visible in flow (height:auto, no clipping) - a11y: hover_timer_wait 750->1200ms (motor accessibility) - a11y: removed hover:scale which caused cursor drift and timer jitter - a11y: px-1.5 py-1 touch targets, focus-visible ring for keyboard/switch users - a11y: fa-eye-slash icons distinguish hide vs hide_event_launcher states - docs: comprehensive OSIT/Aether-specific comments throughout
This commit is contained in:
@@ -28,27 +28,27 @@
|
||||
type="button"
|
||||
onclick={() => ($events_loc.launcher.app_mode = 'default')}
|
||||
class="btn btn-xs text-[9px] font-bold"
|
||||
class:preset-filled-primary-500={$events_loc.launcher
|
||||
class:preset-filled-primary={$events_loc.launcher
|
||||
.app_mode === 'default'}
|
||||
class:opacity-40={$events_loc.launcher.app_mode !==
|
||||
class:preset-tonal-surface={$events_loc.launcher.app_mode !==
|
||||
'default'}>Web</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => ($events_loc.launcher.app_mode = 'native')}
|
||||
class="btn btn-xs text-[9px] font-bold"
|
||||
class:preset-filled-primary-500={$events_loc.launcher
|
||||
class:preset-filled-primary={$events_loc.launcher
|
||||
.app_mode === 'native'}
|
||||
class:opacity-40={$events_loc.launcher.app_mode !==
|
||||
class:preset-tonal-surface={$events_loc.launcher.app_mode !==
|
||||
'native'}>App</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => ($events_loc.launcher.app_mode = 'onsite')}
|
||||
class="btn btn-xs text-[9px] font-bold"
|
||||
class:preset-filled-primary-500={$events_loc.launcher
|
||||
class:preset-filled-primary={$events_loc.launcher
|
||||
.app_mode === 'onsite'}
|
||||
class:opacity-40={$events_loc.launcher.app_mode !==
|
||||
class:preset-tonal-surface={$events_loc.launcher.app_mode !==
|
||||
'onsite'}>Onsite</button
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -36,11 +36,11 @@
|
||||
</div>
|
||||
{#if ws_connected}
|
||||
<span
|
||||
class="badge variant-filled-success text-[8px] animate-pulse"
|
||||
class="badge preset-filled-success text-[8px] animate-pulse"
|
||||
>Connected</span
|
||||
>
|
||||
{:else}
|
||||
<span class="badge variant-filled-error text-[8px]"
|
||||
<span class="badge preset-filled-error text-[8px]"
|
||||
>Disconnected</span
|
||||
>
|
||||
{/if}
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-[10px] font-medium">Engine Health</span>
|
||||
<span class="badge variant-filled-success text-[8px] uppercase"
|
||||
<span class="badge preset-filled-success text-[8px] uppercase"
|
||||
>Stable</span
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
for_obj_type: 'event_location',
|
||||
for_obj_id: location_id,
|
||||
view: 'alt',
|
||||
hidden: 'all', // Launcher is operator-only; fetch all sessions so the "All Sessions" toggle works
|
||||
try_cache: true,
|
||||
log_lvl: 0
|
||||
});
|
||||
|
||||
@@ -89,8 +89,8 @@
|
||||
type="button"
|
||||
onclick={() => (active_tab = 'system')}
|
||||
class="btn btn-sm text-[10px] uppercase font-bold transition-all"
|
||||
class:preset-filled-primary-500={active_tab === 'system'}
|
||||
class:opacity-50={active_tab !== 'system'}
|
||||
class:preset-filled-primary={active_tab === 'system'}
|
||||
class:preset-tonal-surface={active_tab !== 'system'}
|
||||
>
|
||||
<span class="fas fa-microchip mr-1"></span> System
|
||||
</button>
|
||||
@@ -98,8 +98,8 @@
|
||||
type="button"
|
||||
onclick={() => (active_tab = 'sync')}
|
||||
class="btn btn-sm text-[10px] uppercase font-bold transition-all"
|
||||
class:preset-filled-primary-500={active_tab === 'sync'}
|
||||
class:opacity-50={active_tab !== 'sync'}
|
||||
class:preset-filled-primary={active_tab === 'sync'}
|
||||
class:preset-tonal-surface={active_tab !== 'sync'}
|
||||
>
|
||||
<span class="fas fa-sync mr-1"></span> Sync
|
||||
</button>
|
||||
@@ -107,8 +107,8 @@
|
||||
type="button"
|
||||
onclick={() => (active_tab = 'general')}
|
||||
class="btn btn-sm text-[10px] uppercase font-bold transition-all"
|
||||
class:preset-filled-primary-500={active_tab === 'general'}
|
||||
class:opacity-50={active_tab !== 'general'}
|
||||
class:preset-filled-primary={active_tab === 'general'}
|
||||
class:preset-tonal-surface={active_tab !== 'general'}
|
||||
>
|
||||
<span class="fas fa-sliders-h mr-1"></span> General
|
||||
</button>
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
|
||||
{#if $lq__event_session_obj && $lq__event_session_obj.event_session_id}
|
||||
<header
|
||||
class="event_session_about text-center border-b-2 border-gray-400 flex flex-row flex-wrap gap-2 items-center justify-between"
|
||||
class="event_session_about text-center border-b-2 border-gray-400 dark:border-gray-600 flex flex-row flex-wrap gap-2 items-center justify-between"
|
||||
>
|
||||
<h3
|
||||
class:hidden={!$lq__event_session_obj?.start_datetime ||
|
||||
@@ -359,7 +359,7 @@
|
||||
<ul class="event_presentation_list max-w-full space-y-2">
|
||||
{#each $lq__event_presentation_obj_li as event_presentation_obj (event_presentation_obj.event_presentation_id)}
|
||||
<li
|
||||
class="border-b-2 border-gray-300 my-1 py-1 text-center md:text-left"
|
||||
class="border-b-2 border-gray-300 dark:border-gray-700 my-1 py-1 text-center md:text-left"
|
||||
>
|
||||
<!-- The presentation information -->
|
||||
<div
|
||||
|
||||
@@ -1,4 +1,45 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* menu_session_list.svelte — Aether Launcher: Session Selector
|
||||
*
|
||||
* PURPOSE:
|
||||
* This is the primary navigation control for conference operators using
|
||||
* the Aether Events Launcher. It lists all sessions in the selected room
|
||||
* (event_location) and lets the operator switch the room's active session.
|
||||
*
|
||||
* ENVIRONMENT:
|
||||
* The Launcher runs on operator laptops, dedicated podium/kiosk tablets,
|
||||
* projector-connected desktops, and occasionally phones in breakout rooms.
|
||||
* Users range from tech-savvy AV staff to volunteers with limited computer
|
||||
* experience. Some users have motor impairments or shaky hands (e.g. older
|
||||
* members common at IDAA and similar events).
|
||||
*
|
||||
* KEY DESIGN CONSTRAINTS:
|
||||
* - Must show 0–20 sessions without scrolling (compact fixed-height rows)
|
||||
* - Session names can be extremely long (~300 chars) — must truncate at
|
||||
* rest but reveal fully on hover without pushing other rows around
|
||||
* - Hover-to-switch fires after a delay timer (not instantly) to prevent
|
||||
* accidental session changes from casual cursor movement
|
||||
* - Strongly prefer click-to-confirm over hair-trigger hover activation
|
||||
* - Works in light and dark mode; projector-safe high-contrast overlay
|
||||
*
|
||||
* DATA FLOW:
|
||||
* lq__event_session_obj_li (Dexie liveQuery, passed from launcher/+layout.svelte)
|
||||
* → rendered here as buttons
|
||||
* → click / hover-timer sets trigger_reload__event_session_obj_id
|
||||
* → $effect fires load + URL navigation + optional WS remote-control push
|
||||
*
|
||||
* SESSION VISIBILITY (operator toggle — show_content__hidden_sessions):
|
||||
* Normal sessions: always visible
|
||||
* hide_event_launcher = true: hidden from list by default (launcher-specific
|
||||
* suppression, e.g. overflow/backup sessions)
|
||||
* hide = true: globally hidden across all views (draft/cancelled)
|
||||
*
|
||||
* 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.
|
||||
* When revealed they appear dimmed (opacity-40) with an eye-slash indicator.
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
slct__event_session_id?: null | boolean | string;
|
||||
loading__session_id_status?: null | boolean | string;
|
||||
@@ -60,7 +101,14 @@
|
||||
slct__event_presentation_li: null
|
||||
});
|
||||
|
||||
let hover_timer_wait = 750; // Optimized from 1250ms for better responsiveness
|
||||
// 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
|
||||
// routine. 750ms (the previous value) triggered accidental session changes when the
|
||||
// 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.
|
||||
// NOTE: hover-timer only triggers a data PRE-LOAD (preview). The session does not
|
||||
// actually switch until the operator clicks. See onclick handler below.
|
||||
let hover_timer_wait = 1200;
|
||||
let hover_timer: any = $state(null);
|
||||
|
||||
// Navigation Shield Pattern (Refactored 2026-02-11)
|
||||
@@ -71,7 +119,7 @@
|
||||
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}`);
|
||||
}
|
||||
@@ -179,22 +227,21 @@
|
||||
|
||||
<ul
|
||||
class="
|
||||
space-y-1
|
||||
w-full max-w-full
|
||||
p-0 m-0
|
||||
flex flex-col gap-0.5 items-start justify-start
|
||||
transition-all
|
||||
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)}
|
||||
<li
|
||||
class="
|
||||
session-item
|
||||
relative
|
||||
p-0 m-0
|
||||
w-full max-w-full
|
||||
z-0 hover:z-20
|
||||
hover:scale-120
|
||||
transition-all
|
||||
"
|
||||
class:session-active={slct__event_session_id ===
|
||||
event_session_obj?.id}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -223,21 +270,21 @@
|
||||
$events_slct.event_file_obj = null;
|
||||
}}
|
||||
class="
|
||||
btn btn-sm hover:preset-filled-primary-500
|
||||
session-btn
|
||||
btn btn-sm
|
||||
focus-visible:ring-2 focus-visible:ring-primary-400 focus-visible:ring-offset-1
|
||||
|
||||
text-sm
|
||||
w-full max-w-full
|
||||
hover:min-w-fit
|
||||
hover:max-w-2xl
|
||||
text-left
|
||||
m-0
|
||||
p-0
|
||||
px-1.5 py-1
|
||||
|
||||
rounded-md
|
||||
flex flex-row items-center justify-start
|
||||
transition-all
|
||||
transition-colors duration-200
|
||||
"
|
||||
class:preset-filled-primary-500={slct__event_session_id ===
|
||||
class:preset-filled-primary={slct__event_session_id ===
|
||||
event_session_obj?.id}
|
||||
class:preset-tonal-secondary={slct__event_session_id !=
|
||||
event_session_obj?.id}
|
||||
@@ -247,15 +294,24 @@
|
||||
event_session_obj?.id}
|
||||
class:hidden={!$events_loc.launcher
|
||||
.show_content__hidden_sessions &&
|
||||
event_session_obj?.hide}
|
||||
class:dim={event_session_obj?.hide}
|
||||
(event_session_obj?.hide ||
|
||||
event_session_obj?.hide_event_launcher)}
|
||||
class:opacity-40={event_session_obj?.hide ||
|
||||
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)}`}
|
||||
>
|
||||
<!-- hover:scale-115 -->
|
||||
<!-- hover:z-index-10 -->
|
||||
<!-- Session row layout: [date column | session name]
|
||||
Date column is fixed-width (shrink-0) so name column always
|
||||
gets consistent space regardless of date string length.
|
||||
|
||||
VISIBILITY (class:hidden / class:opacity-40 above):
|
||||
- hide_event_launcher: suppressed in Launcher only (operator toggle reveals)
|
||||
- hide: globally hidden across all Aether views (draft/cancelled)
|
||||
Both fetched into Dexie with hidden:'all' by launcher_background_sync.
|
||||
When revealed, dimmed (opacity-40) with eye-slash icon. -->
|
||||
|
||||
<span
|
||||
class="border-r border-surface-400-600 pr-1 min-w-28"
|
||||
class="border-r border-surface-400-600 pr-1 min-w-20 shrink-0"
|
||||
>
|
||||
{#if slct__event_session_id === event_session_obj?.id}
|
||||
<span class="fas fa-calendar-check"></span>
|
||||
@@ -282,15 +338,22 @@
|
||||
|
||||
<span
|
||||
class="
|
||||
grow text-xs
|
||||
min-w-32 hover:min-w-fit
|
||||
overflow-hidden hover:overflow-visible
|
||||
trasnition-all
|
||||
session-name
|
||||
grow text-sm
|
||||
min-w-0
|
||||
"
|
||||
>
|
||||
{#if event_session_obj?.type_code == 'poster'}
|
||||
<span class="fas fa-image mr-1 text-primary-500" title="Digital Poster Session"></span>
|
||||
{/if}
|
||||
<!-- Distinct icon styles distinguish the two hidden states:
|
||||
amber = hide (globally hidden — draft, cancelled, or admin-only)
|
||||
muted = hide_event_launcher (suppressed in Launcher view only) -->
|
||||
{#if event_session_obj?.hide}
|
||||
<span class="fas fa-eye-slash mr-1 text-warning-600" title="Hidden session"></span>
|
||||
{:else if event_session_obj?.hide_event_launcher}
|
||||
<span class="fas fa-eye-slash mr-1 opacity-60" title="Hidden from Launcher"></span>
|
||||
{/if}
|
||||
{event_session_obj?.name}
|
||||
</span>
|
||||
</button>
|
||||
@@ -301,3 +364,125 @@
|
||||
<div class="text-sm">No sessions found.</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════
|
||||
* Aether Launcher — Compact Session List Styles
|
||||
* One Sky IT — specialized for conference operator use
|
||||
* ═══════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* ARCHITECTURE OVERVIEW
|
||||
* ─────────────────────
|
||||
* Each <li class="session-item"> is a FIXED-HEIGHT placeholder (2rem).
|
||||
* Its child <button class="session-btn"> is clipped to that height at rest.
|
||||
*
|
||||
* On hover/focus-within, the button becomes position:absolute — it pops
|
||||
* OUT of the layout flow and floats as an overlay panel above sibling rows.
|
||||
* The <li> retains its original 2rem height permanently, so NO siblings
|
||||
* ever shift position. This is deliberate and critical:
|
||||
*
|
||||
* WHY NOT transform:scale?
|
||||
* scale() keeps the element in flow but enlarges its visual footprint.
|
||||
* If it grows large enough to show wrapped text, neighbors shift OR the
|
||||
* cursor drifts off the hitbox edge, firing mouseleave and cancelling the
|
||||
* hover timer — exactly the jitter problem for shaky-hand operators.
|
||||
*
|
||||
* WHY position:absolute instead?
|
||||
* The overlay grows in z-space only. The operator's cursor stays inside
|
||||
* the expanded button hitbox the entire time (the button IS the element
|
||||
* they are hovering — its hitbox grows with it). No jitter.
|
||||
*
|
||||
* ACTIVE SESSION
|
||||
* ──────────────
|
||||
* The currently-selected session (.session-active) reverts to height:auto
|
||||
* in normal flow so its full (potentially very long) name is always visible
|
||||
* without requiring any interaction. One expanded row in a 20-row list is
|
||||
* visually negligible and gives the operator instant confirmation of where
|
||||
* the room currently is.
|
||||
*
|
||||
* OVERLAY BACKGROUNDS
|
||||
* ────────────────────
|
||||
* preset-tonal-secondary is semi-transparent by design (~20% fill). Used as
|
||||
* the overlay background it lets underlying rows bleed through, making the
|
||||
* text hard to read — especially on projectors and in dark mode.
|
||||
* The overlay therefore uses explicit opaque colours:
|
||||
* Light: slate-100 (#f1f5f9) — matches standard card surface
|
||||
* Dark: slate-800 (#1e293b) + slate-100 text — high contrast, WCAG AA
|
||||
* A primary-accent left border provides a clear visual anchor so operators
|
||||
* can instantly see which row is being previewed.
|
||||
*/
|
||||
|
||||
/* ── Inactive row: fixed compact height ── */
|
||||
.session-item {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
/* ── Active row: always fully visible in flow ── */
|
||||
.session-item.session-active {
|
||||
height: auto;
|
||||
min-height: 2rem;
|
||||
}
|
||||
|
||||
/* Clip inactive button content to row height */
|
||||
.session-btn {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Active button: never clip — operator can always read it */
|
||||
.session-item.session-active .session-btn {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/*
|
||||
* Inactive hover/focus: pop button out as an overlay panel.
|
||||
* - position:absolute keeps the <li> placeholder at 2rem (no layout shift)
|
||||
* - Solid opaque background prevents tonal transparency from showing through
|
||||
* - Left border accent gives a clear "you are here" cue
|
||||
*/
|
||||
.session-item:not(.session-active):hover .session-btn,
|
||||
.session-item:not(.session-active):focus-within .session-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: auto;
|
||||
z-index: 20;
|
||||
overflow: visible;
|
||||
opacity: 1;
|
||||
background-color: #f1f5f9; /* slate-100 — solid light surface */
|
||||
border-left: 3px solid rgb(var(--color-primary-500, 99 102 241));
|
||||
border-radius: 0.375rem;
|
||||
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 */
|
||||
:global(.dark) .session-item:not(.session-active):hover .session-btn,
|
||||
:global(.dark) .session-item:not(.session-active):focus-within .session-btn {
|
||||
background-color: #1e293b; /* slate-800 */
|
||||
color: #f1f5f9; /* slate-100 */
|
||||
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 {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Active session: name always wraps fully */
|
||||
.session-item.session-active .session-name {
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
text-overflow: unset;
|
||||
}
|
||||
|
||||
/* Overlay: name wraps fully (300-char titles readable) */
|
||||
.session-item:not(.session-active):hover .session-name,
|
||||
.session-item:not(.session-active):focus-within .session-name {
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
text-overflow: unset;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user