feat(sys-menu): add URL param builder to app config panel

New e_app_url_builder.svelte component lets admins construct and copy
shareable URLs with any combination of core global params (iframe, theme,
theme_mode, key). Outputs full URL by default; toggleable to params-only
string for pasting onto existing links. Integrated into e_app_cfg Utilities
section (visible in edit mode).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-13 15:27:19 -04:00
parent 456674cc3e
commit a3de95629a
2 changed files with 173 additions and 0 deletions

View File

@@ -3,6 +3,7 @@
import { ae_util } from '$lib/ae_utils/ae_utils';
import { ae_loc, ae_sess, ae_api, slct, slct_trigger } from '$lib/stores/ae_stores';
import E_app_url_builder from '$lib/app_components/e_app_url_builder.svelte';
// import Element_theme from '$lib/e_app_theme.svelte';
@@ -230,6 +231,10 @@
</div>
</section>
<!-- END: Utilities -->
<section class="space-y-2">
<E_app_url_builder />
</section>
</div>
<!-- class:justify-between={expand}

View File

@@ -0,0 +1,168 @@
<script lang="ts">
/**
* e_app_url_builder.svelte
* Core URL Param Builder — lets admins construct and copy shareable URLs
* with any combination of the core global URL params applied.
*
* Core params:
* ?iframe=true|false — hide sys/debug menus (kiosk mode)
* ?theme=<name> — set theme name on load (stripped from URL after apply)
* ?theme_mode=light|dark — set light/dark mode on load (stripped from URL after apply)
* ?key=<access_key> — site access key
*/
import { page } from '$app/stores';
import { Copy, Check, Link } from '@lucide/svelte';
// --- Per-param: include this param in the output URL?
let use_iframe = $state(false);
let use_theme = $state(false);
let use_theme_mode = $state(false);
let use_key = $state(false);
// --- Param values
let val_iframe = $state<'true' | 'false'>('true');
let val_theme = $state('nouveau');
let val_theme_mode = $state<'light' | 'dark'>('dark');
let val_key = $state('');
// Build the output URL reactively
let built_url = $derived.by(() => {
const base = $page.url;
const u = new URL(base.pathname + base.search + base.hash, base.origin);
// Remove all core params first so we start clean each time
u.searchParams.delete('iframe');
u.searchParams.delete('theme');
u.searchParams.delete('theme_mode');
u.searchParams.delete('key');
if (use_iframe) u.searchParams.set('iframe', val_iframe);
if (use_theme) u.searchParams.set('theme', val_theme);
if (use_theme_mode) u.searchParams.set('theme_mode', val_theme_mode);
if (use_key && val_key.trim()) u.searchParams.set('key', val_key.trim());
return u.toString();
});
// Output mode: full URL (default) or params-only string
let params_only = $state(false);
let output = $derived.by(() => {
if (!params_only) return built_url;
const u = new URL(built_url);
return u.search || '(no params set)';
});
let copied = $state(false);
function copy_url() {
navigator.clipboard.writeText(output).then(() => {
copied = true;
setTimeout(() => copied = false, 2000);
});
}
const theme_options = [
{ 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>
<section class="space-y-3">
<h2 class="text-xs font-semibold uppercase tracking-widest text-surface-500 flex items-center gap-1">
<Link size="0.9em" /> URL Param Builder
</h2>
<!-- Param toggles -->
<div class="space-y-2">
<!-- iframe -->
<div class="flex items-center gap-2">
<input id="ubld_iframe" type="checkbox" class="checkbox checkbox-sm" bind:checked={use_iframe} />
<label for="ubld_iframe" class="w-20 text-xs font-mono">iframe</label>
<select bind:value={val_iframe} disabled={!use_iframe} class="select select-sm text-xs flex-1">
<option value="true">true</option>
<option value="false">false</option>
</select>
</div>
<!-- theme -->
<div class="flex items-center gap-2">
<input id="ubld_theme" type="checkbox" class="checkbox checkbox-sm" bind:checked={use_theme} />
<label for="ubld_theme" class="w-20 text-xs font-mono">theme</label>
<select bind:value={val_theme} disabled={!use_theme} class="select select-sm text-xs flex-1">
{#each theme_options as opt}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
</div>
<!-- theme_mode -->
<div class="flex items-center gap-2">
<input id="ubld_theme_mode" type="checkbox" class="checkbox checkbox-sm" bind:checked={use_theme_mode} />
<label for="ubld_theme_mode" class="w-20 text-xs font-mono">theme_mode</label>
<select bind:value={val_theme_mode} disabled={!use_theme_mode} class="select select-sm text-xs flex-1">
<option value="light">light</option>
<option value="dark">dark</option>
</select>
</div>
<!-- key -->
<div class="flex items-center gap-2">
<input id="ubld_key" type="checkbox" class="checkbox checkbox-sm" bind:checked={use_key} />
<label for="ubld_key" class="w-20 text-xs font-mono">key</label>
<input
type="text"
bind:value={val_key}
disabled={!use_key}
placeholder="access key"
class="input input-sm text-xs font-mono flex-1"
/>
</div>
</div>
<!-- params_only toggle + output -->
<div class="flex items-center gap-2">
<input id="ubld_params_only" type="checkbox" class="checkbox checkbox-sm" bind:checked={params_only} />
<label for="ubld_params_only" class="text-xs text-surface-500">Params only</label>
</div>
<!-- Output URL -->
<div class="flex gap-1 items-stretch">
<input
type="text"
readonly
value={output}
class="input input-sm text-xs font-mono flex-1 bg-surface-50/50 dark:bg-surface-700/50 cursor-text"
onclick={(e) => (e.target as HTMLInputElement).select()}
title="Click to select all"
/>
<button
class="btn btn-sm {copied ? 'preset-filled-success' : 'preset-tonal-primary'} shrink-0 transition-all"
onclick={copy_url}
title="Copy URL to clipboard"
>
{#if copied}
<Check size="1em" />
{:else}
<Copy size="1em" />
{/if}
</button>
</div>
</section>