Improve ISAC libraries, roles, and public profiles
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { agentProfileHref } from '$lib/agents';
|
||||
|
||||
let {
|
||||
agent,
|
||||
size = 'default',
|
||||
@@ -6,7 +8,7 @@
|
||||
showId = false,
|
||||
linked = true
|
||||
}: {
|
||||
agent: { id: string; displayName: string; avatarUrl: string };
|
||||
agent: { id: string; displayName: string; avatarUrl: string; publicSlug?: string | null };
|
||||
size?: 'compact' | 'default' | 'large';
|
||||
meta?: string;
|
||||
showId?: boolean;
|
||||
@@ -15,7 +17,7 @@
|
||||
</script>
|
||||
|
||||
{#if linked}
|
||||
<a class="agent-identity" class:compact={size === 'compact'} class:large={size === 'large'} href={`/agents/${encodeURIComponent(agent.id)}`}>
|
||||
<a class="agent-identity" class:compact={size === 'compact'} class:large={size === 'large'} href={agentProfileHref(agent)}>
|
||||
<img src={agent.avatarUrl} alt="" />
|
||||
<span>
|
||||
<strong>{agent.displayName}</strong>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<article class="database-item-card">
|
||||
<article class="database-item-card" class:talent-card={item.variant === 'talent'}>
|
||||
<header>
|
||||
<div class="database-item-icon">
|
||||
{#if item.gameIcon}<GameIcon name={item.gameIcon} decorative/>{:else}<ItemIcon src={item.iconUrl} alt=""/>{/if}
|
||||
@@ -41,8 +41,15 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if item.description}<p class:data-description={item.descriptionStyle === 'data'}>{item.description}</p>{/if}
|
||||
{#if item.secondaryDescription}<p class="secondary-description" class:data-description={item.descriptionStyle === 'data'}>{item.secondaryDescription}</p>{/if}
|
||||
{#if item.variant === 'talent'}
|
||||
<div class="database-talent-tiers">
|
||||
<section><span>TIER I</span><p>{item.description ?? 'Description is not available.'}</p></section>
|
||||
<section><span>TIER II</span><p>{item.secondaryDescription ?? 'Description is not available.'}</p></section>
|
||||
</div>
|
||||
{:else}
|
||||
{#if item.description}<p class:data-description={item.descriptionStyle === 'data'}>{item.description}</p>{/if}
|
||||
{#if item.secondaryDescription}<p class="secondary-description" class:data-description={item.descriptionStyle === 'data'}>{item.secondaryDescription}</p>{/if}
|
||||
{/if}
|
||||
{#if item.afterLines?.length}
|
||||
<div class="database-item-lines after-description">{#each item.afterLines as line}<div><span>{line.label}</span><b>{line.value}</b></div>{/each}</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { tick } from 'svelte';
|
||||
import DatabaseItemCard from './DatabaseItemCard.svelte';
|
||||
import ItemIcon from './ItemIcon.svelte';
|
||||
import DatabaseFacts from './DatabaseFacts.svelte';
|
||||
import type { DatabaseRelatedItem } from '$lib/database/items';
|
||||
|
||||
let {
|
||||
@@ -13,14 +14,45 @@
|
||||
emptyLabel?: string;
|
||||
} = $props();
|
||||
let selected = $state<DatabaseRelatedItem | null>(null);
|
||||
let trigger = $state<HTMLButtonElement | null>(null);
|
||||
let closeButton = $state<HTMLButtonElement | null>(null);
|
||||
let dialog = $state<HTMLDivElement | null>(null);
|
||||
const openPreview = async (item: DatabaseRelatedItem, button: HTMLButtonElement) => {
|
||||
selected = item;
|
||||
trigger = button;
|
||||
await tick();
|
||||
closeButton?.focus();
|
||||
};
|
||||
const closePreview = async () => {
|
||||
const previousTrigger = trigger;
|
||||
selected = null;
|
||||
trigger = null;
|
||||
await tick();
|
||||
previousTrigger?.focus();
|
||||
};
|
||||
const trapDialogFocus = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'Tab' || !dialog) return;
|
||||
const focusable = [...dialog.querySelectorAll<HTMLElement>('a[href],button:not(:disabled),[tabindex]:not([tabindex="-1"])')]
|
||||
.filter((element) => !element.hasAttribute('hidden'));
|
||||
const first = focusable[0];
|
||||
const last = focusable.at(-1);
|
||||
if (!first || !last) return;
|
||||
if (event.shiftKey && document.activeElement === first) {
|
||||
event.preventDefault();
|
||||
last.focus();
|
||||
} else if (!event.shiftKey && document.activeElement === last) {
|
||||
event.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={(event) => { if (event.key === 'Escape') selected = null; }}/>
|
||||
<svelte:window onkeydown={(event) => { if (event.key === 'Escape' && selected) void closePreview(); }}/>
|
||||
|
||||
<div class="item-icon-rail" aria-label={`${label}: ${items.length ? items.map((item) => item.name).join(', ') : emptyLabel}`}>
|
||||
{#if items.length}
|
||||
{#each items as item}
|
||||
<button type="button" title={`View ${item.name}`} aria-label={`View ${item.name} details`} onclick={() => selected = item}>
|
||||
<button type="button" title={`View ${item.name}`} aria-label={`View ${item.name} details`} aria-haspopup={item.preview ? 'dialog' : undefined} onclick={(event) => void openPreview(item, event.currentTarget)}>
|
||||
<ItemIcon src={item.iconUrl} alt=""/>
|
||||
</button>
|
||||
{/each}
|
||||
@@ -30,22 +62,10 @@
|
||||
</div>
|
||||
|
||||
{#if selected?.preview}
|
||||
<div class="database-preview-backdrop" role="presentation" onclick={(event) => { if (event.target === event.currentTarget) selected = null; }}>
|
||||
<div class="database-preview-dialog" role="dialog" aria-modal="true" aria-labelledby={`database-preview-${selected.id}`}>
|
||||
<header><div><small>DATABASE QUICK VIEW</small><strong id={`database-preview-${selected.id}`}>{selected.preview.name}</strong></div><button type="button" aria-label="Close details" onclick={() => selected = null}>×</button></header>
|
||||
<article class="database-item-card">
|
||||
<header>
|
||||
<div class="database-item-icon"><ItemIcon src={selected.preview.iconUrl} alt=""/></div>
|
||||
<div><small>{selected.preview.kicker}</small><h2>{selected.preview.name}</h2></div>
|
||||
</header>
|
||||
{#if selected.preview.badges?.length}<div class="database-item-badges">{#each selected.preview.badges as badge}<span class={badge.tone ?? ''}>{badge.label}</span>{/each}</div>{/if}
|
||||
<DatabaseFacts facts={selected.preview.facts} layout={selected.preview.factLayout}/>
|
||||
{#if selected.preview.lines?.length}<div class="database-item-lines">{#each selected.preview.lines as line}<div><span>{line.label}</span><b>{line.value}</b></div>{/each}</div>{/if}
|
||||
{#if selected.preview.description}<p class:data-description={selected.preview.descriptionStyle === 'data'}>{selected.preview.description}</p>{/if}
|
||||
{#if selected.preview.secondaryDescription}<p class="secondary-description" class:data-description={selected.preview.descriptionStyle === 'data'}>{selected.preview.secondaryDescription}</p>{/if}
|
||||
{#if selected.preview.afterLines?.length}<div class="database-item-lines after-description">{#each selected.preview.afterLines as line}<div><span>{line.label}</span><b>{line.value}</b></div>{/each}</div>{/if}
|
||||
{#if selected.preview.actions?.length}<footer class="database-item-actions">{#each selected.preview.actions as action}<a href={action.href}>{action.label} →</a>{/each}</footer>{/if}
|
||||
</article>
|
||||
<div class="database-preview-backdrop" role="presentation" onclick={(event) => { if (event.target === event.currentTarget) void closePreview(); }}>
|
||||
<div class="database-preview-dialog" bind:this={dialog} role="dialog" aria-modal="true" aria-labelledby={`database-preview-${selected.id}`} tabindex="-1" onkeydown={trapDialogFocus}>
|
||||
<header><div><small>DATABASE QUICK VIEW</small><strong id={`database-preview-${selected.id}`}>{selected.preview.name}</strong></div><button type="button" bind:this={closeButton} aria-label="Close details" onclick={() => void closePreview()}>×</button></header>
|
||||
<DatabaseItemCard item={selected.preview}/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
roles,
|
||||
label = 'Assigned site roles',
|
||||
compact = false
|
||||
}: {
|
||||
roles: string[];
|
||||
label?: string;
|
||||
compact?: boolean;
|
||||
} = $props();
|
||||
|
||||
const visibleRoles = $derived(
|
||||
[...new Set(roles.map((role) => role.trim()).filter(Boolean))]
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if visibleRoles.length}
|
||||
<ul class="role-badge-list" class:compact aria-label={label}>
|
||||
{#each visibleRoles as role}
|
||||
<li><span aria-hidden="true"></span>{role}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.role-badge-list{min-width:0;display:flex;flex-wrap:wrap;align-items:center;gap:5px;margin:0;padding:0;list-style:none}
|
||||
.role-badge-list li{min-width:0;min-height:27px;display:flex;align-items:center;gap:7px;padding:4px 8px;border:1px solid var(--shd-border);background:var(--shd-surface-raised);color:var(--shd-text);font-size:var(--shd-type-telemetry);font-weight:900;line-height:1.25;letter-spacing:.65px;text-transform:uppercase;overflow-wrap:anywhere}
|
||||
.role-badge-list li span{flex:none;width:5px;height:5px;border:1px solid var(--shd-orange);background:color-mix(in srgb,var(--shd-orange) 35%,transparent);transform:rotate(45deg)}
|
||||
.role-badge-list.compact{gap:3px}.role-badge-list.compact li{min-height:23px;padding:3px 5px;gap:5px;letter-spacing:.55px}.role-badge-list.compact li span{width:4px;height:4px}
|
||||
@media(max-width:600px){.role-badge-list:not(.compact) li{min-height:30px}}
|
||||
</style>
|
||||
Reference in New Issue
Block a user