feat: plantilla base workspace SaaS
This commit is contained in:
81
frontend/src/lib/components/sidebar/app-launcher.svelte
Normal file
81
frontend/src/lib/components/sidebar/app-launcher.svelte
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import LayoutGridIcon from '@lucide/svelte/icons/layout-grid';
|
||||
import { workspaceAppsStore, type WorkspaceApp } from '$lib/stores/workspace-apps.svelte';
|
||||
|
||||
// Abrir la app seleccionada:
|
||||
// - Mismo origin (es esta misma app): si trae `active_system`, cambio de sistema local
|
||||
// recargando la página actual con ese parámetro (sin rebotar al Workspace). Si no, ir al dashboard.
|
||||
// - Otro origin: navegar a su entrada (login_url); su propio login resuelve el SSO vía Workspace
|
||||
// y aterriza en la app.
|
||||
function openApp(app: WorkspaceApp) {
|
||||
let target: URL;
|
||||
try {
|
||||
target = new URL(app.url);
|
||||
} catch {
|
||||
window.location.assign(app.url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.origin !== window.location.origin) {
|
||||
window.location.assign(app.url);
|
||||
return;
|
||||
}
|
||||
|
||||
const system = target.searchParams.get('active_system');
|
||||
if (system) {
|
||||
const current = new URL(window.location.href);
|
||||
current.searchParams.set('active_system', system);
|
||||
window.location.assign(current.pathname + current.search);
|
||||
} else {
|
||||
window.location.assign('/dashboard');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<button
|
||||
{...props}
|
||||
class="inline-flex size-10 shrink-0 select-none items-center justify-center rounded-md
|
||||
text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground
|
||||
transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring
|
||||
data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
title="Aplicaciones"
|
||||
aria-label="Abrir selector de aplicaciones"
|
||||
>
|
||||
<LayoutGridIcon class="size-5" />
|
||||
</button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content
|
||||
class="w-64 rounded-xl p-3"
|
||||
align="end"
|
||||
side="bottom"
|
||||
sideOffset={8}
|
||||
>
|
||||
<p class="mb-3 px-1 text-xs font-medium text-muted-foreground">Tus aplicaciones</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{#each workspaceAppsStore.apps as app (app.id)}
|
||||
<button
|
||||
onclick={() => openApp(app)}
|
||||
class="flex flex-col items-center gap-1.5 rounded-lg p-3 text-center transition-colors hover:bg-accent"
|
||||
>
|
||||
<div class="flex size-10 items-center justify-center overflow-hidden rounded-xl bg-primary/10 text-primary">
|
||||
{#if app.iconUrl}
|
||||
<img src={app.iconUrl} alt={app.name} class="size-full object-cover" />
|
||||
{:else}
|
||||
<LayoutGridIcon class="size-5" />
|
||||
{/if}
|
||||
</div>
|
||||
<span class="text-sm font-medium leading-tight">{app.name}</span>
|
||||
{#if app.slug}
|
||||
<span class="text-xs text-muted-foreground">{app.slug}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
Reference in New Issue
Block a user