709 lines
35 KiB
Svelte
709 lines
35 KiB
Svelte
<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>
|