Add public agent profiles and community moderation
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user