Files
OSIT-AE-App-Svelte/src/lib/app_components/e_app_sys_bar.svelte
2026-04-08 10:21:08 -04:00

791 lines
38 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
/**
* e_app_sys_bar.svelte
* Global system bar — replaces e_app_sys_menu.svelte.
*
* Visual states:
* Idle: compact icon strip (bottom-right corner)
* Hover: info strip appears above bar; button labels revealed
* Expanded: full panel slides up above the bar
*
* Sub-components are reused unchanged for complex business logic
* (access_type, sign_in_out). Simple controls (theme, cfg utilities)
* are inlined here for clean panel layout without blue-bg conflicts.
*/
import {
Bug,
CircleX,
Eraser,
LogOut,
Maximize2,
Menu,
Minimize2,
Moon,
Palette,
RefreshCw,
ShieldEllipsis,
ShieldMinus,
ShieldUser,
Sun,
ToggleLeft,
ToggleRight,
User
} from '@lucide/svelte';
import { ae_loc, ae_sess, ae_api } from '$lib/stores/ae_stores';
import Element_access_type from '$lib/app_components/e_app_access_type.svelte';
import Element_sign_in_out from '$lib/app_components/e_app_sign_in_out.svelte';
import E_app_url_builder from '$lib/app_components/e_app_url_builder.svelte';
interface Props {
log_lvl?: number;
data: any;
hide?: null | boolean;
expand?: boolean;
}
let {
log_lvl = $bindable(0),
data = null,
hide = $bindable(false),
expand = $bindable(false)
}: Props = $props();
// Passed down to Element_access_type — triggers handle_clear_access() there.
// See e_app_access_type.svelte for the full clear logic (reverts to user_access_type).
let trigger_clear_access: null | boolean = $state(null);
// ── Accessibility: font size cycler ─────────────────────────────────────
function cycle_font_size() {
const mode = $ae_loc.font_size_mode;
if (!mode || mode === 'default') {
$ae_loc.font_size_mode = 'larger';
} else if (mode === 'larger') {
$ae_loc.font_size_mode = 'smaller';
} else {
$ae_loc.font_size_mode = 'default';
}
}
// ── Accessibility: dark / light toggle ──────────────────────────────────
function toggle_theme_mode() {
if ($ae_loc.theme_mode === 'light') {
$ae_loc.theme_mode = 'dark';
} else {
$ae_loc.theme_mode = 'light';
}
// DOM sync (class) is handled reactively in +layout.svelte effect #3
}
// ── Dev: clear all browser storage + key IndexedDB tables, then reload ──
function handle_clear_storage_db() {
if (
!confirm(
'Clear all local/session storage and IndexedDB? The page will reload.'
)
)
return;
localStorage.clear();
sessionStorage.clear();
indexedDB.deleteDatabase('ae_archives_db');
indexedDB.deleteDatabase('ae_core_db');
indexedDB.deleteDatabase('ae_events_db');
indexedDB.deleteDatabase('ae_journals_db');
indexedDB.deleteDatabase('ae_posts_db');
indexedDB.deleteDatabase('ae_sponsorships_db');
window.location.reload();
}
// ── Menu expand / collapse ───────────────────────────────────────────────
function toggle_expand() {
if (!expand) {
expand = true;
$ae_sess.sys_menu.expand = true;
$ae_loc.app_cfg.show_element__access_type = true;
if (!$ae_loc?.access_type || $ae_loc?.access_type === 'anonymous') {
// onDestroy in Element_access_type resets show_element__passcode_input to false
// on panel close, so we must restore it here for menu-button opens too.
$ae_sess.app_cfg.show_element__passcode_input = true;
$ae_sess.sys_menu.focus_passcode_input = true;
} else {
$ae_loc.sys_menu.expand_user = false;
$ae_sess.show__sign_in_out__fields = false;
}
} else {
expand = false;
$ae_sess.sys_menu.expand = false;
}
}
// ── Auth shield: click behaviour depends on current auth state ───────────
// Mirrors the three-state shield logic from e_app_sys_menu.svelte.
function handle_shield_click() {
if ($ae_loc.access_type && $ae_loc.access_type !== 'anonymous') {
if (
$ae_loc?.user_access_type &&
$ae_loc?.access_type === $ae_loc?.user_access_type
) {
// At user's own level — offer passcode upgrade
if (!expand) {
expand = true;
$ae_sess.sys_menu.expand = true;
$ae_loc.sys_menu.hide_access_type = false;
$ae_loc.sys_menu.expand_access_type = true;
} else {
$ae_loc.sys_menu.hide_access_type = false;
$ae_loc.sys_menu.expand_access_type = true;
}
} else {
// Elevated via passcode — offer to clear back to user level
trigger_clear_access = true;
if (expand) {
$ae_loc.sys_menu.hide_access_type = false;
expand = false;
$ae_loc.app_cfg.show_element__access_type = true;
$ae_sess.app_cfg.show_element__passcode_input = true;
} else {
$ae_loc.sys_menu.hide_access_type = false;
$ae_loc.sys_menu.expand_access_type = true;
}
}
} else {
// Anonymous — open menu and focus passcode
expand = true;
$ae_sess.sys_menu.expand = true;
$ae_loc.sys_menu.hide_access_type = false;
$ae_loc.sys_menu.expand_access_type = true;
$ae_sess.app_cfg.show_element__passcode_input = true;
$ae_sess.sys_menu.focus_passcode_input = true;
// Note: focus is handled reactively by the focus_input binding in Element_access_type.
// Direct getElementById here would fail — the panel DOM doesn't exist yet at this point.
}
}
// ── Panel section collapse state ─────────────────────────────────────────
// Open/closed per section — access open by default (most common action).
let sec_access = $state(true);
let sec_signin = $state(false);
let sec_appearance = $state(false);
let sec_dev = $state(false);
// ── Derived display helpers ──────────────────────────────────────────────
let font_label = $derived.by(() => {
if ($ae_loc.font_size_mode === 'larger') return 'A+';
if ($ae_loc.font_size_mode === 'smaller') return 'A';
return 'A';
});
let font_title = $derived.by(() => {
if ($ae_loc.font_size_mode === 'larger')
return 'Font: Larger — click for Smaller';
if ($ae_loc.font_size_mode === 'smaller')
return 'Font: Smaller — click for Normal';
return 'Font: Normal — click for Larger';
});
let person_display = $derived(
$ae_loc?.person?.informal_name ?? $ae_loc?.person?.given_name ?? null
);
let access_label = $derived.by(() => {
const t = $ae_loc?.access_type;
if (!t || t === 'anonymous') return null;
const map: Record<string, string> = {
super: 'Super',
manager: 'Manager',
administrator: 'Admin',
trusted: 'Trusted',
public: 'Public',
authenticated: "Auth'd"
};
return map[t] ?? t;
});
// Theme options — keep in sync with e_app_url_builder.svelte
const theme_options = [
{ value: '', label: '-- None --' },
{ value: 'cerberus', label: 'Cerberus' },
{ value: 'concord', label: 'Concord' },
{ value: 'crimson', label: 'Crimson' },
{ value: 'hamlindigo', label: 'Hamlindigo' },
{ value: 'modern', label: 'Modern' },
{ value: 'nouveau', label: 'Nouveau' },
{ value: 'rocket', label: 'Rocket' },
{ value: 'terminus', label: 'Terminus' },
{ value: 'vintage', label: 'Vintage' },
{ value: 'wintry', label: 'Wintry' },
{ value: 'AE_OSIT_default', label: 'OSIT' },
{ value: 'AE_Firefly', label: 'Firefly ✦' },
{ value: 'AE_Firefly_SteelBlue', label: 'Firefly SteelBlue ✦' },
{ value: 'AE_Firefly_Indigo', label: 'Firefly Indigo ✦' },
{ value: 'AE_Firefly_Rainbow', label: 'Firefly Rainbow ✨' },
{ value: 'AE_Firefly_Axonius', label: 'Firefly Axonius ✦' },
{ value: 'AE_c_IDAA_light', label: 'IDAA light' },
{ value: 'AE_c_LCI', label: 'LCI' }
];
</script>
<!-- ══════════════════════════════════════════════════════════════════════════
OUTER WRAPPER — fixed bottom-right, hidden on print
Visibility when in iframe is controlled by +layout.svelte (trusted_access gate)
══════════════════════════════════════════════════════════════════════════ -->
<!-- {#if disable} -->
<!-- Hidden state: render nothing (not even the bar) -->
<!-- {:else} -->
<div
class="
ae_sys_bar
fixed
right-2 bottom-12 z-50
flex
flex-col items-end gap-1 print:hidden
"
class:d-none={hide}
class:hidden={hide}>
<!-- ── EXPANDED PANEL ──────────────────────────────────────────────────── -->
{#if expand}
<div
class="
ae_sys_panel
flex max-h-[80vh] w-80 max-w-[94vw]
flex-col items-stretch
gap-0 overflow-x-hidden overflow-y-auto
rounded-xl border border-gray-200/70 bg-white/85 text-sm
text-gray-900 shadow-2xl backdrop-blur-md
dark:border-gray-700/70 dark:bg-gray-900/85
dark:text-gray-100
">
<!-- Panel header: person info + close -->
<div
class="sticky top-0 z-10 flex items-center justify-between border-b border-gray-100/70 bg-white/85 px-3 py-2 backdrop-blur-md dark:border-gray-800/70 dark:bg-gray-900/85">
<div
class="flex min-w-0 items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
{#if $ae_loc?.person_id}
<User size="0.9em" class="shrink-0" />
<span class="truncate">{person_display ?? '—'}</span>
{/if}
{#if access_label}
<span
class="text-primary-600 dark:text-primary-400 shrink-0 font-semibold">
· {access_label}
</span>
{/if}
{#if !$ae_loc?.person_id && !access_label}
<span class="italic opacity-50">Not signed in</span>
{/if}
</div>
<button
type="button"
class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-error ml-2 shrink-0 transition-all"
onclick={toggle_expand}
title="Close menu">
<CircleX size="1.1em" />
</button>
</div>
<!-- ── Sign In / Out (collapsible) ──────────────────────────────── -->
{#if $ae_loc?.app_cfg?.show_element__sign_in_out}
<div class="border-b border-gray-100 dark:border-gray-800">
<button
type="button"
class="flex w-full items-center justify-between px-3 py-2 text-xs font-semibold tracking-wider text-gray-500 uppercase transition-colors hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-800"
onclick={() => (sec_signin = !sec_signin)}>
<span class="flex min-w-0 items-center gap-1.5">
{#if $ae_loc?.person_id && $ae_loc?.user_id}
<LogOut
size="0.85em"
class="shrink-0 opacity-60" />
<span>Sign Out</span>
<span
class="truncate font-normal normal-case opacity-70">
{$ae_loc?.user?.username ??
person_display ??
''}
</span>
{:else}
<ShieldUser
size="0.85em"
class="shrink-0 opacity-60" />
<span>User Sign In</span>
{/if}
</span>
<span class="opacity-50">{sec_signin ? '▲' : '▼'}</span>
</button>
{#if sec_signin}
<div class="px-2 pb-2">
<Element_sign_in_out
{data}
hidden={$ae_loc.iframe ||
!$ae_loc.app_cfg
?.show_element__sign_in_out} />
</div>
{/if}
</div>
{/if}
<!-- ── Access Level / Passcode (collapsible) ────────────────────── -->
{#if !$ae_loc?.sys_menu?.hide_access_type && !$ae_loc?.iframe}
<div class="border-b border-gray-100 dark:border-gray-800">
<button
type="button"
class="flex w-full items-center justify-between px-3 py-2 text-xs font-semibold tracking-wider text-gray-500 uppercase transition-colors hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-800"
onclick={() => (sec_access = !sec_access)}>
<span class="flex min-w-0 items-center gap-1.5">
{#if $ae_loc?.access_type && $ae_loc?.access_type !== 'anonymous'}
{#if $ae_loc?.user_access_type && $ae_loc?.access_type !== $ae_loc?.user_access_type}
<ShieldMinus
size="0.85em"
class="shrink-0 opacity-60" />
<span>Elevated Access</span>
<span
class="text-primary-600 dark:text-primary-400 truncate font-normal normal-case"
>({access_label})</span>
{:else}
<ShieldEllipsis
size="0.85em"
class="shrink-0 opacity-60" />
<span>Access</span>
<span
class="text-primary-600 dark:text-primary-400 truncate font-normal normal-case"
>({access_label})</span>
{/if}
{:else}
<ShieldUser
size="0.85em"
class="shrink-0 opacity-60" />
<span>Enter Passcode</span>
{/if}
</span>
<span class="opacity-50">{sec_access ? '▲' : '▼'}</span>
</button>
{#if sec_access}
<div class="px-2 pb-2">
<Element_access_type
bind:hide={$ae_loc.sys_menu.hide_access_type}
bind:focus_input={
$ae_sess.sys_menu.focus_passcode_input
}
bind:expand={
$ae_loc.sys_menu.expand_access_type
}
bind:show_passcode_input={
$ae_sess.app_cfg
.show_element__passcode_input
}
bind:trigger_clear_access />
</div>
{/if}
</div>
{/if}
<!-- ── Appearance (collapsible) ─────────────────────────────────── -->
<div class="border-b border-gray-100 dark:border-gray-800">
<button
type="button"
class="flex w-full items-center justify-between px-3 py-2 text-xs font-semibold tracking-wider text-gray-500 uppercase transition-colors hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-800"
onclick={() => (sec_appearance = !sec_appearance)}>
<span class="flex items-center gap-1">
<Palette size="1em" class="opacity-60" />
Appearance
<span class="ml-1 font-normal normal-case opacity-60">
({$ae_loc?.theme_name ?? '—'} · {$ae_loc?.theme_mode ??
'—'})
</span>
</span>
<span class="opacity-50">{sec_appearance ? '▲' : '▼'}</span>
</button>
{#if sec_appearance}
<div class="space-y-3 px-3 pt-1 pb-3">
<!-- Quick actions row: mode + font (most commonly used) -->
<div class="flex gap-2">
<button
type="button"
class="btn btn-sm flex-1 transition-all
{$ae_loc?.theme_mode === 'dark'
? 'preset-tonal-secondary hover:preset-filled-secondary-500'
: 'preset-tonal-warning hover:preset-filled-warning-500'}"
onclick={toggle_theme_mode}
title="Toggle light/dark mode (currently: {$ae_loc?.theme_mode ??
'unknown'})">
{#if $ae_loc?.theme_mode === 'dark'}
<Moon size="1em" class="shrink-0" />
<span class="ml-1 text-xs">Dark</span>
{:else}
<Sun size="1em" class="shrink-0" />
<span class="ml-1 text-xs">Light</span>
{/if}
</button>
<button
type="button"
class="btn btn-sm preset-tonal-surface hover:preset-tonal-primary flex-1 transition-all"
onclick={cycle_font_size}
title={font_title}>
<span class="text-sm leading-none font-bold"
>{font_label}</span>
<span class="ml-1 text-xs opacity-70"
>Font</span>
</button>
</div>
<!-- Theme name select -->
<div class="space-y-1.5">
<div
class="text-xs font-medium text-gray-400 dark:text-gray-500">
Theme
</div>
<select
bind:value={$ae_loc.theme_name}
onchange={(e) =>
document.documentElement.setAttribute(
'data-theme',
(e.target as HTMLSelectElement).value
)}
class="select w-full border border-gray-300 bg-white text-sm text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100">
{#each theme_options as opt (opt.value)}
<option value={opt.value}
>{opt.label}</option>
{/each}
</select>
</div>
</div>
{/if}
</div>
<!-- ── Dev / Tools (edit mode only, collapsible) ────────────────── -->
<!--
Dev tools are separate from user-facing config.
These are only useful during development and testing —
reload, storage wipe, iframe toggle, URL builder, debug overlay.
-->
{#if $ae_loc.edit_mode}
<div class="border-b border-gray-100 dark:border-gray-800">
<button
type="button"
class="flex w-full items-center justify-between px-3 py-2 text-xs font-semibold tracking-wider text-gray-500 uppercase transition-colors hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-800"
onclick={() => (sec_dev = !sec_dev)}>
<span class="flex items-center gap-1">
<Bug size="0.9em" class="opacity-60" />
Dev / Tools
</span>
<span class="opacity-50">{sec_dev ? '▲' : '▼'}</span>
</button>
{#if sec_dev}
<div class="space-y-2 px-3 pb-3">
<!-- iframe mode toggle -->
{#if $ae_loc.iframe}
<a
class="btn btn-sm preset-tonal-secondary w-full justify-end"
href="/?iframe=false">
<Minimize2 size="1em" class="opacity-60" />
Exit iframe Mode
</a>
{:else}
<a
class="btn btn-sm preset-tonal-secondary w-full justify-end"
href="/?iframe=true">
<Maximize2 size="1em" class="opacity-60" />
Enable iframe Mode
</a>
{/if}
<!-- Reload -->
<button
type="button"
class="btn btn-sm preset-tonal-warning w-full justify-end"
onclick={() => window.location.reload()}
title="Hard reload the page">
<RefreshCw size="1em" class="opacity-60" />
Reload Page
</button>
<!-- Clear storage + IndexedDB -->
<button
type="button"
class="btn btn-sm preset-tonal-error w-full justify-end"
onclick={handle_clear_storage_db}
title="Clear localStorage, sessionStorage, and all IndexedDB tables then reload">
<Eraser size="1em" class="opacity-60" />
Clear Storage & DB
</button>
<!-- URL param builder -->
<div
class="border-t border-gray-100 pt-1 dark:border-gray-800">
<E_app_url_builder />
</div>
<!-- Debug overlay toggle -->
<div
class="border-t border-gray-100 pt-1 dark:border-gray-800">
<button
type="button"
class="btn btn-sm w-full justify-end {$ae_loc
.debug_menu.expand
? 'preset-filled-error-500'
: 'preset-outlined-error-400-600'} hover:preset-tonal-error transition-all"
onclick={() => {
$ae_loc.debug_menu.expand =
!$ae_loc.debug_menu.expand;
}}
title="Toggle debug overlay ($ae_loc dump)">
<Bug size="1em" class="shrink-0" />
<span class="ml-1 text-xs">
{$ae_loc.debug_menu.expand
? 'Hide'
: 'Show'} Debug Overlay
</span>
</button>
</div>
</div>
{/if}
</div>
{/if}
</div>
{/if}
<!-- END: Expanded panel -->
<!-- ── COMPACT BAR ────────────────────────────────────────────────────── -->
<!--
Outer div is `group` for group-hover.
The info strip is absolute (above the bar) so it never shifts the bar layout.
Hover labels on individual buttons use nested group/[name] variants.
delay-500 on the info strip prevents it flashing on quick mouse-overs.
-->
<div class="group relative">
<!-- Hover info strip — always in DOM (no mount flash), opacity-only transition.
pointer-events-none so it never blocks clicks. -->
<div
class="
pointer-events-none absolute right-0 bottom-full
mb-1
flex items-center gap-1.5
rounded-lg border border-gray-200/60
bg-white/30 px-2
py-1 text-xs whitespace-nowrap
text-gray-500 shadow-md
backdrop-blur-sm transition-opacity
delay-500 duration-200 dark:border-gray-700/60
dark:bg-gray-900/30 dark:text-gray-400
"
class:opacity-0={expand || (!person_display && !access_label)}
class:group-hover:opacity-100={!expand &&
(!!person_display || !!access_label)}>
{#if person_display}
<User size="0.8em" class="shrink-0" />
<span>{person_display}</span>
{/if}
{#if access_label}
<span
class="text-primary-600 dark:text-primary-400 font-semibold">
· {access_label}
</span>
{/if}
</div>
<!-- Bar strip — h-9 is fixed so inline hover labels never shift height -->
<div
class="
ae_sys_bar__strip
flex h-9 flex-row items-center
gap-1
rounded-xl border
border-gray-200/60
bg-white/30 px-2 shadow-lg
backdrop-blur-sm
transition-colors
duration-300
dark:border-gray-700/60 dark:bg-gray-900/30
"
class:border-primary-400={expand}>
<!-- AUTH STATUS SHIELD — hidden in iframe; host page manages auth context -->
{#if !$ae_loc?.iframe}
<button
type="button"
class="
btn btn-sm group/shield transition-all duration-200
{$ae_loc?.access_type &&
$ae_loc?.access_type !== 'anonymous'
? $ae_loc?.user_access_type &&
$ae_loc?.access_type === $ae_loc?.user_access_type
? 'variant-outline-surface hover:variant-ghost-warning'
: 'variant-outline-warning hover:variant-ghost-warning'
: 'variant-outline-surface hover:variant-ghost-success'}
"
onclick={handle_shield_click}
title={$ae_loc?.access_type &&
$ae_loc?.access_type !== 'anonymous'
? $ae_loc?.user_access_type &&
$ae_loc?.access_type === $ae_loc?.user_access_type
? `Access: ${$ae_loc?.access_type}. Click to use a passcode.`
: `Elevated access: ${$ae_loc?.access_type}. Click to clear.`
: 'Anonymous. Click to sign in or enter a passcode.'}>
{#if $ae_loc?.access_type && $ae_loc?.access_type !== 'anonymous'}
{#if $ae_loc?.user_access_type && $ae_loc?.access_type === $ae_loc?.user_access_type}
<ShieldEllipsis size="1.1em" class="shrink-0" />
{:else}
<ShieldMinus size="1.1em" class="shrink-0" />
{/if}
{:else}
<ShieldUser size="1.1em" class="shrink-0" />
{/if}
<span
class="btn-label max-w-0 overflow-hidden text-xs opacity-0 transition-all duration-300 ease-in-out group-hover/shield:max-w-20 group-hover/shield:opacity-100">
{access_label ?? 'Auth?'}
</span>
</button>
{/if}
<!-- FONT SIZE CYCLER — hidden in iframe; font choice belongs to the host page -->
{#if !$ae_loc?.iframe}
<button
type="button"
class="btn btn-sm preset-tonal-surface hover:preset-tonal-primary group/font transition-all duration-700 hover:duration-200"
onclick={cycle_font_size}
title={font_title}>
<span class="text-sm leading-none font-bold">{font_label}</span>
<span
class="btn-label max-w-0 overflow-hidden text-xs opacity-0 transition-all duration-700 hover:duration-200 ease-in-out group-hover/font:max-w-20 group-hover/font:opacity-100 group-hover/display:inline-block"
>Font</span>
</button>
{/if}
<!-- DARK / LIGHT TOGGLE — hidden in iframe; theme is set by the host page -->
{#if !$ae_loc?.iframe}
<button
type="button"
class="btn btn-sm preset-tonal-surface hover:preset-tonal-secondary group/mode transition-all duration-500 hover:duration-200"
onclick={toggle_theme_mode}
title="Toggle light/dark mode (currently: {$ae_loc?.theme_mode ??
'unknown'})">
{#if $ae_loc?.theme_mode === 'dark'}
<Moon size="1.1em" class="shrink-0" />
<span
class="btn-label max-w-0 overflow-hidden text-xs opacity-0 transition-all duration-700 hover:duration-200 ease-in-out group-hover/mode:max-w-20 group-hover/mode:opacity-100"
>Dark</span>
{:else}
<Sun size="1.1em" class="shrink-0" />
<span
class="btn-label max-w-0 overflow-hidden text-xs opacity-0 transition-all duration-700 hover:duration-200 ease-in-out group-hover/mode:max-w-20 group-hover/mode:opacity-100"
>Light</span>
{/if}
</button>
{/if}
<!-- EDIT MODE TOGGLE (public+ only; was authenticated+) ───────────────────── -->
{#if $ae_loc?.public_access}
{#if $ae_loc.edit_mode}
<button
type="button"
class="btn btn-sm preset-tonal-warning hover:preset-tonal-success group/edit transition-all duration-500 hover:duration-200 pl-.05 pr-1.5"
onclick={() => {
$ae_loc.edit_mode = false;
}}
title="Edit mode ON click to turn off">
<ToggleRight size="1em" class="inline-block text-sm" />
<span
class="btn-label max-w-0 overflow-hidden text-xs font-semibold opacity-0 transition-all duration-700 hover:duration-200 ease-in-out group-hover/edit:max-w-20 group-hover/edit:opacity-100"
>Edit</span>
</button>
{:else}
<button
type="button"
class="btn btn-sm preset-tonal-surface hover:preset-tonal-warning group/edit transition-all duration-500 hover:duration-200 pl-.05 pr-1.5"
onclick={() => {
$ae_loc.edit_mode = true;
}}
title="Edit mode OFF click to turn on">
<ToggleLeft
size="1em"
class="inline-block text-sm opacity-50" />
<span
class="btn-label max-w-0 overflow-hidden text-xs opacity-0 transition-all duration-700 hover:duration-200 ease-in-out group-hover/edit:max-w-20 group-hover/edit:opacity-100"
>Edit?</span>
</button>
{/if}
{/if}
<!-- MENU EXPAND / COLLAPSE ───────────────────────────────────── -->
<button
type="button"
class="btn btn-sm preset-filled-tertiary-400-600 hover:preset-filled-success group/menu transition-all duration-200"
onclick={toggle_expand}
title={expand ? 'Close menu' : 'Open menu'}>
{#if expand}
<CircleX size="1.1em" class="shrink-0" />
<span
class="btn-label max-w-0 overflow-hidden text-xs opacity-0 transition-all duration-300 ease-in-out group-hover/menu:max-w-20 group-hover/menu:opacity-100"
>Close</span>
{:else}
<Menu size="1.1em" class="shrink-0" />
<span
class="btn-label max-w-0 overflow-hidden text-xs opacity-0 transition-all duration-300 ease-in-out group-hover/menu:max-w-20 group-hover/menu:opacity-100"
>Menu</span>
{/if}
</button>
</div>
</div>
<!-- END: Compact bar -->
</div>
<!-- {/if} -->
<style>
/*
* Strip blue backgrounds and fixed widths from sub-components when
* rendered inside the sys panel. The panel provides its own white/dark
* background and full-width layout — the sub-components' default styling
* was designed for standalone use and clashes inside the panel.
*/
:global(.ae_sys_panel .ae_access_type),
:global(.ae_sys_panel .ae_sign_in_out) {
background: transparent !important;
border: none !important;
width: 100% !important;
max-width: 100% !important;
padding: 0 !important;
}
/*
* Fix native browser rendering of <select> options in dark mode.
* Without color-scheme, the browser renders <option> elements with its
* default (light) chrome even on a dark background — white text on
* white options, or dark text on dark options.
* color-scheme: dark tells the browser to flip its native UI to dark.
* Applied globally to the panel so it covers sub-components (URL builder,
* access type, etc.) without touching every individual element.
*/
:global(.ae_sys_panel) {
color-scheme: light;
}
:global(.dark .ae_sys_panel) {
color-scheme: dark;
}
/*
* Dark mode: Skeleton UI v3 form classes (select, input) do not carry
* dark-mode color tokens, so without these overrides the elements render
* with a light/white background and dark text — unreadable on the dark
* panel background. These rules force a consistent dark appearance for
* every select and text-type input inside the panel (sub-components
* included) without needing per-element dark: classes.
*/
:global(.dark .ae_sys_panel select),
:global(.dark .ae_sys_panel option) {
color: rgb(243 244 246); /* gray-100 */
background-color: rgb(55 65 81); /* gray-700 */
border-color: rgb(75 85 99); /* gray-600 */
}
:global(.dark .ae_sys_panel input:not([type='checkbox']):not([type='radio'])) {
color: rgb(243 244 246); /* gray-100 */
background-color: rgb(55 65 81); /* gray-700 */
border-color: rgb(75 85 99); /* gray-600 */
}
/* Placeholder text should still be legible at reduced opacity */
:global(.dark .ae_sys_panel input::placeholder) {
color: rgb(156 163 175); /* gray-400 */
}
</style>