227 lines
23 KiB
Svelte
227 lines
23 KiB
Svelte
<script lang="ts">
|
||
import PageBreadcrumb from '$lib/components/PageBreadcrumb.svelte';
|
||
import PendingForm from '$lib/components/PendingForm.svelte';
|
||
import ImageUploadInput from '$lib/components/ImageUploadInput.svelte';
|
||
import { browserSupportsWebAuthn, startRegistration } from '@simplewebauthn/browser';
|
||
import { onMount } from 'svelte';
|
||
let { data, form } = $props();
|
||
const user = $derived(data.user!);
|
||
let discordLinkPending = $state(false);
|
||
let discordUnlinkConfirm = $state(false);
|
||
type AccountPanel = 'identity' | 'access' | 'account';
|
||
type AccessPanel = 'passkeys' | 'email' | 'local' | 'connections';
|
||
type AvatarPanel = 'presets' | 'upload';
|
||
function initialAccountPanel(): AccountPanel {
|
||
if (form?.discordUnlinkError || data.oauthError || data.discordLinked || form?.discordUnlinked || form?.passwordError || form?.loginUsernameError || form?.localCredentialError || form?.emailError || form?.passkeyError || data.emailVerified) return 'access';
|
||
if (form?.deleteError) return 'account';
|
||
return 'identity';
|
||
}
|
||
function initialAccessPanel(): AccessPanel {
|
||
if (form?.discordUnlinkError || data.oauthError || data.discordLinked || form?.discordUnlinked) return 'connections';
|
||
if (form?.emailError || form?.emailVerificationSent || form?.emailRemoved || data.emailVerified) return 'email';
|
||
if (form?.passwordError || form?.loginUsernameError || form?.localCredentialError || form?.localCredentialsAdded || form?.localCredentialsRemoved) return 'local';
|
||
return 'passkeys';
|
||
}
|
||
function initialAvatarPanel(): AvatarPanel {
|
||
return form?.avatarError || form?.avatarReviewQueued ? 'upload' : 'presets';
|
||
}
|
||
const notificationIsIsac = (notification: any) => notification.category === 'isac' || String(notification.source).startsWith('ISAC');
|
||
const notificationTone = (notification: any) => {
|
||
const text = `${notification.title} ${notification.body}`.toLocaleLowerCase();
|
||
if (/(approved|accepted|deployed|restored|successful)/.test(text)) return 'positive';
|
||
if (/(removed|rejected|not approved|failed|could not|needs revision|violation)/.test(text)) return 'negative';
|
||
return 'neutral';
|
||
};
|
||
const notificationSeverity = (notification: any) => {
|
||
const text = `${notification.title} ${notification.body}`.toLocaleLowerCase();
|
||
if (/(error|failed|could not)/.test(text)) return 'ERROR';
|
||
if (/(warning|violation|needs revision)/.test(text)) return 'WARNING';
|
||
return null;
|
||
};
|
||
let accountPanel = $state<AccountPanel>(initialAccountPanel());
|
||
let accessPanel = $state<AccessPanel>(initialAccessPanel());
|
||
let avatarPanel = $state<AvatarPanel>(initialAvatarPanel());
|
||
let passkeySupported = $state(true);
|
||
let passkeyPending = $state(false);
|
||
let passkeyMessage = $state('');
|
||
let passkeyClientError = $state('');
|
||
let passkeyName = $state('My passkey');
|
||
onMount(() => passkeySupported = browserSupportsWebAuthn());
|
||
async function registerPasskey() {
|
||
if (passkeyPending) return;
|
||
passkeyPending = true;
|
||
passkeyMessage = '';
|
||
passkeyClientError = '';
|
||
try {
|
||
const optionsRequest = await fetch('/auth/passkey/register/options', { method: 'POST' });
|
||
if (!optionsRequest.ok) throw new Error('Could not start passkey registration.');
|
||
const { options, challengeId } = await optionsRequest.json();
|
||
const response = await startRegistration({ optionsJSON: options });
|
||
const verification = await fetch('/auth/passkey/register/verify', {
|
||
method: 'POST', headers: { 'content-type': 'application/json' },
|
||
body: JSON.stringify({ challengeId, response, name: passkeyName })
|
||
});
|
||
const result = await verification.json();
|
||
if (!verification.ok || !result.verified) throw new Error(result.message || 'Passkey registration failed.');
|
||
passkeyMessage = 'Passkey registered. Reloading account methods…';
|
||
location.reload();
|
||
} catch (error) {
|
||
passkeyClientError = error instanceof Error ? error.message : 'Passkey registration was cancelled.';
|
||
passkeyPending = false;
|
||
}
|
||
}
|
||
function beginDiscordLink(event: MouseEvent) {
|
||
if (discordLinkPending) { event.preventDefault(); return; }
|
||
discordLinkPending = true;
|
||
}
|
||
</script>
|
||
|
||
<svelte:head><title>{user.displayName} — SHD.build</title></svelte:head>
|
||
<main class="page profile-page">
|
||
<section class="profile-command">
|
||
<div class="profile-ident"><img class="profile-avatar" src={user.avatarUrl} alt={`${user.displayName}'s avatar`} /><div><PageBreadcrumb items={[{ label: 'AGENT PROFILE' }]} /><h1>{user.displayName}</h1><span class="role-badge">{user.role.toUpperCase()} ACCESS</span></div></div>
|
||
<div class="profile-metrics"><div><strong>{data.account.builds.length}</strong><span>BUILDS</span></div><div><strong>{data.account.guides.length}</strong><span>GUIDES</span></div><div><strong>{data.account.favorites.length}</strong><span>FAVORITES</span></div><div><strong>{data.account.ratings.length}</strong><span>RATED</span></div><a href="/account/notifications"><strong>{data.account.notificationUnread}</strong><span>INBOX</span></a></div>
|
||
</section>
|
||
{#if data.chooseDisplayName}<div class="account-alert"><b>IDENTITY SETUP</b><span>Choose the SHD.build display name other agents will see. Unicode characters are supported.</span></div>{/if}
|
||
{#if data.discordLinked}<p class="success">Discord account linked successfully.</p>{/if}
|
||
{#if form?.discordUnlinked}<p class="success">Discord account unlinked. Your other configured sign-in methods remain active.</p>{/if}
|
||
{#if data.oauthError}<div class="account-alert error-alert" role="alert"><b>{data.oauthError.title}</b><span>{data.oauthError.message}</span></div>{/if}
|
||
<div class="profile-dashboard">
|
||
<section class="profile-feed">
|
||
<div class="profile-module profile-notifications">
|
||
<div class="module-head"><div><p class="eyebrow">SHD COMMUNICATION NETWORK</p><h2>NOTIFICATIONS</h2></div><a class="secondary" href="/account/notifications">OPEN INBOX{data.account.notificationUnread ? ` · ${data.account.notificationUnread} UNREAD` : ''} →</a></div>
|
||
<div class="profile-notification-list">
|
||
{#each data.account.notifications as notification}
|
||
<a class:unread={!notification.read_at} class:mandatory={Boolean(notification.mandatory)} class:isac-message={notificationIsIsac(notification)} class:positive={notificationTone(notification) === 'positive'} class:negative={notificationTone(notification) === 'negative'} href="/account/notifications"><div><img src={notificationIsIsac(notification) ? '/favicon.svg' : '/avatars/default/comms-specialist.webp'} alt=""/><span><b>{notification.source}</b><small>{String(notification.category).toUpperCase()} · {new Date(String(notification.created_at)).toLocaleDateString()}</small></span></div><span><strong>{notification.title}</strong><small>{notification.body}</small></span><em>{notificationSeverity(notification) ?? 'OPEN'} ›</em></a>
|
||
{:else}<div class="profile-empty compact"><span>◇</span><b>NO NOTIFICATIONS</b><p>ISAC and community messages will appear here.</p></div>{/each}
|
||
</div>
|
||
</div>
|
||
<div class="profile-module"><div class="module-head"><div><p class="eyebrow">LOADOUT ARCHIVE</p><h2>MY BUILDS</h2></div><a class="secondary" href="/builder">NEW BUILD +</a></div>
|
||
{#if data.account.builds.length}<div class="profile-table"><div class="profile-row heading"><span>LOADOUT</span><span>VISIBILITY</span><span>UPDATED</span><span></span></div>{#each data.account.builds as build}<div class="profile-row"><div><strong>{build.title}</strong><small>/{build.slug}</small></div><span>{build.visibility}</span><span>{new Date(String(build.updated_at)).toLocaleDateString()}</span><a href={`/builder?build=${build.slug}`}>EDIT →</a></div>{/each}</div>{:else}<div class="profile-empty"><span>◇</span><b>NO BUILDS RECORDED</b><p>Your saved and published loadouts will appear here.</p><a href="/builder">CREATE YOUR FIRST BUILD →</a></div>{/if}
|
||
</div>
|
||
<div class="profile-columns"><div class="profile-module"><p class="eyebrow">SAVED INTELLIGENCE</p><h2>FAVORITES</h2><strong class="large-count">{data.account.favorites.length}</strong><span class="count-label">SAVED LOADOUTS</span></div><div class="profile-module"><p class="eyebrow">FIELD FEEDBACK</p><h2>RATINGS</h2><strong class="large-count">{data.account.ratings.length}</strong><span class="count-label">RATED LOADOUTS</span></div></div>
|
||
</section>
|
||
<aside class="account-console">
|
||
<div class="console-head"><span>ACCOUNT CONTROL</span><i></i></div>
|
||
<div class="account-tabs" aria-label="Account controls" role="tablist">
|
||
<button id="identity-tab" class:active={accountPanel === 'identity'} role="tab" aria-selected={accountPanel === 'identity'} aria-controls="identity-panel" onclick={() => accountPanel = 'identity'}>PROFILE</button>
|
||
<button id="access-tab" class:active={accountPanel === 'access'} role="tab" aria-selected={accountPanel === 'access'} aria-controls="access-panel" onclick={() => accountPanel = 'access'}>ACCESS</button>
|
||
<button id="account-tab" class:active={accountPanel === 'account'} role="tab" aria-selected={accountPanel === 'account'} aria-controls="account-panel" onclick={() => accountPanel = 'account'}>ACCOUNT</button>
|
||
</div>
|
||
|
||
{#if accountPanel === 'identity'}
|
||
<div class="account-panel" id="identity-panel" role="tabpanel" aria-labelledby="identity-tab">
|
||
<div id="avatar" class="profile-anchor" aria-hidden="true"></div><p class="eyebrow">AGENT PORTRAIT</p><h2>AVATAR</h2>
|
||
<div class="account-subtabs" role="tablist" aria-label="Avatar source">
|
||
<button class:active={avatarPanel === 'presets'} role="tab" aria-selected={avatarPanel === 'presets'} onclick={() => avatarPanel = 'presets'}>SHD PRESETS</button>
|
||
<button class:active={avatarPanel === 'upload'} role="tab" aria-selected={avatarPanel === 'upload'} onclick={() => avatarPanel = 'upload'}>CUSTOM UPLOAD</button>
|
||
</div>
|
||
{#if avatarPanel === 'presets'}
|
||
<div class="avatar-picker" aria-label="SHD avatar presets">
|
||
{#each data.avatarPresets as preset}
|
||
<PendingForm action="?/selectAvatar" class="avatar-preset-form">
|
||
<button class="avatar-preset" class:selected={user.avatarUrl === preset.url} name="preset" value={preset.id} aria-label={`Use ${preset.label} avatar`} aria-pressed={user.avatarUrl === preset.url}>
|
||
<img src={preset.url} alt="" /><span>{preset.label}</span>
|
||
</button>
|
||
</PendingForm>
|
||
{/each}
|
||
</div>
|
||
{#if form?.avatarChanged}<p class="success">Avatar updated.</p>{/if}
|
||
{:else}
|
||
<PendingForm action="?/uploadAvatar" enctype="multipart/form-data" class="avatar-upload">
|
||
<p class="eyebrow">CUSTOM AVATAR</p>
|
||
<ImageUploadInput name="avatar" accept="image/jpeg,image/png,image/webp,image/avif" required emptyLabel="NO AVATAR SELECTED" browseLabel="CHOOSE AVATAR"/>
|
||
<small class="identity-note">JPEG, PNG, WebP, or AVIF · 5 MiB maximum · cropped to a 256 px square.</small>
|
||
{#if form?.avatarError}<p class="error">{form.avatarError}</p>{/if}
|
||
{#if form?.avatarChanged}<p class="success">{form?.avatarReviewQueued ? 'Avatar active now · queued for Oversight review.' : 'Avatar updated.'}</p>{/if}
|
||
<button class="secondary console-action">UPLOAD CUSTOM AVATAR →</button>
|
||
</PendingForm>
|
||
{/if}
|
||
<hr/>
|
||
<p class="eyebrow">PUBLIC IDENTITY</p><h2>DISPLAY NAME</h2>
|
||
<PendingForm action="?/changeDisplayName"><label class="field">PUBLIC DISPLAY NAME<input name="displayName" value={user.displayName} maxlength="80" required /></label><small class="identity-note">Visible to other agents. Unicode, spaces, and non-ASCII writing systems are supported.</small>{#if form?.displayNameError}<p class="error">{form.displayNameError}</p>{/if}<button class="primary console-action">UPDATE DISPLAY NAME →</button></PendingForm>
|
||
</div>
|
||
{:else if accountPanel === 'access'}
|
||
<div class="account-panel" id="access-panel" role="tabpanel" aria-labelledby="access-tab">
|
||
<div class="access-subtabs" role="tablist" aria-label="Sign-in method controls">
|
||
<button class:active={accessPanel === 'passkeys'} role="tab" aria-selected={accessPanel === 'passkeys'} onclick={() => accessPanel = 'passkeys'}><span>PASSKEYS</span><b>{data.passkeys.length}</b></button>
|
||
<button class:active={accessPanel === 'email'} role="tab" aria-selected={accessPanel === 'email'} onclick={() => accessPanel = 'email'}><span>EMAIL</span><b>{data.emailIdentity ? 1 : 0}</b></button>
|
||
<button class:active={accessPanel === 'local'} role="tab" aria-selected={accessPanel === 'local'} onclick={() => accessPanel = 'local'}><span>PASSWORD</span><b>{data.hasLocalCredentials ? 1 : 0}</b></button>
|
||
<button class:active={accessPanel === 'connections'} role="tab" aria-selected={accessPanel === 'connections'} onclick={() => accessPanel = 'connections'}><span>LINKS</span><b>{data.discordConnection ? 1 : 0}</b></button>
|
||
</div>
|
||
{#if accessPanel === 'passkeys'}
|
||
<p class="eyebrow">PREFERRED ACCESS</p><h2>PASSKEYS</h2>
|
||
<p class="identity-note passkey-intro">Use Face ID, Touch ID, Windows Hello, a security key, or your password manager without entering a SHD.build password.</p>
|
||
{#if data.passkeys.length}
|
||
<div class="passkey-list">
|
||
{#each data.passkeys as passkey}
|
||
<div class="passkey-card"><div><b>{passkey.name}</b><small>{passkey.backedUp ? 'SYNCED PASSKEY' : 'DEVICE PASSKEY'} · ADDED {new Date(passkey.createdAt).toLocaleDateString()}</small></div><PendingForm action="?/removePasskey"><input type="hidden" name="passkeyId" value={passkey.id}/><button class="passkey-remove" aria-label={`Remove ${passkey.name}`} disabled={data.authenticationMethods.total <= 1}>REMOVE</button></PendingForm></div>
|
||
{/each}
|
||
</div>
|
||
{:else}<p class="account-method-empty">NO PASSKEYS REGISTERED</p>{/if}
|
||
{#if form?.passkeyError}<p class="error">{form.passkeyError}</p>{/if}
|
||
{#if form?.passkeyRemoved}<p class="success">Passkey removed.</p>{/if}
|
||
{#if passkeyClientError}<p class="error">{passkeyClientError}</p>{/if}
|
||
{#if passkeyMessage}<p class="success">{passkeyMessage}</p>{/if}
|
||
<label class="field">PASSKEY NAME<input bind:value={passkeyName} maxlength="60" placeholder="e.g. Windows Hello" /></label>
|
||
<button class="primary console-action" disabled={!passkeySupported || passkeyPending || !passkeyName.trim()} aria-busy={passkeyPending} onclick={registerPasskey}>{passkeyPending ? 'REGISTERING…' : 'ADD A PASSKEY →'}</button>
|
||
{#if !passkeySupported}<p class="connection-warning">This browser does not support passkeys.</p>{/if}
|
||
{:else if accessPanel === 'email'}
|
||
<p class="eyebrow">PASSWORDLESS RECOVERY</p><h2>MAGIC LINK EMAIL</h2>
|
||
{#if data.emailIdentity}<div class="verified-email"><div><b>{data.emailIdentity.email}</b><small>VERIFIED EMAIL IDENTITY</small></div><span>VERIFIED</span></div><PendingForm action="?/removeEmailIdentity"><button class="secondary console-action" disabled={data.authenticationMethods.total <= 1}>REMOVE MAGIC-LINK EMAIL</button></PendingForm>{/if}
|
||
<PendingForm action="?/requestEmailVerification"><label class="field">{data.emailIdentity ? 'CHANGE VERIFIED EMAIL' : 'EMAIL ADDRESS'}<input name="email" type="email" value={data.emailIdentity?.email ?? ''} autocomplete="email" required /></label><small class="identity-note">We will email a verification link before this address can be used to sign in.</small>{#if form?.emailError}<p class="error">{form.emailError}</p>{/if}{#if form?.emailVerificationSent}<p class="success">Verification link sent. It expires in 15 minutes.</p>{/if}{#if data.emailVerified}<p class="success">Email verified. Magic-link sign-in is now active.</p>{/if}<button class="secondary console-action" disabled={!data.emailDeliveryConfigured}>SEND VERIFICATION LINK →</button></PendingForm>
|
||
{:else if accessPanel === 'local'}
|
||
<p class="eyebrow">OPTIONAL FALLBACK</p><h2>USERNAME & PASSWORD</h2>
|
||
{#if data.hasLocalCredentials}
|
||
<PendingForm action="?/changeLoginUsername"><label class="field">PRIVATE LOGIN USERNAME<input name="username" value={user.loginUsername ?? ''} autocomplete="username" required /></label><small class="identity-note">Used only to sign in and visible to you and authorized ISAC operators.</small>{#if form?.loginUsernameError}<p class="error">{form.loginUsernameError}</p>{/if}<button class="secondary console-action">UPDATE LOGIN USERNAME →</button></PendingForm>
|
||
<hr/><PendingForm action="?/changePassword"><label class="field">CURRENT PASSWORD<input name="currentPassword" type="password" autocomplete="current-password" required /></label><label class="field">NEW PASSWORD<input name="newPassword" type="password" autocomplete="new-password" minlength="12" required /></label><label class="field">CONFIRM PASSWORD<input name="confirmPassword" type="password" autocomplete="new-password" minlength="12" required /></label>{#if form?.passwordError}<p class="error">{form.passwordError}</p>{/if}<button class="primary console-action">UPDATE PASSWORD →</button></PendingForm>
|
||
<details class="method-removal"><summary>REMOVE USERNAME & PASSWORD</summary><p>This removes both local credentials. Your public display name is unaffected.</p><PendingForm action="?/removeLocalCredentials"><label class="field">CURRENT PASSWORD<input name="password" type="password" autocomplete="current-password" required /></label>{#if form?.localCredentialError}<p class="error">{form.localCredentialError}</p>{/if}<button class="danger console-action" disabled={data.authenticationMethods.total <= 1}>REMOVE PASSWORD SIGN-IN</button></PendingForm></details>
|
||
{:else}
|
||
<p class="account-method-empty">USERNAME AND PASSWORD SIGN-IN IS NOT CONFIGURED</p>
|
||
<PendingForm action="?/addLocalCredentials"><label class="field">PRIVATE LOGIN USERNAME<input name="username" autocomplete="username" required /></label><label class="field">PASSWORD<input name="newPassword" type="password" autocomplete="new-password" minlength="12" required /></label><label class="field">CONFIRM PASSWORD<input name="confirmPassword" type="password" autocomplete="new-password" minlength="12" required /></label>{#if form?.localCredentialError}<p class="error">{form.localCredentialError}</p>{/if}{#if form?.localCredentialsAdded}<p class="success">Username and password sign-in added.</p>{/if}<button class="secondary console-action">ADD PASSWORD FALLBACK →</button></PendingForm>
|
||
{/if}
|
||
{:else}
|
||
<p class="eyebrow">CONNECTIONS</p><h2>SIGN-IN PROVIDERS</h2>
|
||
<div class="provider-stack">
|
||
{#if data.discordConnection}
|
||
<div class="connection-card discord-connection linked-connection provider-connection-card" class:confirming={discordUnlinkConfirm}>
|
||
<div class="connection-copy"><img src="/discord-symbol.svg" alt="" aria-hidden="true" /><div><b>{data.discordConnection.displayName ?? data.discordConnection.username ?? 'DISCORD USER'}</b><small>{data.discordConnection.username ? `@${data.discordConnection.username} · ` : ''}ID {data.discordConnection.id}</small></div></div>
|
||
<button class="connection-state provider-unlink-trigger" disabled={!data.discordConnection.canUnlink} aria-label="Unlink Discord" onclick={() => discordUnlinkConfirm = true}><span>LINKED</span><span>UNLINK</span></button>
|
||
{#if discordUnlinkConfirm}
|
||
<div class="provider-unlink-confirm"><div><b>UNLINK DISCORD?</b><p>Disconnect this identity from your SHD.build account?</p></div><div><button class="secondary" onclick={() => discordUnlinkConfirm = false}>CANCEL</button><PendingForm action="?/unlinkDiscord"><button class="primary">CONFIRM UNLINK</button></PendingForm></div></div>
|
||
{/if}
|
||
{#if !data.discordConnection.canUnlink}<p class="connection-warning provider-card-message">Discord is your only sign-in method. Add a passkey, verified email, or password before unlinking.</p>{/if}
|
||
{#if form?.discordUnlinkError}<p class="error provider-card-message">{form.discordUnlinkError}</p>{/if}
|
||
</div>
|
||
{:else}
|
||
<div class="connection-card discord-connection"><div class="connection-copy"><img src="/discord-symbol.svg" alt="" aria-hidden="true" /><div><b>DISCORD</b><small>OAuth identity provider</small></div></div><a class="discord-link-button" class:interaction-pending={discordLinkPending} aria-busy={discordLinkPending} href="/auth/discord?mode=link" onclick={beginDiscordLink}><img src="/discord-symbol.svg" alt="" aria-hidden="true" /><span>Link Discord</span></a></div>
|
||
{/if}
|
||
<div class="provider-placeholder"><span class="provider-monogram google-provider">G</span><div><b>GOOGLE</b><small>ACCOUNT LINKING PLACEHOLDER</small></div><em>PLANNED</em></div>
|
||
<div class="provider-placeholder"><span class="provider-monogram apple-provider">A</span><div><b>APPLE</b><small>ACCOUNT LINKING PLACEHOLDER</small></div><em>PLANNED</em></div>
|
||
<div class="provider-placeholder"><span class="provider-monogram steam-provider">S</span><div><b>STEAM</b><small>ACCOUNT LINKING PLACEHOLDER</small></div><em>PLANNED</em></div>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{:else}
|
||
<div class="account-panel" id="account-panel" role="tabpanel" aria-labelledby="account-tab">
|
||
<p class="eyebrow danger-eyebrow">ACCOUNT LIFECYCLE</p><h2>DELETE ACCOUNT</h2>
|
||
<p class="account-retention">Your builds, guides, and comments remain attributed to a deleted agent. Your login username, display name, and Discord identity become available again.</p>
|
||
<details class="danger-zone" open={Boolean(form?.deleteError)}><summary>OPEN DELETION CONTROLS</summary><PendingForm action="?/deleteAccount"><label class="field">TYPE DELETE TO CONFIRM<input name="confirmation" autocomplete="off" required /></label><label class="field">PASSWORD · IF CONFIGURED<input name="password" type="password" autocomplete="current-password" /></label><label class="deletion-purge-option"><input name="purgeSecurityData" type="checkbox"/><span><b>PURGE SECURITY HISTORY NOW</b><small>Immediately delete retained login events and their IP, device, session, and failure metadata instead of waiting for normal retention cleanup.</small></span></label>{#if form?.deleteError}<p class="error">{form.deleteError}</p>{/if}<button class="danger console-action">SOFT-DELETE MY ACCOUNT</button></PendingForm></details>
|
||
</div>
|
||
{/if}
|
||
</aside>
|
||
</div>
|
||
</main>
|
||
|
||
<style>
|
||
.profile-notification-list a > div { grid-template-columns: 30px minmax(0, 1fr); align-items: center; }
|
||
.profile-notification-list a > div > img { width: 30px; height: 30px; object-fit: cover; border: 1px solid #3b4446; border-radius: 50%; background: #111; }
|
||
.profile-notification-list a > div > span { min-width: 0; display: grid; gap: 3px; }
|
||
.profile-notification-list a.isac-message b { color: #8ecce5; }
|
||
.profile-notification-list a.positive { border-left: 2px solid var(--shd-success); background: #101713; }
|
||
.profile-notification-list a.negative { border-left: 2px solid var(--shd-danger); background: #191111; }
|
||
.profile-notification-list a.isac-message:not(.positive):not(.negative) { border-left: 2px solid #68a9c4; background: #10171a; }
|
||
.profile-notification-list a.positive em { color: var(--shd-success); }
|
||
.profile-notification-list a.negative em { color: var(--shd-danger); }
|
||
</style>
|