Add public agent profiles and community moderation

This commit is contained in:
JB
2026-07-27 15:38:27 -04:00
parent efb0ccdc7e
commit 6b76110b76
28 changed files with 2101 additions and 64 deletions
@@ -0,0 +1,44 @@
<script lang="ts">
let {
agent,
size = 'default',
meta = '',
showId = false,
linked = true
}: {
agent: { id: string; displayName: string; avatarUrl: string };
size?: 'compact' | 'default' | 'large';
meta?: string;
showId?: boolean;
linked?: boolean;
} = $props();
</script>
{#if linked}
<a class="agent-identity" class:compact={size === 'compact'} class:large={size === 'large'} href={`/agents/${encodeURIComponent(agent.id)}`}>
<img src={agent.avatarUrl} alt="" />
<span>
<strong>{agent.displayName}</strong>
{#if meta}<small>{meta}</small>{:else if showId}<small>AGENT {agent.id.slice(0,8).toUpperCase()}</small>{/if}
</span>
<i aria-hidden="true">›</i>
</a>
{:else}
<div class="agent-identity unavailable" class:compact={size === 'compact'} class:large={size === 'large'}>
<img src={agent.avatarUrl} alt="" />
<span><strong>{agent.displayName}</strong><small>PROFILE HIDDEN</small></span>
</div>
{/if}
<style>
.agent-identity{min-width:0;display:grid;grid-template-columns:38px minmax(0,1fr) auto;align-items:center;gap:9px;color:var(--shd-text)}
.agent-identity img{width:38px;height:38px;border:1px solid var(--shd-orange);border-radius:50%;background:var(--shd-surface);object-fit:cover}
.agent-identity span{min-width:0;display:grid;gap:3px}.agent-identity strong,.agent-identity small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.agent-identity strong{font-size:var(--shd-type-supporting)}.agent-identity small{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}
.agent-identity i{color:var(--shd-text-dim);font-size:18px;font-style:normal}.agent-identity:hover strong,.agent-identity:focus-visible strong,.agent-identity:hover i,.agent-identity:focus-visible i{color:var(--shd-orange)}
.agent-identity.compact{grid-template-columns:30px minmax(0,1fr) auto;gap:7px}.agent-identity.compact img{width:30px;height:30px}.agent-identity.compact strong{font-size:var(--shd-type-telemetry)}
.agent-identity.large{grid-template-columns:74px minmax(0,1fr) auto;gap:15px}.agent-identity.large img{width:74px;height:74px;box-shadow:0 0 0 5px var(--shd-bg),0 0 0 6px color-mix(in srgb,var(--shd-orange) 35%,transparent)}.agent-identity.large strong{font-size:clamp(1.5rem,3vw,2.6rem);line-height:1}.agent-identity.large small{font-size:var(--shd-type-supporting)}
.agent-identity.unavailable{grid-template-columns:38px minmax(0,1fr)}.agent-identity.unavailable strong{color:var(--shd-text-muted)}
.agent-identity.unavailable.compact{grid-template-columns:30px minmax(0,1fr)}.agent-identity.unavailable.large{grid-template-columns:74px minmax(0,1fr)}
@media(max-width:600px){.agent-identity{min-height:44px}.agent-identity.large{grid-template-columns:58px minmax(0,1fr) auto}.agent-identity.large img{width:58px;height:58px}.agent-identity.large strong{font-size:1.6rem}.agent-identity.unavailable.large{grid-template-columns:58px minmax(0,1fr)}}
</style>
+35
View File
@@ -0,0 +1,35 @@
<script lang="ts">
let {
html = '',
emptyMessage = 'No field notes have been added.',
compact = false
}: {
html?: string;
emptyMessage?: string;
compact?: boolean;
} = $props();
</script>
{#if html.trim()}
<div class="markdown-content" class:compact>{@html html}</div>
{:else}
<p class="markdown-empty">{emptyMessage}</p>
{/if}
<style>
.markdown-content{max-width:76ch;color:var(--shd-text);font-family:var(--shd-font-editorial);font-size:var(--shd-type-body);line-height:1.7;overflow-wrap:anywhere}
.markdown-content.compact{font-size:var(--shd-type-supporting);line-height:1.55}
.markdown-content :global(h3),.markdown-content :global(h4),.markdown-content :global(h5){margin:1.5rem 0 .45rem;color:var(--shd-text);font-family:var(--shd-font-sans);line-height:1.15}
.markdown-content :global(h3){font-size:1.55rem}.markdown-content :global(h4){font-size:1.3rem}.markdown-content :global(h5){font-size:1.1rem}
.markdown-content :global(p),.markdown-content :global(ul),.markdown-content :global(ol),.markdown-content :global(blockquote){margin:.7em 0}
.markdown-content :global(h3 + p),.markdown-content :global(h4 + p),.markdown-content :global(h5 + p),.markdown-content :global(h3 + ul),.markdown-content :global(h4 + ul),.markdown-content :global(h5 + ul),.markdown-content :global(h3 + ol),.markdown-content :global(h4 + ol),.markdown-content :global(h5 + ol),.markdown-content :global(h3 + blockquote),.markdown-content :global(h4 + blockquote),.markdown-content :global(h5 + blockquote){margin-top:0}
.markdown-content :global(ul),.markdown-content :global(ol){padding-left:1.4rem}
.markdown-content :global(li+li){margin-top:.3em}
.markdown-content :global(a){color:var(--shd-orange);font-family:var(--shd-font-sans);font-weight:700}
.markdown-content :global(a:hover),.markdown-content :global(a:focus-visible){color:var(--shd-orange-hover)}
.markdown-content :global(blockquote){padding:.2em 0 .2em 1em;border-left:2px solid var(--shd-orange);color:var(--shd-text-muted)}
.markdown-content :global(code){padding:2px 4px;border:1px solid var(--shd-border);background:#0d0f10;color:var(--shd-text);font-family:var(--shd-font-mono);font-size:.9em}
.markdown-content :global(:first-child){margin-top:0}
.markdown-content :global(:last-child){margin-bottom:0}
.markdown-empty{margin:0;color:var(--shd-text-muted);font-family:var(--shd-font-editorial);font-size:var(--shd-type-supporting);line-height:1.55}
</style>
+119
View File
@@ -0,0 +1,119 @@
<script lang="ts">
import MarkdownContent from '$lib/components/MarkdownContent.svelte';
import { renderUserMarkdown } from '$lib/markdown';
import { tick } from 'svelte';
let {
value = $bindable(''),
name,
label = 'MARKDOWN',
help = 'Use Markdown for headings, lists, emphasis, quotes, and links.',
maxLength = 4000,
rows = 9,
required = false,
disabled = false,
id = `${name}-markdown-editor`
}: {
value?: string;
name: string;
label?: string;
help?: string;
maxLength?: number;
rows?: number;
required?: boolean;
disabled?: boolean;
id?: string;
} = $props();
let textarea = $state<HTMLTextAreaElement>();
let previewing = $state(false);
const remaining = $derived(maxLength - value.length);
const previewHtml = $derived(renderUserMarkdown(value));
async function replaceSelection(
before: string,
after = '',
placeholder = 'text',
linePrefix = false
) {
if (!textarea || disabled) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
if (linePrefix) {
const lineStart = value.lastIndexOf('\n', Math.max(0, start - 1)) + 1;
value = `${value.slice(0, lineStart)}${before}${value.slice(lineStart)}`.slice(0, maxLength);
await tick();
textarea.focus();
textarea.setSelectionRange(start + before.length, end + before.length);
return;
}
const selected = value.slice(start, end) || placeholder;
const insert = `${before}${selected}${after}`;
value = `${value.slice(0, start)}${insert}${value.slice(end)}`.slice(0, maxLength);
await tick();
textarea.focus();
const selectionStart = start + before.length;
textarea.setSelectionRange(selectionStart, Math.min(selectionStart + selected.length, value.length));
}
</script>
<div class="markdown-editor">
<div class="markdown-editor-head">
<label for={id}>{label}</label>
<div class="markdown-editor-meta">
<div class="markdown-view-tabs" aria-label={`${label} view`}>
<button type="button" class:active={!previewing} aria-pressed={!previewing} onclick={() => previewing = false}>EDIT</button>
<button type="button" class:active={previewing} aria-pressed={previewing} onclick={() => previewing = true}>PREVIEW</button>
</div>
<span class:near-limit={remaining < Math.min(200, maxLength * .1)}>{remaining.toLocaleString()} REMAINING</span>
</div>
</div>
{#if previewing}
<input type="hidden" {name} value={value}/>
<div class="markdown-preview" aria-live="polite"><MarkdownContent html={previewHtml} emptyMessage="Nothing to preview yet."/></div>
{:else}
<div class="markdown-toolbar" aria-label={`${label} formatting controls`}>
<button type="button" disabled={disabled} aria-label="Bold selected text" onclick={() => replaceSelection('**','**','bold text')}><b>B</b></button>
<button type="button" disabled={disabled} aria-label="Italicize selected text" onclick={() => replaceSelection('*','*','italic text')}><i>I</i></button>
<button type="button" disabled={disabled} aria-label="Add heading" onclick={() => replaceSelection('## ','','Heading',true)}>H2</button>
<button type="button" disabled={disabled} aria-label="Add bulleted list item" onclick={() => replaceSelection('- ','','List item',true)}>LIST</button>
<button type="button" disabled={disabled} aria-label="Add quote" onclick={() => replaceSelection('> ','','Quoted text',true)}>QUOTE</button>
<button type="button" disabled={disabled} aria-label="Add link" onclick={() => replaceSelection('[','](https://)','link text')}>LINK</button>
</div>
<textarea
bind:this={textarea}
bind:value
{id}
{name}
{rows}
{required}
{disabled}
maxlength={maxLength}
spellcheck="true"
></textarea>
{/if}
<div class="markdown-editor-foot">
<small>{help}</small>
<span>MARKDOWN SUPPORTED</span>
</div>
</div>
<style>
.markdown-editor{display:grid;border:1px solid var(--shd-border);background:#0d0f10}
.markdown-editor-head,.markdown-editor-foot{display:flex;align-items:center;justify-content:space-between;gap:var(--shd-gap-sm);padding:7px 9px;background:var(--shd-surface-raised)}
.markdown-editor-head{border-bottom:1px solid var(--shd-border)}.markdown-editor-foot{border-top:1px solid var(--shd-border)}
.markdown-editor-head label,.markdown-editor-head span,.markdown-editor-foot span{font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}
.markdown-editor-head label{color:var(--shd-text)}.markdown-editor-head span,.markdown-editor-foot span{color:var(--shd-text-muted)}.markdown-editor-head span.near-limit{color:var(--shd-orange)}
.markdown-editor-meta{display:flex;align-items:center;gap:9px}.markdown-view-tabs{display:flex;border:1px solid var(--shd-border)}.markdown-view-tabs button{min-height:28px;padding:0 8px;border:0;border-left:1px solid var(--shd-border);background:var(--shd-surface);color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900}.markdown-view-tabs button:first-child{border-left:0}.markdown-view-tabs button.active{background:#211a0e;color:var(--shd-orange)}
.markdown-editor-foot small{color:var(--shd-text-muted);font-size:var(--shd-type-supporting);line-height:1.4}
.markdown-toolbar{display:flex;flex-wrap:wrap;border-bottom:1px solid var(--shd-border)}
.markdown-toolbar button{min-width:40px;height:36px;padding:0 9px;border:0;border-right:1px solid var(--shd-border);background:var(--shd-surface);color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;cursor:pointer}
.markdown-toolbar button:hover,.markdown-toolbar button:focus-visible{background:var(--shd-surface-active);color:var(--shd-orange)}
.markdown-toolbar button:active{transform:translateY(1px);filter:brightness(.82)}
.markdown-toolbar button:disabled{cursor:not-allowed;opacity:.48}
.markdown-editor textarea{width:100%;min-height:150px;margin:0;padding:11px;border:0;border-radius:0;background:#0d0f10;color:var(--shd-text);font-family:var(--shd-font-editorial);font-size:var(--shd-type-body);line-height:1.55;resize:vertical}
.markdown-editor textarea:focus-visible{position:relative;z-index:1;outline:1px solid var(--shd-orange);outline-offset:-1px}
.markdown-preview{min-height:150px;padding:11px}.markdown-view-tabs button:active{transform:translateY(1px)}
@media(max-width:600px){.markdown-toolbar button{min-width:44px;height:44px}.markdown-editor-head,.markdown-editor-foot{align-items:flex-start;flex-direction:column}.markdown-editor-meta{width:100%;justify-content:space-between}.markdown-view-tabs button{min-height:44px}.markdown-editor-foot span{align-self:flex-end}}
@media(prefers-reduced-motion:reduce){.markdown-toolbar button{transition:none}}
</style>