diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index 296cd51..847f5d6 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -95,6 +95,25 @@ cards collapse from four columns to two and then one; hover color alone never replaces their textual destination labels, focus-visible treatment, or pressed feedback. +Authenticated desktop account destinations are consolidated into one +agent-status menu at the right edge of the global header. Its trigger combines +the agent avatar, display name, online state, unread count, and an explicit +expanded marker. The menu contains My Profile, Notifications, and Sign Out; +these actions are not duplicated as standalone desktop header links. The menu +uses the same square, compact control-surface treatment as the rest of the +header, with orange reserved for the open/current state and unread count and +danger color reserved for the Sign Out hover state. + +The account trigger exposes its expanded and controlled-menu state. Opening the +menu moves keyboard focus to its first destination; Escape closes it and +returns focus to the trigger, while an outside click dismisses it. Mobile keeps +Profile, Notifications, and Sign Out as explicit navigation actions and shows +the avatar in the signed-in identity block. Avatar imagery supplements rather +than replaces the textual identity, state, and unread indicators. User avatars +are always rendered as circular portraits with the restrained orange SHD ring +used by the Agent Profile; the source image remains square and uses +`object-fit: cover` so authenticated surfaces crop it consistently. + Contact notifications use the mandatory ISAC visual identity because they are operational inbound messages. The notification carries a concise preview and a specific action to an authorization-checked full record. Full messages never @@ -1392,3 +1411,34 @@ Weapon 1 · Top, and Weapon 2 · Bottom. Always include the Top/Bottom context when presenting numbered Weapon Talent slots to users. The filter remains additive within the group and wraps to two columns on narrow screens; do not introduce search or dropdown controls for this four-option set. + +Notification delivery preferences are a temporary profile control surface. +After a successful save, close the interface and return focus to the inbox +context. Consume a unique server result token only once so later actions or +reopening the panel do not close it again. Validation and server errors keep +the interface open with the submitted controls available for correction. + +### Global toast feedback + +Use the root-mounted `ToastViewport` and the shared API in `src/lib/toast.ts` +for transient action feedback. Enhanced server mutations should normally use +`PendingForm`, which derives success and failure feedback from the action +result. Client-only operations call `toast.success`, `toast.error`, +`toast.warning`, or `toast.info` directly. Do not create page-specific toast +implementations. + +Toasts occupy the top-right safe area below the global header and show no more +than four messages. Each uses a textual state label, tone-colored top and left +rails, a concise title, optional supporting text, and an explicit dismiss +control. Success is green, errors use danger red, warnings use SHD orange, and +informational feedback uses restrained blue-gray. Color never carries state +alone. + +Use success to confirm completed mutations, error for failed requests, warning +when attention is required, and info for neutral client or system state. Keep +inline validation beside affected fields even when an error toast is also +emitted. Avoid toasts for passive navigation, filtering, hover, unpersisted +selection changes, and background refreshes. Toasts automatically dismiss +according to severity, remain keyboard dismissible, use `status` or `alert` +semantics, and disable entrance motion with reduced motion. On mobile they span +the safe viewport width below the header without obscuring navigation. diff --git a/src/app.css b/src/app.css index 9c4a944..37e4680 100644 --- a/src/app.css +++ b/src/app.css @@ -214,5 +214,14 @@ .database-category-page{overflow-x:clip}.database-category-command{width:100%;height:auto;min-height:132px}.database-category-command>*{min-width:0}.database-category-command h1{overflow-wrap:anywhere}.database-category-nav{min-width:0}.database-category-nav>a{min-width:0;grid-template-columns:auto minmax(0,1fr) auto}.community-index-hero{height:auto} .database-item-icon-groups{display:grid;gap:8px;margin:8px 12px 0}.database-item-icon-groups>div{display:grid;grid-template-columns:110px minmax(0,1fr);align-items:center;gap:9px;padding-top:8px;border-top:1px solid var(--shd-border)}.database-item-icon-groups>div>span{color:#626a6c;font-size:5px;font-weight:900;letter-spacing:.65px}.item-icon-rail{min-width:0;display:flex;flex-wrap:wrap;gap:5px}.item-icon-rail>button{width:34px;height:34px;display:grid;place-items:center;padding:0;border:1px solid #394041;background:#0d1011;cursor:pointer}.item-icon-rail>button:hover,.item-icon-rail>button:focus-visible{border-color:var(--shd-orange);background:#211a0e}.item-icon-rail>button:active{transform:translateY(1px);filter:brightness(.82)}.item-icon-rail img{width:30px;height:30px;object-fit:contain}.item-icon-rail small{color:#687073;font-size:6px;font-weight:900;letter-spacing:.55px}.database-preview-backdrop{position:fixed;z-index:1200;inset:0;display:grid;place-items:center;padding:20px;background:rgba(3,5,5,.82);backdrop-filter:blur(3px)}.database-preview-dialog{width:min(520px,100%);max-height:calc(100vh - 40px);overflow:auto;border:1px solid #4b5254;border-top:2px solid var(--shd-orange);background:#0b0e0f;box-shadow:0 18px 60px #000}.database-preview-dialog>header{height:auto;display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid var(--shd-border);background:#15191a}.database-preview-dialog>header>div{display:grid;gap:3px}.database-preview-dialog>header small{color:#697174;font-size:5px;letter-spacing:.8px}.database-preview-dialog>header strong{font-size:10px;letter-spacing:.45px}.database-preview-dialog>header button{width:32px;height:32px;padding:0;border:1px solid #434a4c;background:#101314;color:#aab0b2;font-size:20px;line-height:1;cursor:pointer}.database-preview-dialog>header button:hover,.database-preview-dialog>header button:focus-visible{border-color:var(--shd-orange);color:var(--shd-orange)}.database-preview-dialog>.database-item-card{margin:10px;border-top-color:#404748} @media(max-width:1200px){.database-category-nav{grid-template-columns:repeat(4,1fr)}.database-category-nav>a:nth-child(5){border-left:0}.database-category-nav>a:nth-child(n+5){border-top:1px solid var(--shd-border)}.database-facet-groups{grid-template-columns:1fr 1fr}.database-facet-group:first-child{grid-column:1/-1}.database-item-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:700px){.database-category-command{align-items:stretch;flex-direction:column}.database-category-command>div:last-child{justify-items:start}.database-category-nav{grid-template-columns:repeat(2,1fr)}.database-category-nav>a:nth-child(odd){border-left:0}.database-category-nav>a:nth-child(n+3){border-top:1px solid var(--shd-border)}.database-facet-panel>header{align-items:flex-start;flex-direction:column}.database-facet-groups{grid-template-columns:1fr}.database-facet-group:first-child{grid-column:auto}.database-facet-group>div,.database-facet-groups.compact .database-facet-group>div{grid-template-columns:repeat(2,minmax(0,1fr))}.database-item-grid{grid-template-columns:1fr}.database-item-card>header{grid-template-columns:52px minmax(0,1fr)}.database-item-icon{width:52px;height:52px}.database-item-icon :is(img,.game-icon){width:46px;height:46px}}@media(prefers-reduced-motion:reduce){.database-registry-grid>a{transition:none}} + +/* Authenticated desktop identity and account actions */ +.desktop-agent-menu{position:relative}.site-header .desktop-agent-trigger{display:grid;grid-template-columns:36px minmax(110px,1fr) auto 12px;align-items:center;gap:10px;min-width:218px;min-height:50px;padding:6px 9px;border:1px solid var(--shd-border);background:#111415;color:var(--shd-text);text-align:left}.site-header .desktop-agent-trigger:hover,.site-header .desktop-agent-trigger:focus-visible,.site-header .desktop-agent-trigger[aria-expanded=true]{border-color:var(--shd-orange);background:#171b1c;color:var(--shd-text)}.desktop-agent-trigger>img,.desktop-agent-panel header>img,.mobile-agent-state>img{width:36px;height:36px;border:1px solid var(--shd-border);background:#191d1e;object-fit:cover}.desktop-agent-trigger>span{display:grid;min-width:0;line-height:1}.desktop-agent-trigger>span small{color:var(--shd-text-muted);font-size:6px;letter-spacing:1px}.desktop-agent-trigger>span b{overflow:hidden;margin-top:5px;font-size:10px;letter-spacing:1px;text-overflow:ellipsis;text-transform:uppercase;white-space:nowrap}.desktop-agent-trigger>em{display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:22px;padding:0 5px;border:1px solid #a86d16;background:#241b0d;color:var(--shd-orange);font-size:7px;font-style:normal;font-weight:900}.desktop-agent-trigger>strong{color:var(--shd-orange);font:800 14px/1 monospace;text-align:center}.desktop-agent-panel{position:absolute;z-index:80;top:calc(100% + 10px);right:0;width:min(290px,calc(100vw - 32px));border:1px solid #4a5051;background:#0f1213;box-shadow:0 18px 46px #000a}.desktop-agent-panel:before{position:absolute;top:-6px;right:18px;width:10px;height:10px;border-top:1px solid var(--shd-orange);border-left:1px solid var(--shd-orange);background:#111415;content:"";transform:rotate(45deg)}.desktop-agent-panel header{height:auto;min-height:68px;display:grid;grid-template-columns:44px minmax(0,1fr);align-items:center;gap:12px;padding:11px 12px;border-bottom:1px solid var(--shd-border);background:linear-gradient(90deg,#281e0e66,transparent 70%),#111415}.desktop-agent-panel header>img{width:44px;height:44px}.desktop-agent-panel header>div{display:grid;min-width:0;gap:3px}.desktop-agent-panel header small,.desktop-agent-panel header span{color:var(--shd-text-muted);font-size:6px;letter-spacing:1px}.desktop-agent-panel header b{overflow:hidden;font-size:11px;letter-spacing:1px;text-overflow:ellipsis;text-transform:uppercase;white-space:nowrap}.desktop-agent-panel>a{min-height:53px;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;padding:8px 12px;border-bottom:1px solid var(--shd-border-soft);color:var(--shd-text)}.desktop-agent-panel>a>span{font-size:8px;font-weight:900;letter-spacing:1px}.desktop-agent-panel>a>b{color:var(--shd-text-muted);font-size:6px;letter-spacing:.7px}.desktop-agent-panel>a:hover,.desktop-agent-panel>a:focus-visible,.desktop-agent-panel>a.active{background:#171b1c;color:var(--shd-orange)}.desktop-agent-panel>a:hover>b,.desktop-agent-panel>a:focus-visible>b,.desktop-agent-panel>a.active>b{color:var(--shd-orange)}.desktop-agent-panel form,.desktop-agent-panel fieldset{margin:0;padding:0;border:0}.site-header .desktop-agent-panel .desktop-agent-sign-out{width:100%;min-height:45px;padding:8px 12px;border:0;background:#111415;color:var(--shd-text-muted);font-size:7px;font-weight:900;letter-spacing:1px;text-align:right;cursor:pointer}.site-header .desktop-agent-panel .desktop-agent-sign-out:hover,.site-header .desktop-agent-panel .desktop-agent-sign-out:focus-visible{background:#211514;color:var(--shd-danger)}.site-header .desktop-agent-panel button:disabled{cursor:wait;opacity:.62}@media(max-width:900px){.mobile-agent-state{grid-template-columns:36px minmax(0,1fr);column-gap:10px}.mobile-agent-state>img{grid-row:1/3;align-self:center}.mobile-agent-state>span,.mobile-agent-state>strong{grid-column:2}}@media(prefers-reduced-motion:reduce){.desktop-agent-trigger,.desktop-agent-panel a,.desktop-agent-panel button{transition:none}} + +.desktop-agent-trigger>img,.desktop-agent-panel header>img,.mobile-agent-state>img{border-color:var(--shd-orange);border-radius:50%;box-shadow:0 0 0 3px #0a0c0d,0 0 0 4px #f6a82144} +@media(max-width:900px){.mobile-navigation{overflow-x:hidden}.mobile-navigation>a span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}} + +/* Global action feedback */ +.toast-viewport{position:fixed;z-index:1000;top:calc(var(--shd-header-height) + 12px);right:clamp(12px,2vw,28px);width:min(390px,calc(100vw - 24px));display:grid;gap:8px;pointer-events:none}.toast-message{--toast-tone:#829093;position:relative;min-height:72px;display:grid;grid-template-columns:4px minmax(0,1fr) 34px;overflow:hidden;border:1px solid #414849;border-top:2px solid var(--toast-tone);background:linear-gradient(105deg,#171b1cee,#0c0f10f7);box-shadow:0 12px 30px #000b;color:var(--shd-text);pointer-events:auto;animation:toast-enter .18s ease-out}.toast-message.success{--toast-tone:var(--shd-success)}.toast-message.error{--toast-tone:var(--shd-danger)}.toast-message.warning{--toast-tone:var(--shd-orange)}.toast-message.info{--toast-tone:#6d9daf}.toast-message>i{width:4px;height:100%;background:var(--toast-tone);box-shadow:0 0 12px color-mix(in srgb,var(--toast-tone) 50%,transparent)}.toast-message>div{min-width:0;display:grid;align-content:center;gap:4px;padding:11px 10px}.toast-message span{color:var(--toast-tone);font-size:6px;font-weight:900;letter-spacing:1px}.toast-message strong{overflow-wrap:anywhere;font-size:9px;line-height:1.3;letter-spacing:.3px}.toast-message p{margin:0;color:#91999b;font-size:7px;line-height:1.45}.toast-message>button{width:34px;height:34px;margin:4px 3px 0 0;border:0;background:transparent;color:#70787a;font-size:17px;cursor:pointer}.toast-message>button:hover,.toast-message>button:focus-visible{background:#ffffff09;color:var(--toast-tone)}.toast-message>button:active{transform:translateY(1px);filter:brightness(.8)}@keyframes toast-enter{from{opacity:0;transform:translateX(18px)}to{opacity:1;transform:none}}@media(max-width:700px){.toast-viewport{top:calc(var(--shd-header-height) + 8px);right:8px;width:calc(100vw - 16px)}.toast-message{min-height:68px}}@media(prefers-reduced-motion:reduce){.toast-message{animation:none}} .community-metrics{gap:38px}.community-metrics strong{color:var(--shd-orange);font-size:15px;letter-spacing:1px}.community-preview .gear-node{width:98px;color:var(--shd-text)}.community-preview .gear-node:hover,.community-preview .gear-node:focus-visible{border-color:var(--shd-orange);background:#211a0e}.community-preview .gear-node:active{transform:translateY(1px);filter:brightness(.86)}.community-preview .gear-node b{font-size:7px;letter-spacing:.55px}.community-channels{padding:58px max(24px,5vw) 70px;border-top:1px solid var(--shd-border);background:#0c0f10}.community-channels>.section-heading{max-width:1240px}.community-channels>.section-heading>p{max-width:310px;margin:0;color:var(--shd-text-muted);font:12px/1.5 Georgia,serif;text-align:right}.community-channel-grid{max-width:1240px;display:grid;grid-template-columns:repeat(4,1fr);margin:auto;border:1px solid var(--shd-border)}.community-channel-grid>a{min-height:205px;display:flex;flex-direction:column;padding:20px;border-left:1px solid var(--shd-border);background:#111415;color:var(--shd-text)}.community-channel-grid>a:first-child{border-left:0}.community-channel-grid>a:hover,.community-channel-grid>a:focus-visible{background:#171b1c;box-shadow:inset 0 2px var(--shd-orange)}.community-channel-grid>a:active{filter:brightness(.84)}.community-channel-grid small{color:#626a6c;font-size:6px;letter-spacing:1px}.community-channel-grid strong{margin:18px 0 9px;font-size:14px;letter-spacing:.6px}.community-channel-grid span{color:#858d8f;font:11px/1.55 Georgia,serif}.community-channel-grid b{margin-top:auto;padding-top:17px;color:var(--shd-orange);font-size:7px;letter-spacing:.8px}.community-index-page{padding-top:34px}.community-index-hero{margin-bottom:12px}.community-database-intro{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px}.community-database-intro>div{display:grid;grid-template-columns:150px 1fr 1.4fr;align-items:center;gap:12px;min-height:65px;padding:10px 13px;border:1px solid var(--shd-border);background:#101314}.community-database-intro span{color:#666e70;font-size:6px;letter-spacing:1px}.community-database-intro strong{color:var(--shd-orange);font-size:8px;letter-spacing:.7px}.community-database-intro small{color:#7f8789;font:10px/1.45 Georgia,serif}.database-registry-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.database-registry-grid article{min-height:180px;display:flex;flex-direction:column;padding:17px;border:1px solid var(--shd-border);border-top:2px solid #43494a;background:#111415}.database-registry-grid article:hover{border-top-color:var(--shd-orange);background:#15191a}.database-registry-grid header{height:auto;display:flex;justify-content:space-between}.database-registry-grid header small{color:#596164;font-size:6px;letter-spacing:.8px}.database-registry-grid header b{color:var(--shd-orange);font-size:20px}.database-registry-grid h2{margin:18px 0 8px;font-size:16px}.database-registry-grid p{color:#858d8f;font:11px/1.55 Georgia,serif}.database-registry-grid>article>span{margin-top:auto;color:#687073;font-size:6px;font-weight:900;letter-spacing:.8px}.news-channel-grid{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:10px}.news-primary{min-height:390px;padding:30px;border:1px solid var(--shd-border);border-top:2px solid var(--shd-orange);background:linear-gradient(135deg,#151819,#0d1011)}.news-primary>div:first-child{display:flex;justify-content:space-between;padding-bottom:13px;margin-bottom:30px;border-bottom:1px solid var(--shd-border);color:#626a6c;font-size:6px;letter-spacing:1px}.news-primary>div:first-child b{color:var(--shd-orange)}.news-primary h2{max-width:720px;margin:19px 0;font-size:clamp(30px,4vw,52px);line-height:.95}.news-primary>p:not(.eyebrow){max-width:760px;color:#969d9f;font:14px/1.7 Georgia,serif}.news-primary footer{display:flex;gap:7px;margin-top:35px}.news-primary footer span{display:grid;padding:8px 10px;border:1px solid var(--shd-border);color:#626a6c;font-size:6px;letter-spacing:.8px}.news-primary footer b{margin-top:4px;color:#d5d8d5;font-size:8px}.news-channel-grid>aside{padding:19px;border:1px solid var(--shd-border);background:#111415}.news-channel-grid>aside>div{min-height:58px;display:grid;grid-template-columns:30px 1fr auto;align-items:center;border-top:1px solid var(--shd-border)}.news-channel-grid>aside small{color:#596164;font-size:7px}.news-channel-grid>aside span{font-size:8px;letter-spacing:.7px}.news-channel-grid>aside b{color:#6e7678;font-size:6px}@media(max-width:1100px){.community-channel-grid{grid-template-columns:1fr 1fr}.community-channel-grid>a:nth-child(3){border-left:0;border-top:1px solid var(--shd-border)}.community-channel-grid>a:nth-child(4){border-top:1px solid var(--shd-border)}.community-database-intro{grid-template-columns:1fr}.database-registry-grid{grid-template-columns:1fr 1fr}}@media(max-width:750px){.community-channels>.section-heading>p{display:none}.community-channel-grid,.database-registry-grid,.news-channel-grid{grid-template-columns:1fr}.community-channel-grid>a{min-height:155px;border-left:0;border-top:1px solid var(--shd-border)}.community-channel-grid>a:first-child{border-top:0}.community-database-intro>div{grid-template-columns:1fr}.news-primary{padding:22px}.news-primary footer{flex-wrap:wrap}}@media(max-width:430px){.site-header .brand-name small{display:block;font-size:4px;letter-spacing:.7px}} diff --git a/src/lib/components/AppShell.svelte b/src/lib/components/AppShell.svelte index 635c5b5..a25c526 100644 --- a/src/lib/components/AppShell.svelte +++ b/src/lib/components/AppShell.svelte @@ -5,7 +5,7 @@ import type { Snippet } from 'svelte'; let { user, isacAccess, notificationSummary, children }: { - user: { username: string; displayName: string; role: string } | null; + user: { username: string; displayName: string; role: string; avatarUrl: string } | null; isacAccess: boolean; notificationSummary: { unread: number }; children: Snippet; @@ -22,6 +22,9 @@ let showCookieNotice = $state(false); let mobileOpen = $state(false); let mobilePanel = $state(null); + let accountOpen = $state(false); + let accountButton = $state(null); + let accountMenu = $state(null); let unreadNotifications = $state(0); $effect(() => { unreadNotifications = notificationSummary.unread; }); @@ -70,15 +73,33 @@ event.preventDefault(); closeMobileNavigation(); } + if (event.key === 'Escape' && accountOpen) { + event.preventDefault(); + accountOpen = false; + accountButton?.focus(); + } + } + + async function toggleAccountMenu() { + accountOpen = !accountOpen; + if (accountOpen) { + await tick(); + accountMenu?.querySelector('a')?.focus(); + } + } + + function closeAccountFromOutside(event: MouseEvent) { + if (accountOpen && !(event.target as Element).closest('.desktop-agent-menu')) accountOpen = false; } $effect(() => { page.url.pathname; mobileOpen = false; + accountOpen = false; }); - +