Files
OSIT-AE-App-Svelte/src/lib/app_components/e_app_sys_bar.svelte

709 lines
35 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,
LogOut,
Menu,
Moon,
Sun,
ShieldEllipsis,
ShieldMinus,
ShieldUser,
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_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)
══════════════════════════════════════════════════════════════════════════ -->
<div
class="
ae_sys_bar
print:hidden
fixed bottom-12 right-2
z-50
flex flex-col items-end gap-1
"
class:hidden={hide}
>
<!-- ── EXPANDED PANEL ──────────────────────────────────────────────────── -->
{#if expand}
<div
class="
ae_sys_panel
flex flex-col items-stretch gap-0
w-80 max-w-[94vw]
max-h-[80vh] overflow-y-auto overflow-x-hidden
bg-white/85 dark:bg-gray-900/85 text-gray-900 dark:text-gray-100 backdrop-blur-md
border border-gray-200/70 dark:border-gray-700/70
rounded-xl shadow-2xl
text-sm
"
>
<!-- Panel header: person info + close -->
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-100/70 dark:border-gray-800/70 sticky top-0 bg-white/85 dark:bg-gray-900/85 backdrop-blur-md z-10">
<div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 min-w-0">
{#if $ae_loc?.person_id}
<User size="0.9em" class="shrink-0" />
<span class="truncate">{person_display ?? '—'}</span>
{/if}
{#if access_label}
<span class="font-semibold text-primary-600 dark:text-primary-400 shrink-0">
· {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 transition-all shrink-0 ml-2"
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="w-full flex items-center justify-between px-3 py-2 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
onclick={() => sec_signin = !sec_signin}
>
<span class="flex items-center gap-1.5 min-w-0">
{#if $ae_loc?.person_id && $ae_loc?.user_id}
<LogOut size="0.85em" class="opacity-60 shrink-0" />
<span>Sign Out</span>
<span class="normal-case font-normal opacity-70 truncate">
{$ae_loc?.user?.username ?? person_display ?? ''}
</span>
{:else}
<ShieldUser size="0.85em" class="opacity-60 shrink-0" />
<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="w-full flex items-center justify-between px-3 py-2 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
onclick={() => sec_access = !sec_access}
>
<span class="flex items-center gap-1.5 min-w-0">
{#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="opacity-60 shrink-0" />
<span>Elevated Access</span>
<span class="normal-case font-normal text-primary-600 dark:text-primary-400 truncate">({access_label})</span>
{:else}
<ShieldEllipsis size="0.85em" class="opacity-60 shrink-0" />
<span>Access</span>
<span class="normal-case font-normal text-primary-600 dark:text-primary-400 truncate">({access_label})</span>
{/if}
{:else}
<ShieldUser size="0.85em" class="opacity-60 shrink-0" />
<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="w-full flex items-center justify-between px-3 py-2 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
onclick={() => sec_appearance = !sec_appearance}
>
<span class="flex items-center gap-1">
<span class="fas fa-palette opacity-60"></span>
Appearance
<span class="normal-case font-normal opacity-60 ml-1">
({$ae_loc?.theme_name ?? '—'} · {$ae_loc?.theme_mode ?? '—'})
</span>
</span>
<span class="opacity-50">{sec_appearance ? '▲' : '▼'}</span>
</button>
{#if sec_appearance}
<div class="px-3 pb-3 pt-1 space-y-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="text-xs ml-1">Dark</span>
{:else}
<Sun size="1em" class="shrink-0" />
<span class="text-xs ml-1">Light</span>
{/if}
</button>
<button
type="button"
class="btn btn-sm flex-1 preset-tonal-surface hover:preset-tonal-primary transition-all"
onclick={cycle_font_size}
title={font_title}
>
<span class="font-bold text-sm leading-none">{font_label}</span>
<span class="text-xs ml-1 opacity-70">Font</span>
</button>
</div>
<!-- Theme name select -->
<div class="space-y-1.5">
<div class="text-xs text-gray-400 dark:text-gray-500 font-medium">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 text-sm text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600"
>
{#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="w-full flex items-center justify-between px-3 py-2 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
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="px-3 pb-3 space-y-2">
<!-- iframe mode toggle -->
{#if $ae_loc.iframe}
<a class="btn btn-sm preset-tonal-secondary w-full justify-end" href="/?iframe=false">
<span class="fas fa-compress-arrows-alt opacity-60"></span>
Exit iframe Mode
</a>
{:else}
<a class="btn btn-sm preset-tonal-secondary w-full justify-end" href="/?iframe=true">
<span class="fas fa-expand-arrows-alt opacity-60"></span>
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"
>
<span class="fas fa-sync opacity-60"></span>
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"
>
<span class="fas fa-eraser opacity-60"></span>
Clear Storage & DB
</button>
<!-- URL param builder -->
<div class="pt-1 border-t border-gray-100 dark:border-gray-800">
<E_app_url_builder />
</div>
<!-- Debug overlay toggle -->
<div class="pt-1 border-t border-gray-100 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="text-xs ml-1">
{$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="relative group">
<!-- Hover info strip — always in DOM (no mount flash), opacity-only transition.
pointer-events-none so it never blocks clicks. -->
<div
class="
absolute bottom-full right-0 mb-1
pointer-events-none
transition-opacity duration-200 delay-500
flex items-center gap-1.5
bg-white/30 dark:bg-gray-900/30
border border-gray-200/60 dark:border-gray-700/60
backdrop-blur-sm rounded-lg
px-2 py-1
text-xs text-gray-500 dark:text-gray-400
whitespace-nowrap shadow-md
"
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="font-semibold text-primary-600 dark:text-primary-400">
· {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 flex-row items-center gap-1
h-9
bg-white/30 dark:bg-gray-900/30
backdrop-blur-sm
border border-gray-200/60 dark:border-gray-700/60
rounded-xl
px-2
shadow-lg
transition-colors duration-300
"
class:border-primary-400={expand}
>
<!-- AUTH STATUS SHIELD ───────────────────────────────────────── -->
<button
type="button"
class="
btn btn-sm transition-all duration-200 group/shield
{$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 opacity-0 group-hover/shield:max-w-20 group-hover/shield:opacity-100 transition-all duration-300 ease-in-out text-xs pl-1">
{access_label ?? 'Auth?'}
</span>
</button>
<!-- FONT SIZE CYCLER ─────────────────────────────────────────── -->
<button
type="button"
class="btn btn-sm preset-tonal-surface hover:preset-tonal-primary transition-all duration-200 group/font"
onclick={cycle_font_size}
title={font_title}
>
<span class="font-bold leading-none text-sm">{font_label}</span>
<span class="btn-label max-w-0 overflow-hidden opacity-0 group-hover/font:max-w-20 group-hover/font:opacity-100 transition-all duration-300 ease-in-out text-xs pl-1">Font</span>
</button>
<!-- DARK / LIGHT TOGGLE ──────────────────────────────────────── -->
<button
type="button"
class="btn btn-sm preset-tonal-surface hover:preset-tonal-secondary transition-all duration-200 group/mode"
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 opacity-0 group-hover/mode:max-w-20 group-hover/mode:opacity-100 transition-all duration-300 ease-in-out text-xs pl-1">Dark</span>
{:else}
<Sun size="1.1em" class="shrink-0" />
<span class="btn-label max-w-0 overflow-hidden opacity-0 group-hover/mode:max-w-20 group-hover/mode:opacity-100 transition-all duration-300 ease-in-out text-xs pl-1">Light</span>
{/if}
</button>
<!-- EDIT MODE TOGGLE (authenticated+ only) ───────────────────── -->
{#if $ae_loc?.authenticated_access}
{#if $ae_loc.edit_mode}
<button
type="button"
class="btn btn-sm preset-tonal-warning hover:preset-tonal-success transition-all duration-200 group/edit"
onclick={() => { $ae_loc.edit_mode = false; }}
title="Edit mode ON click to turn off"
>
<span class="fas fa-toggle-on text-sm inline-block"></span>
<span class="btn-label max-w-0 overflow-hidden opacity-0 group-hover/edit:max-w-20 group-hover/edit:opacity-100 transition-all duration-300 ease-in-out text-xs pl-1">Edit</span>
</button>
{:else}
<button
type="button"
class="btn btn-sm preset-tonal-surface hover:preset-tonal-warning transition-all duration-200 group/edit"
onclick={() => { $ae_loc.edit_mode = true; }}
title="Edit mode OFF click to turn on"
>
<span class="fas fa-toggle-off text-sm inline-block opacity-50"></span>
<span class="btn-label max-w-0 overflow-hidden opacity-0 group-hover/edit:max-w-20 group-hover/edit:opacity-100 transition-all duration-300 ease-in-out text-xs pl-1">Edit</span>
</button>
{/if}
{/if}
<!-- MENU EXPAND / COLLAPSE ───────────────────────────────────── -->
<button
type="button"
class="btn btn-sm preset-filled-tertiary-400-600 hover:preset-filled-success transition-all duration-200 group/menu"
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 opacity-0 group-hover/menu:max-w-20 group-hover/menu:opacity-100 transition-all duration-300 ease-in-out text-xs pl-1">Close</span>
{:else}
<Menu size="1.1em" class="shrink-0" />
<span class="btn-label max-w-0 overflow-hidden opacity-0 group-hover/menu:max-w-20 group-hover/menu:opacity-100 transition-all duration-300 ease-in-out text-xs pl-1">Menu</span>
{/if}
</button>
</div>
</div>
<!-- END: Compact bar -->
</div>
<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>