15 Commits

Author SHA1 Message Date
Scott Idem
194c89f6d1 style(launcher): layout and Tailwind class adjustments
+layout.svelte: add lg:min-h-8/12 and max-h-screen to main content area.
launcher_background_sync.svelte: reposition sync monitor panel (bottom-15,
left-2, z-10 — was bottom-20, left-4, z-9999).
launcher_menu.svelte: reorder Tailwind classes for readability, no change
to applied styles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:09:08 -04:00
Scott Idem
469729ce22 revert(help_tech): restore ae_loc preservation in Clear & Reload button
Reverts the change from d1f5d0e2f that removed ae_loc preservation.
The tech help component is used across non-Launcher contexts where
users are authenticated normally and should not be signed out.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:06:56 -04:00
Scott Idem
d1f5d0e2fd fix(launcher): clear ae_loc in cache cleanup; align tech help Clear & Reload
menu_launcher_controls: handle_cache_cleanup now removes both ae_events_loc
and ae_loc from localStorage, giving a true clean slate on reload.

e_app_help_tech: Clear & Reload button no longer silently re-saves ae_loc
after clearing — if edit mode wipes localStorage, ae_loc goes with it.
Updated confirm message and title tooltip to say "you will be signed out"
instead of the previous misleading "sign-in will be preserved."

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:05:16 -04:00
Scott Idem
9c83567430 feat(launcher): add Clear Cache and Reload Launcher buttons to controls bar
Fills in two new buttons added to menu_launcher_controls.svelte:
- Clear Cache: removes 'ae_events_loc' from localStorage and deletes the
  ae_events_db IndexedDB database, then reloads — clears stale launch state
  without touching downloaded file cache or user prefs (theme/font size).
- Reload Launcher: calls native.window_control({ action: 'reload' }) in
  Electron, falls back to window.location.reload() in browser mode.

Also fixes a stray 'lucide-svelte' import (merged Recycle into '@lucide/svelte')
and separates cache_status from reset_apps_status so button labels stay correct
when multiple actions fire.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:02:26 -04:00
Scott Idem
b4d0d82141 fix(launcher): fix VLC stopping 10-15 seconds after open on macOS
Root cause: run_cmd uses exec() which blocks until the child exits. The
direct VLC binary forks its GUI process and exits — exec returns and the
post_script begins. The old post_script polled for VLC focus (up to 10s)
then sent Cmd+F, which fired mid-playback and stopped the video.

Fix 1 — nohup + &: detaches VLC from exec immediately so run_cmd returns
in ~0ms. This decouples the launcher flow from VLC's lifecycle.

Fix 2 — --fullscreen flag: VLC opens fullscreen directly via CLI option.
Eliminates the Cmd+F keystroke that was the proximate cause of the stop.

Fix 3 — > /dev/null 2>&1: silences VLC's verbose logging to prevent
exec's 1MB stdout buffer from overflowing and killing the process.

post_script simplified to a single `tell application "VLC" activate`
to bring VLC to the foreground after the 3s startup delay.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:10:37 -04:00
Scott Idem
15bfe6d5d6 feat(launcher): move Reset Apps to always-visible controls bar
Adds a presenter-accessible "Reset Apps" button to menu_launcher_controls
that is always visible (no edit mode required). Kills presentation apps
(PowerPoint, Keynote, Acrobat, VLC, soffice) — critical recovery path for
presenters stuck on stage with a frozen app.

Also: warning colors on All Files / All Sessions when showing non-default
(hidden) content, and state-aware tooltips on the Display Mode toggle that
describe current state and what pressing will do.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:02:22 -04:00
Scott Idem
dddf4b6170 feat(launcher): restore Kill Apps button in Native OS config
Kills the standard conference presentation app set between sessions:
Microsoft PowerPoint, Keynote, Adobe Acrobat Reader DC, VLC, soffice.

- Calls native.kill_processes({ process_name_li }) via existing relay
- Process list overridable per device via event_device.other_json.launcher.kill_process_li
- Button lives in Native OS config > System Actions (edit mode only)
- Reuses system_status for feedback — shows which apps are being killed
- Original list recovered from git history of legacy architecture docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 16:43:42 -04:00
Scott Idem
587b815446 docs(todo): mark composable flow + slide scripts done; add event_file cfg_json backend task
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 15:58:38 -04:00
Scott Idem
ca51a82dae feat(launcher): richer tooltip on file download button
Tooltip now shows file size, created/updated timestamps, and open_in_os
setting alongside the existing SHA256 and hosted ID info.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 15:58:35 -04:00
Scott Idem
a38320c7f5 fix(launcher): monospace font for session list date/time column
Datetime values align cleanly across rows when rendered in font-mono.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 15:58:31 -04:00
Scott Idem
c76fb8f2b5 fix(launcher): open_in_os win routing, display override, and onsite ext fix
- open_in_os='win' now routes to Windows launch profiles (pptxwin/pptwin/odpwin/pdfwin)
  via WIN_EXTENSION_MAP in get_launch_profile() — was silently ignored before
- Display override migrated from non-existent cfg_json backend field to localStorage
  ($events_loc.launcher.file_display_overrides) — only visible in edit mode; TODO added
  for proper backend column when event_file gains cfg_json
- Onsite mode WIN extension rename now covers all 4 types (pptx, ppt, odp, pdf)
  instead of only pptx/ppt
- open_in_os button shows LoaderCircle spinner during API call
- Remove cfg_json from properties_to_save (column does not exist on event_file table)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 15:58:27 -04:00
Scott Idem
a26ea8b49c fix(launcher): optimistic update for display override button
Without this, the button depended on the liveQuery round-trip to show
the new state — invisible on stale IDB caches that predate the cfg_json
properties_to_save fix. Now mutates event_file_obj locally on click so
the button reflects the new state immediately, with the background
refresh as confirmation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 14:18:58 -04:00
Scott Idem
21fad1a698 fix(launcher): restore open_in_os win routing, fix cfg_json in IDB, fix display state
open_in_os = 'win': get_launch_profile() now maps pptx→pptxwin, ppt→pptwin,
odp→odpwin, pdf→pdfwin when open_in_os is 'win', routing to the Windows-variant
launch profiles (Parallels/CrossOver). Was never wired in native mode — feature
was silently lost in the MasterKey→Launcher port.

cfg_json missing from properties_to_save: the per-file display override was
always read as undefined from Dexie because cfg_json was never saved. Added
cfg_json to properties_to_save so display_override and any other cfg fields
persist correctly. NOTE: IDB_CONTENT_VERSIONS for event_file is not yet wired;
existing devices need a manual cache clear to pick up the new field.

Display override button: removed $ae_loc.is_native gate — must be configurable
from any device ahead of the event, not only on the podium Mac.

Display toggle persistence: quick_display_mode now reads from and writes to
$events_loc.launcher.display_mode so the last-set state survives page reloads
instead of always defaulting to 'extend'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 14:12:12 -04:00
Scott Idem
33e9eeef78 fix(launcher): retry activate in post-script loop to beat macOS focus-stealing
All app post-scripts called activate once before the poll loop, then only
checked `frontmost` passively. When Electron retains focus (common when the
app is spawned via run_cmd), macOS focus-stealing prevention blocks the one-
shot activate and VLC/PowerPoint/etc. never come to front. The poll loop
times out and fires the keystroke at Electron instead.

Fix: move activate inside the repeat loop so it re-fires every 0.5s until
macOS yields focus. Also bumped VLC post_delay_ms 1000→2000 and iterations
15→20 for slow conference machines.

Affected profiles: VLC (mac), PowerPoint (mac), LibreOffice (mac+win),
Acrobat (mac+win).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 13:51:53 -04:00
Scott Idem
172ea994c7 refactor(launcher): consolidate menu controls and anchor to bottom
- Combine Extend/Mirror into a single toggle button, moved behind edit_mode
- All edit-mode controls (All Files, All Sessions, Display) now share consistent preset-tonal-tertiary styling
- Remove the always-visible display row and its non-native-mode disclaimer
- Wrap Menu_launcher_controls in mt-auto to keep it pinned to the bottom of the sidebar regardless of session count
- Add min-w-20 to file size chip to prevent collapse on narrow sessions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 13:48:53 -04:00
10 changed files with 428 additions and 276 deletions

View File

@@ -5,13 +5,14 @@
## 🔴 CMSC Charlotte — May 27 (Presentation Management)
**Drive down:** May 25 | **Setup:** May 26 morning | **Show:** May 27+
- [ ] **[Launcher] Composable open flow** — refactor `handle_open_file()` to use
`copy_from_cache_to_temp` + `run_osascript` / `run_cmd` directly instead of the all-in-one
`launch_from_cache`. Finer error handling at each step.
- [ ] **[Launcher] Slide control scripts in Svelte config** — Move AppleScript one-liners from
Electron to device config or Svelte constants.
- [ ] **[Launcher] `kill_processes` target list in config** — Implement UI for manual "Kill Apps"
button and auto-cleanup on file open.
- [x] **[Launcher] Composable open flow** — `handle_open_file()` uses `copy_from_cache_to_temp` +
`run_osascript` / `run_cmd` directly with per-step error handling. Complete.
- [x] **[Launcher] Slide control scripts in Svelte config** — AppleScript post_scripts live in
`ae_launcher__default_launch_profiles.ts`. VLC focus-stealing fix applied. Complete.
- [x] **[Launcher] Kill Apps button** — "Kill Apps" button added to Native OS config (System
Actions, edit mode only). Kills PowerPoint, Keynote, Adobe Acrobat Reader DC, VLC, soffice.
List overridable via `event_device.other_json.launcher.kill_process_li`. Auto-cleanup on file
open (deferred — manual button sufficient for CMSC).
- [ ] **[Launcher] End-to-end test on macOS** — test pptx and key opens on a real podium Mac.
- [ ] **[Launcher/Electron] Wallpaper stops applying after several changes (post-CMSC)** —
Append timestamp/random suffix to temp filename so macOS always sees a new path.
@@ -79,6 +80,12 @@ The app uses `svelte-persisted-store` (coarse reactivity). Migration target: rep
## ⚙️ DevOps & Backend
- [ ] **[Backend] `event_file` — add `cfg_json` column (post-CMSC)** — The per-file display
override currently uses a localStorage workaround (`$events_loc.launcher.file_display_overrides`)
because `event_file` has no JSON blob column. Proper fix: add `cfg_json` to the `event_file` DB
table, expose it through the FastAPI model, then migrate the frontend back to reading/writing the
backend field (restoring global/cross-device persistence). Frontend code is in
`launcher_file_cont.svelte` — search for `file_display_overrides`.
- [ ] **[Backend] Re-add `Access-Control-Allow-Private-Network: true` CORS header.**
- [ ] **[DevOps] Nginx caching** — Investigate `index.html` cache-pickup issues.
- [ ] **[DevOps] Simplify Dockerfile env file selection** — Use plain `.env` instead of `BUILD_MODE`.

View File

@@ -346,7 +346,20 @@ async function handle_click() {
disabled={require_auth && !$ae_loc.authenticated_access}
class={variant_classes}
onclick={handle_click}
title={`Download this file:\n${final_filename}\n[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}...\nHosted ID: ${file_id}\n Linked to: ${linked_to_type} ID: ${linked_to_id}`}>
title={
`Download this file:
${final_filename}
[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}...
Hosted ID: ${file_id}
File size: ${hosted_file_obj.file_size ? ae_util.format_bytes(hosted_file_obj.file_size) : 'Unknown size'}
Created on: ${ae_util.iso_datetime_formatter(hosted_file_obj.created_on, 'datetime_short')}
Updated on: ${ae_util.iso_datetime_formatter(hosted_file_obj.updated_on, 'datetime_short')}
Open with: ${hosted_file_obj.open_in_os == 'win' ? 'Windows' : hosted_file_obj.open_in_os == 'mac' ? 'macOS' : hosted_file_obj.open_in_os == 'linux' ? 'Linux' : '--not set--'}
Linked to Type: ${linked_to_type ?? '--none--'} ID: ${linked_to_id ?? '---'}`
}>
{@render content()}
</button>
{/if}

View File

@@ -55,32 +55,35 @@ export interface LaunchProfile {
/**
* macOS VLC profile — uses direct binary path for max reliability.
* Bypasses `open -a` argument-handling quirks that could lose file path or re-use existing process.
*
* WHY nohup + &:
* run_cmd uses exec() which blocks until the child process exits (or the 30s timeout fires).
* The direct VLC binary forks a GUI process then exits — exec returns early and the code
* proceeds to the post_script. The old post_script polled for VLC focus (up to 10s) then
* sent Cmd+F, which was firing exactly 1015 seconds into playback and stopping the video.
* nohup + & detaches VLC immediately so exec returns in ~0ms, decoupling run_cmd from
* VLC's lifecycle entirely.
*
* WHY --fullscreen:
* Starting VLC fullscreen via flag avoids the need to send Cmd+F via AppleScript. The old
* keystroke approach was the proximate cause of the video stopping — Cmd+F may have hit the
* wrong VLC window, triggered a menu action, or paused playback during the fullscreen
* transition. Using the flag is simpler and more reliable.
*
* WHY > /dev/null 2>&1:
* VLC logs verbosely to stdout/stderr. exec() buffers output (1MB default). Without
* redirection the buffer could overflow and kill VLC mid-playback.
*/
function make_vlc_mirror_mac_profile(): LaunchProfile {
return {
app: 'VLC (macOS)',
display_mode: 'mirror',
// Direct binary path ensures VLC receives media file + flags reliably.
// `--no-play-and-exit` prevents closing on end, `--play-and-pause` holds final frame.
open_cmd: '/Applications/VLC.app/Contents/MacOS/VLC --no-play-and-exit --play-and-pause "{{path}}"',
post_delay_ms: 1000,
// Poll until VLC is frontmost before sending Cmd+F. A fixed delay is unreliable because
// VLC cold-start on a loaded conference Mac can take 3-5 seconds.
// Polling (15 × 0.5 s = up to 7.5 s after the initial wait) fires as soon as VLC is ready.
open_cmd: 'nohup /Applications/VLC.app/Contents/MacOS/VLC --no-play-and-exit --play-and-pause --fullscreen "{{path}}" > /dev/null 2>&1 &',
post_delay_ms: 3000,
// Activate VLC after it has had time to open. Fullscreen is already set by the flag
// above — this just ensures VLC is the frontmost app and the presenter sees it.
post_script: `tell application "VLC"
activate
end tell
repeat 15 times
delay 0.5
tell application "System Events"
if frontmost of process "VLC" is true then exit repeat
end tell
end repeat
delay 0.3
tell application "System Events"
tell process "VLC"
keystroke "f" using command down
end tell
end tell`
};
}
@@ -104,10 +107,10 @@ const POWERPOINT_MAC_EXTEND_PROFILE: LaunchProfile = {
display_mode: 'extend',
open_cmd: 'open -a "Microsoft PowerPoint" "{{path}}"',
post_delay_ms: 1000,
post_script: `tell application "Microsoft PowerPoint"
activate
end tell
repeat 15 times
post_script: `repeat 20 times
tell application "Microsoft PowerPoint"
activate
end tell
delay 0.5
tell application "System Events"
if frontmost of process "Microsoft PowerPoint" is true then exit repeat
@@ -148,10 +151,10 @@ const LIBREOFFICE_MAC_EXTEND_PROFILE: LaunchProfile = {
display_mode: 'extend',
open_cmd: 'open -a "LibreOffice" "{{path}}"',
post_delay_ms: 1000,
post_script: `tell application "LibreOffice"
activate
end tell
repeat 15 times
post_script: `repeat 20 times
tell application "LibreOffice"
activate
end tell
delay 0.5
tell application "System Events"
if frontmost of process "soffice" is true then exit repeat
@@ -170,10 +173,10 @@ const ACROBAT_MAC_MIRROR_PROFILE: LaunchProfile = {
display_mode: 'mirror',
open_cmd: 'open -a "Adobe Acrobat Reader DC" "{{path}}"',
post_delay_ms: 1000,
post_script: `tell application "Adobe Acrobat Reader DC"
activate
end tell
repeat 15 times
post_script: `repeat 20 times
tell application "Adobe Acrobat Reader DC"
activate
end tell
delay 0.5
tell application "System Events"
if frontmost of process "AdobeReader" is true then exit repeat
@@ -215,10 +218,10 @@ const LIBREOFFICE_WIN_EXTEND_PROFILE: LaunchProfile = {
display_mode: 'extend',
open_cmd: 'open -a "LibreOffice" "{{path}}"',
post_delay_ms: 1500,
post_script: `tell application "LibreOffice"
activate
end tell
repeat 15 times
post_script: `repeat 20 times
tell application "LibreOffice"
activate
end tell
delay 0.5
tell application "System Events"
if frontmost of process "soffice" is true then exit repeat
@@ -237,10 +240,10 @@ const ACROBAT_WIN_MIRROR_PROFILE: LaunchProfile = {
display_mode: 'mirror',
open_cmd: 'open -a "Acrobat Reader Windows" "{{path}}"',
post_delay_ms: 1500,
post_script: `tell application "Acrobat Reader Windows"
activate
end tell
repeat 15 times
post_script: `repeat 20 times
tell application "Acrobat Reader Windows"
activate
end tell
delay 0.5
tell application "System Events"
if frontmost of process "Acrobat Reader Windows" is true then exit repeat

View File

@@ -17,7 +17,8 @@ import {
RefreshCw,
SkipBack,
SkipForward,
Square
Square,
XCircle
} from '@lucide/svelte';
interface Props {
on_expand?: () => void;
@@ -69,6 +70,27 @@ async function handle_display_mode(mode: 'extend' | 'mirror') {
setTimeout(() => (system_status = ''), 4000);
}
// Process names sent to kill_processes() when the operator hits "Kill Apps".
// Covers the standard conference presentation app set — PowerPoint, Keynote, Acrobat, VLC, LibreOffice.
// Override per device via event_device.other_json.launcher.kill_process_li.
const DEFAULT_KILL_LIST = [
'Microsoft PowerPoint',
'Keynote',
'Adobe Acrobat Reader DC',
'VLC',
'soffice'
];
async function handle_kill_apps() {
const native_device = ($ae_loc as any).native_device ?? null;
const process_name_li: string[] =
native_device?.other_json?.launcher?.kill_process_li ?? DEFAULT_KILL_LIST;
system_status = `Killing: ${process_name_li.join(', ')}...`;
await native.kill_processes({ process_name_li });
system_status = 'Kill signal sent';
setTimeout(() => (system_status = ''), 4000);
}
// Modal state for dangerous actions
let show_power_confirm = $state<{ action: string; label: string } | null>(null);
</script>
@@ -240,6 +262,16 @@ let show_power_confirm = $state<{ action: string; label: string } | null>(null);
disabled={!$ae_loc.site_header_image_path}>
<Image size="0.85em" class="mr-1 shrink-0" /> Reset Wallpaper
</button>
<!-- Kill running presentation apps — PowerPoint, Keynote, Acrobat, VLC, LibreOffice.
Use between sessions to ensure a clean slate. List is overridable via
event_device.other_json.launcher.kill_process_li. -->
<button
type="button"
onclick={handle_kill_apps}
class="btn btn-xs preset-tonal-error hover:preset-filled-error-500 justify-start"
title="Kill presentation apps: {DEFAULT_KILL_LIST.join(', ')}">
<XCircle size="0.85em" class="mr-1 shrink-0" /> Kill Apps
</button>
</div>
</div>
<div class="flex flex-col gap-1">

View File

@@ -601,8 +601,10 @@ $effect(() => {
class="
static
m-auto
mb-16 h-full w-full
max-w-7xl border-x
mb-16
lg:min-h-8/12 h-full max-h-screen
w-full max-w-7xl
border-x
border-gray-200
transition-all sm:mb-12
dark:border-gray-600

View File

@@ -645,7 +645,7 @@ async function force_location_sync() {
and the sys bar (bottom-12 right-2). Panel grows upward from the status chip. -->
{#if $events_loc.launcher.app_mode === 'native' || $ae_loc.is_native}
<div
class="pointer-events-none fixed bottom-20 left-4 z-[9999] flex flex-col items-start gap-2">
class="pointer-events-none fixed bottom-15 left-2 z-10 flex flex-col items-start gap-2">
{#if show_monitor}
<div
class="bg-surface-50/95 dark:bg-surface-900/95 text-surface-800 dark:text-surface-100 border-surface-200 dark:border-primary-700 pointer-events-auto min-w-52 rounded-lg border p-3 font-mono text-[10px] shadow-2xl backdrop-blur-sm">

View File

@@ -93,6 +93,14 @@ let open_file_status: null | string = $state(null);
let open_file_status_message: null | string = $state(null);
let open_file_error_detail: string | null = $state(null);
let open_in_os_loading: boolean = $state(false);
/** Reactive display override for this file — stored in $events_loc (localStorage) not in the backend. */
const current_display_override = $derived.by(() => {
const overrides = (($events_loc.launcher as Record<string, unknown>)?.file_display_overrides ?? {}) as Record<string, string>;
return (overrides[event_file_id] ?? null) as 'extend' | 'mirror' | 'none' | null;
});
/** State for the native test mode debug popup */
let test_mode_popup_open: boolean = $state(false);
let test_mode_popup_data: Record<string, any> | null = $state(null);
@@ -135,9 +143,27 @@ function get_launch_profile(
native_device?.launch_profiles ??
null;
const local_profiles = ($events_loc as any).launcher?.launch_profiles ?? null;
const display_override = file_obj?.cfg_json?.display_override ?? null;
// Display override is stored per-device in $events_loc — not in the backend (event_file has no JSON column).
// This is intentional: display mode is a room/device preference, not a global file property.
const launcher_kv = $events_loc.launcher as Record<string, unknown>;
const file_display_overrides = (launcher_kv?.file_display_overrides ?? {}) as Record<string, string>;
const display_override = (file_display_overrides[event_file_id] ?? null) as 'extend' | 'mirror' | 'none' | null;
// open_in_os = 'win' routes to the Windows-variant profile for apps that have one.
// These profiles target Windows PowerPoint / LibreOffice / Acrobat running via Parallels or CrossOver.
const WIN_EXTENSION_MAP: Record<string, string> = {
pptx: 'pptxwin',
ppt: 'pptwin',
odp: 'odpwin',
pdf: 'pdfwin'
};
const effective_extension =
file_obj?.open_in_os === 'win'
? (WIN_EXTENSION_MAP[extension] ?? extension)
: extension;
return resolve_launch_profile(
extension,
effective_extension,
display_override,
device_profiles,
local_profiles
@@ -455,12 +481,11 @@ async function handle_open_file() {
open_file_status_message = 'Downloading (Onsite Mode)...';
open_file_error_detail = null;
// Append 'win' to the filename for extensions that have Windows file associations
// (pptx→pptxwin, ppt→pptwin, odp→odpwin, pdf→pdfwin). Must match WIN_EXTENSION_MAP.
const WIN_ONSITE_EXTS = ['pptx', 'ppt', 'odp', 'pdf'];
let filename = event_file_obj.filename;
if (
(event_file_obj.extension === 'ppt' ||
event_file_obj.extension === 'pptx') &&
event_file_obj.open_in_os === 'win'
) {
if (event_file_obj.open_in_os === 'win' && WIN_ONSITE_EXTS.includes(event_file_obj.extension)) {
filename = event_file_obj.filename + 'win';
}
@@ -606,7 +631,7 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
click={handle_open_file}>
{#snippet label()}
{@const file_id = event_file_obj.hosted_file_id}
<span class="shrink border-r border-gray-400 pr-1 text-xs">
<span class="shrink border-r border-surface-300-700 pr-1 text-xs">
{#await ae_promises[event_file_id]}
<LoaderCircle
size="1em"
@@ -620,26 +645,28 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
{/if}
</span>
{:then result}
<span class=" font-mono">
{#if is_url}
<Link2 size="1em" class="mx-0.5 inline {!is_online ? 'text-warning-500' : ''}" />
<span class:text-warning-500={!is_online}>url</span>
{#if !is_online}<WifiOff size="0.85em" class="mx-0.5 inline text-warning-500" title="Network offline" />{/if}
<Link2 size="1em" class="inline opacity-50 {!is_online ? 'text-warning-900-100' : ''}" />
<span class:text-warning-900-100={!is_online}>url</span>
{#if !is_online}<WifiOff size="0.85em" class="inline text-warning-900-100" title="Network offline" />{/if}
{:else}
{@const FileIcon =
ae_util.file_extension_icon_lucide(
event_file_obj.extension
)}
<FileIcon size="1em" class="mx-0.5 inline" />
<FileIcon size="1em" class="inline opacity-50" />
{event_file_obj.extension}
{#if result === null || result === false}
<span class="text-error-500"
<span class="text-error-900-100"
><TriangleAlert
size="1em"
class="mx-1 inline" />Failed!</span>
class="inline" />Failed!</span>
{/if}
{/if}
</span>
{:catch error}
<span class="text-error-500" title={error?.message}
<span class="text-error-900-100" title={error?.message}
><AlertCircle
size="1em"
class="mx-0.5 inline" />Error!</span>
@@ -673,79 +700,71 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
<button
type="button"
onclick={async () => {
let new_val: string | null;
if (!event_file_obj?.open_in_os) new_val = 'win';
else if (event_file_obj?.open_in_os == 'win') new_val = 'mac';
else new_val = null;
await api.update_ae_obj({
api_cfg: $ae_api,
obj_type: 'event_file',
obj_id: event_file_id,
fields: { open_in_os: new_val }
});
events_func.load_ae_obj_id__event_file({
api_cfg: $ae_api,
event_file_id: event_file_obj?.event_file_id,
log_lvl
});
}}
class="btn btn-sm group transition-all"
class:preset-tonal-warning={event_file_obj?.open_in_os == 'win'}
class:preset-tonal-success={event_file_obj?.open_in_os == 'mac'}
disabled={!$ae_loc.trusted_access}
title={`Open in OS: ${
event_file_obj?.open_in_os
? event_file_obj.open_in_os.toUpperCase()
: 'None'
}`}
>
{#if event_file_obj?.open_in_os == 'win'}
<!-- <Monitor
size="1em"
class="m-1" /> -->
Win
{:else if event_file_obj?.open_in_os == 'mac'}
<!-- <Laptop
size="1em"
class="m-1" /> -->
Mac
{:else}
<FolderOpen size="1em" class="m-1" />
{/if}
</button>
{#if $ae_loc.trusted_access && $ae_loc.is_native}
<!-- Display override: per-file display_mode override for this file only.
null = use profile default, 'extend' = force extend, 'mirror' = force mirror.
Stored in event_file.cfg_json.display_override. Cycles null → extend → mirror → null. -->
<button
type="button"
onclick={async () => {
const cur = event_file_obj?.cfg_json?.display_override ?? null;
let next: string | null;
if (!cur) next = 'extend';
else if (cur === 'extend') next = 'mirror';
else next = null;
const new_cfg = { ...(event_file_obj.cfg_json ?? {}), display_override: next };
open_in_os_loading = true;
try {
let new_val: string | null;
if (!event_file_obj?.open_in_os) new_val = 'win';
else if (event_file_obj?.open_in_os == 'win') new_val = 'mac';
else new_val = null;
await api.update_ae_obj({
api_cfg: $ae_api,
obj_type: 'event_file',
obj_id: event_file_id,
fields: { cfg_json: new_cfg }
fields: { open_in_os: new_val }
});
events_func.load_ae_obj_id__event_file({
api_cfg: $ae_api,
event_file_id: event_file_obj?.event_file_id,
log_lvl
});
} finally {
open_in_os_loading = false;
}
}}
class="btn btn-sm group transition-all"
class:preset-tonal-warning={event_file_obj?.open_in_os == 'win'}
class:preset-tonal-success={event_file_obj?.open_in_os == 'mac'}
disabled={!$ae_loc.trusted_access || open_in_os_loading}
title={`Open in OS: ${
event_file_obj?.open_in_os == 'win' ? 'Windows' : event_file_obj?.open_in_os == 'mac' ? 'macOS' : event_file_obj?.open_in_os == 'linux' ? 'Linux' : '--not set--'
}`}
>
{#if open_in_os_loading}
<LoaderCircle size="1em" class="m-1 animate-spin" />
{:else if event_file_obj?.open_in_os == 'win'}
Win
{:else if event_file_obj?.open_in_os == 'mac'}
Mac
{:else}
<FolderOpen size="1em" class="m-1" />
{/if}
</button>
{#if $ae_loc.edit_mode}
<!-- Display override (temporary — local/device only, stored in $events_loc).
Cycles null → extend → mirror → null. Instant write, no API call.
TODO: replace with backend cfg_json once event_file gains a JSON column. -->
<button
type="button"
onclick={() => {
const cur = current_display_override;
const next: 'extend' | 'mirror' | null = !cur ? 'extend' : cur === 'extend' ? 'mirror' : null;
const launcher = $events_loc.launcher as Record<string, unknown>;
const new_overrides = { ...((launcher?.file_display_overrides ?? {}) as Record<string, string>) };
if (next === null) {
delete new_overrides[event_file_id];
} else {
new_overrides[event_file_id] = next;
}
launcher.file_display_overrides = new_overrides;
}}
class="btn btn-sm transition-all"
class:preset-tonal-primary={event_file_obj?.cfg_json?.display_override === 'extend'}
class:preset-tonal-warning={event_file_obj?.cfg_json?.display_override === 'mirror'}
title={`Display override: ${event_file_obj?.cfg_json?.display_override ?? 'default'}`}>
{#if event_file_obj?.cfg_json?.display_override === 'extend'}
class:preset-tonal-primary={current_display_override === 'extend'}
class:preset-tonal-warning={current_display_override === 'mirror'}
title={`Display override: ${current_display_override ?? 'default'}`}>
{#if current_display_override === 'extend'}
Ext
{:else if event_file_obj?.cfg_json?.display_override === 'mirror'}
{:else if current_display_override === 'mirror'}
Mir
{:else}
<Monitor size="1em" class="m-1" />
@@ -755,8 +774,13 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
<span
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}>
<CalendarDays size="0.85em" class="inline" />
class:hidden={hide_created_on}
title={`Created on:\n${ae_util.iso_datetime_formatter(
event_file_obj.created_on,
'datetime_long'
)}`}
>
<CalendarDays size="0.85em" class="inline opacity-50" />
<span class="w-18"
>{ae_util.iso_datetime_formatter(
event_file_obj.created_on,
@@ -765,9 +789,11 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
</span>
<span
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}>
<Save size="0.85em" class="inline" />
class="event_file_size preset-filled-surface-100-900 flex min-w-20 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}
title={`File size:\n${event_file_obj.file_size ? ae_util.format_bytes(event_file_obj.file_size) : 'Unknown size'}\nBytes: ${event_file_obj.file_size}`}
>
<Save size="0.85em" class="inline opacity-50" />
{#if event_file_obj.file_size}{ae_util.format_bytes(
event_file_obj.file_size
)}{/if}

View File

@@ -146,8 +146,10 @@ let ae_promises: key_val = $state({
<div
class="
event_launcher_menu
flex h-full w-full max-w-full
shrink flex-col flex-wrap items-center justify-start gap-1
h-full
w-full max-w-full
flex flex-col flex-wrap items-center justify-start gap-1
shrink
">
<!-- overflow-x-clip -->
@@ -247,5 +249,7 @@ let ae_promises: key_val = $state({
bind:trigger_reload__event_session_obj_id />
{/if}
<Menu_launcher_controls />
<div class="mt-auto w-full">
<Menu_launcher_controls />
</div>
</div>

View File

@@ -10,15 +10,20 @@
*
* SECTIONS:
* 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) — Reset Apps, font size cycler, light/dark toggle
*
* WHY ALWAYS-VISIBLE ACCESSIBILITY CONTROLS:
* 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
* theme mode without hunting through the system menu or requiring admin access.
*
* WHY RESET APPS IS ALWAYS VISIBLE:
* If a presentation app (PowerPoint, Keynote, etc.) hangs or stalls on stage, the
* presenter needs a one-tap way to force-close it and recover — without entering edit
* mode or navigating the config menu. This button is intentionally prominent.
*/
import { Moon, Sun, Eye, EyeOff, Columns2, Copy } from '@lucide/svelte';
import { Moon, Sun, Eye, EyeOff, Columns2, Copy, XCircle, Trash, Recycle } from '@lucide/svelte';
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
@@ -30,78 +35,203 @@ interface Props {
let { log_lvl = $bindable(0) }: Props = $props();
let quick_display_mode = $state<'extend' | 'mirror'>('extend');
// Persist display mode across reloads — reflects the last-set state, not hardware-queried state.
let quick_display_mode = $state<'extend' | 'mirror'>(
($events_loc.launcher as any)?.display_mode ?? 'extend'
);
const is_native_launcher_mode = $derived(
!!$ae_loc.is_native && $events_loc.launcher.app_mode === 'native'
);
async function set_quick_display_mode(mode: 'extend' | 'mirror') {
if (!is_native_launcher_mode) return;
const res = await native.set_display_layout({ mode });
if (res?.success) quick_display_mode = mode;
async function toggle_display_mode() {
const next = quick_display_mode === 'extend' ? 'mirror' : 'extend';
if (is_native_launcher_mode) {
const res = await native.set_display_layout({ mode: next });
if (res?.success) {
quick_display_mode = next;
($events_loc.launcher as any).display_mode = next;
}
} else {
quick_display_mode = next;
($events_loc.launcher as any).display_mode = next;
}
}
// Process names closed when presenter hits "Reset Apps". Covers the standard
// conference app set. Override per device via event_device.other_json.launcher.kill_process_li.
const DEFAULT_KILL_LIST = [
'Microsoft PowerPoint',
'Keynote',
'Adobe Acrobat Reader DC',
'VLC',
'soffice'
];
let reset_apps_status = $state('');
let cache_status = $state('');
async function handle_reset_apps() {
const native_device = ($ae_loc as any).native_device ?? null;
const process_name_li: string[] =
native_device?.other_json?.launcher?.kill_process_li ?? DEFAULT_KILL_LIST;
reset_apps_status = 'Resetting...';
await native.kill_processes({ process_name_li });
reset_apps_status = 'Done';
setTimeout(() => (reset_apps_status = ''), 3000);
}
async function handle_cache_cleanup() {
cache_status = 'Clearing...';
try {
localStorage.removeItem('ae_events_loc');
localStorage.removeItem('ae_loc');
indexedDB.deleteDatabase('ae_events_db');
cache_status = 'Done — reloading...';
setTimeout(() => window.location.reload(), 800);
} catch {
cache_status = 'Error';
setTimeout(() => (cache_status = ''), 3000);
}
}
function handle_reload_launcher() {
if ($ae_loc.is_native) {
native.window_control({ action: 'reload' });
} else {
window.location.reload();
}
}
</script>
<div class="flex w-full max-w-full flex-col items-center justify-center gap-1">
<!-- ── Visibility toggles — edit mode only ── -->
{#if $ae_loc.edit_mode}
<div
class="flex w-full max-w-full flex-row items-center justify-center gap-1">
<button
type="button"
onclick={() => {
if ($events_loc.launcher.show_content__hidden_files) {
$events_loc.launcher.show_content__hidden_files = false;
$events_loc.launcher.show_content__internal_files = false;
} else {
$events_loc.launcher.show_content__hidden_files = true;
$events_loc.launcher.show_content__internal_files = true;
}
}}
class="
btn btn-sm preset-tonal-tertiary
hover:preset-filled-tertiary-500 w-1/2
max-w-1/2 text-xs
transition-all
"
title="Toggle visibility of hidden and draft files in the launcher file list.">
{#if $events_loc.launcher.show_content__hidden_files}
<EyeOff size="0.85em" class="m-1 text-neutral-800/80" />
Hide Files
{:else}
<Eye size="0.85em" class="m-1 text-neutral-800/80" />
All Files
{/if}
</button>
<div class="flex flex-row flex-wrap w-full max-w-full items-center justify-center gap-0.5">
<button
type="button"
onclick={() => {
$events_loc.launcher.show_content__hidden_sessions =
!$events_loc.launcher.show_content__hidden_sessions;
}}
class="
btn btn-sm preset-tonal-tertiary
hover:preset-filled-tertiary-500 w-1/2
max-w-1/2 text-xs
transition-all
"
title="Toggle visibility of hidden and cancelled sessions in the launcher session list.">
{#if $events_loc.launcher.show_content__hidden_sessions}
<EyeOff size="0.85em" class="m-1 text-neutral-800/80" />
Hide Sessions
{:else}
<Eye size="0.85em" class="m-1 text-neutral-800/80" />
All Sessions
{/if}
</button>
</div>
{/if}
<!-- All Files / All Sessions toggles.
Warning color when showing hidden content — signals non-default state to operators. -->
<div class="min-w-32 flex flex-row flex-wrap items-center justify-center gap-0.5">
<button
type="button"
onclick={() => {
if ($events_loc.launcher.show_content__hidden_files) {
$events_loc.launcher.show_content__hidden_files = false;
$events_loc.launcher.show_content__internal_files = false;
} else {
$events_loc.launcher.show_content__hidden_files = true;
$events_loc.launcher.show_content__internal_files = true;
}
}}
class="btn btn-sm min-w-34 w-34 max-w-1/2 text-xs transition-all"
class:preset-tonal-warning={$events_loc.launcher.show_content__hidden_files}
class:hover:preset-filled-warning-500={$events_loc.launcher
.show_content__hidden_files}
class:preset-tonal-tertiary={!$events_loc.launcher.show_content__hidden_files}
class:hover:preset-filled-tertiary-500={!$events_loc.launcher
.show_content__hidden_files}
title={$events_loc.launcher.show_content__hidden_files
? 'Showing all files including hidden and draft. Tap to hide them again.'
: 'Showing only public files. Tap to show all files including hidden and draft.'}>
{#if $events_loc.launcher.show_content__hidden_files}
<EyeOff size="0.85em" class="m-1" />
Hide Files
{:else}
<Eye size="0.85em" class="m-1" />
All Files
{/if}
</button>
<!-- ── Accessibility controls — always visible ── -->
<div
class="flex w-full max-w-full flex-row items-center justify-center gap-1">
{#if $ae_loc.edit_mode}
<button
type="button"
onclick={() => {
$events_loc.launcher.show_content__hidden_sessions =
!$events_loc.launcher.show_content__hidden_sessions;
}}
class="btn btn-sm min-w-34 w-34 max-w-1/2 text-xs transition-all"
class:preset-tonal-warning={$events_loc.launcher.show_content__hidden_sessions}
class:hover:preset-filled-warning-500={$events_loc.launcher
.show_content__hidden_sessions}
class:preset-tonal-tertiary={!$events_loc.launcher.show_content__hidden_sessions}
class:hover:preset-filled-tertiary-500={!$events_loc.launcher
.show_content__hidden_sessions}
title={$events_loc.launcher.show_content__hidden_sessions
? 'Showing all sessions including cancelled and hidden. Tap to hide them again.'
: 'Showing only active sessions. Tap to show all sessions including hidden and cancelled.'}>
{#if $events_loc.launcher.show_content__hidden_sessions}
<EyeOff size="0.85em" class="shrink-0" />
Hide Sessions
{:else}
<Eye size="0.85em" class="shrink-0" />
All Sessions
{/if}
</button>
{/if}
</div>
<div class="min-w-32 flex flex-row flex-wrap items-center justify-center gap-0.5">
<!-- Display mode toggle — warning color when mirroring (non-default operator setting).
Tooltip describes the CURRENT state and what pressing will do, so operators know
which way they are switching before they tap. -->
<button
type="button"
onclick={toggle_display_mode}
class="btn btn-sm min-w-34 w-34 max-w-1/2 text-xs transition-all"
class:preset-tonal-tertiary={quick_display_mode === 'extend'}
class:hover:preset-filled-tertiary-500={quick_display_mode === 'extend'}
class:preset-tonal-warning={quick_display_mode === 'mirror'}
class:hover:preset-filled-warning-500={quick_display_mode === 'mirror'}
title={quick_display_mode === 'extend'
? 'Screens are extended: laptop can show notes while projector shows slides. Tap to mirror — make both screens show the same content.'
: 'Screens are mirrored: both screens show the same content. Tap to extend — allow the laptop and projector to show different content.'}>
{#if quick_display_mode === 'extend'}
<Columns2 size="0.85em" class="shrink-0" />
Display: Extend
{:else}
<Copy size="0.85em" class="shrink-0" />
Display: Mirror
{/if}
</button>
</div>
<!-- ── Always-visible controls ── -->
<div class="w-full flex flex-row flex-wrap items-center justify-center gap-0.5">
{#if $ae_loc.edit_mode}
<button
type="button"
onclick={handle_cache_cleanup}
class="btn btn-sm preset-tonal-error hover:preset-filled-error-500 w-34 max-w-1/2 text-xs transition-all"
title="Clear localStorage and IDB caches used by the Launcher. Does *not* delete cached files."
>
<Trash size="0.85em" class="shrink-0" />
{cache_status || 'Clear Cache'}
</button>
{/if}
<button
type="button"
onclick={handle_reload_launcher}
class="btn btn-sm preset-tonal-error hover:preset-filled-error-500 w-34 max-w-1/2 text-xs transition-all"
title="Reload the Launcher interface."
>
<Recycle size="0.85em" class="shrink-0" />
Reload Launcher
</button>
<!-- Reset Apps: force-closes hung presentation apps without requiring edit mode.
Essential recovery tool for presenters stuck on stage with a frozen app. -->
<button
type="button"
onclick={handle_reset_apps}
class="btn btn-sm preset-tonal-error hover:preset-filled-error-500 w-34 max-w-1/2 text-xs transition-all"
title="Close all presentation apps (PowerPoint, Keynote, Adobe Acrobat, VLC). Use this if a presentation is frozen or stuck.">
<XCircle size="0.85em" class="shrink-0" />
{reset_apps_status || 'Reset Apps'}
</button>
</div>
<div class="w-full flex flex-row flex-wrap items-center justify-center gap-0.5">
<!-- Font size cycler: default → larger → smaller → default -->
<button
type="button"
@@ -115,29 +245,18 @@ async function set_quick_display_mode(mode: 'extend' | 'mirror') {
$ae_loc.font_size_mode = 'default';
}
}}
class="
btn btn-sm preset-tonal-tertiary
hover:preset-filled-tertiary-500 group
w-1/2 max-w-1/2
text-xs transition-all
"
class="btn btn-sm preset-tonal-tertiary hover:preset-filled-tertiary-500 group min-w-32 max-w-1/2 text-xs transition-all"
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'}
<span class="m-1 font-mono text-sm leading-none font-bold"
>A</span>
<span class="hidden text-xs group-hover:inline-block"
>Font: Normal</span>
<span class="m-1 font-mono text-sm leading-none font-bold">A</span>
<span class="hidden text-xs group-hover:inline-block">Font: Normal</span>
{:else if $ae_loc.font_size_mode === 'larger'}
<span class="m-1 font-mono text-base leading-none font-bold"
>A+</span>
<span class="hidden text-xs group-hover:inline-block"
>Font: Larger</span>
<span class="m-1 font-mono text-base leading-none font-bold">A+</span>
<span class="hidden text-xs group-hover:inline-block">Font: Larger</span>
{:else}
<span class="m-1 font-mono text-xs leading-none font-bold"
>A</span>
<span class="hidden text-xs group-hover:inline-block"
>Font: Smaller</span>
<span class="m-1 font-mono text-xs leading-none font-bold">A</span>
<span class="hidden text-xs group-hover:inline-block">Font: Smaller</span>
{/if}
</button>
@@ -145,17 +264,10 @@ async function set_quick_display_mode(mode: 'extend' | 'mirror') {
<button
type="button"
onclick={() => {
$ae_loc.theme_mode =
$ae_loc.theme_mode === 'dark' ? 'light' : 'dark';
$ae_loc.theme_mode = $ae_loc.theme_mode === 'dark' ? 'light' : 'dark';
}}
class="
btn btn-sm preset-tonal-tertiary
hover:preset-filled-tertiary-500 group
w-1/2 max-w-1/2
text-xs transition-all
"
title="Toggle light/dark display mode. Current: {$ae_loc.theme_mode ??
'light'}">
class="btn btn-sm preset-tonal-tertiary hover:preset-filled-tertiary-500 group min-w-32 max-w-1/2 text-xs transition-all"
title="Toggle light/dark display mode. Current: {$ae_loc.theme_mode ?? 'light'}">
{#if $ae_loc.theme_mode === 'dark'}
<Moon class="m-1 inline-block" size="1em" />
<span class="hidden group-hover:inline-block">Dark Mode</span>
@@ -165,51 +277,4 @@ async function set_quick_display_mode(mode: 'extend' | 'mirror') {
{/if}
</button>
</div>
<!-- ── Quick display mode controls — always visible (native-only action) ── -->
<div class="flex w-full max-w-full flex-row items-center justify-center gap-1">
<button
type="button"
onclick={() => set_quick_display_mode('extend')}
disabled={!is_native_launcher_mode}
class="
btn btn-sm group w-1/2 max-w-1/2 text-xs transition-all
border-2
"
class:border-primary-500={quick_display_mode === 'extend'}
class:preset-tonal-primary={quick_display_mode === 'extend'}
class:border-surface-400={quick_display_mode !== 'extend'}
class:preset-tonal-surface={quick_display_mode !== 'extend'}
title="Set display layout to Extend (separate laptop and projector screens).">
<Columns2 size="0.9em" class="m-1 inline-block" />
<span class="hidden group-hover:inline-block">Display: Extend</span>
<span class="group-hover:hidden">Extend</span>
</button>
<button
type="button"
onclick={() => set_quick_display_mode('mirror')}
disabled={!is_native_launcher_mode}
class="
btn btn-sm group w-1/2 max-w-1/2 text-xs transition-all
border-2
"
class:border-warning-500={quick_display_mode === 'mirror'}
class:preset-tonal-warning={quick_display_mode === 'mirror'}
class:border-surface-400={quick_display_mode !== 'mirror'}
class:preset-tonal-surface={quick_display_mode !== 'mirror'}
title="Set display layout to Mirror (same content on laptop and projector).">
<Copy size="0.9em" class="m-1 inline-block" />
<span class="hidden group-hover:inline-block">Display: Mirror</span>
<span class="group-hover:hidden">Mirror</span>
</button>
</div>
{#if !is_native_launcher_mode}
<div
class="text-[10px] leading-tight opacity-70 text-center px-2"
title="Shown here as a visual preview. Active in native app mode in the session room.">
Display toggle shown as an example preview. Active in native app mode in the session room.
</div>
{/if}
</div>

View File

@@ -275,11 +275,11 @@ $effect(() => {
When revealed, dimmed (opacity-40) with eye-slash icon. -->
<span
class="border-surface-400-600 min-w-20 shrink-0 border-r pr-1">
class="border-surface-300-700 min-w-20 shrink-0 border-r pr-1 font-mono">
{#if slct__event_session_id === event_session_obj?.id}
<CalendarCheck size="0.85em" class="inline" />
<CalendarCheck size="0.85em" class="inline opacity-50" />
{:else}
<CalendarDays size="0.85em" class="inline" />
<CalendarDays size="0.85em" class="inline opacity-50" />
{/if}
<span
class="text-xs"