feat: web UI chat render, panels, presence + analytics

This commit is contained in:
h
2026-05-31 19:41:01 +02:00
parent 75425d1bee
commit ed469ba8dd
83 changed files with 6034 additions and 136 deletions
@@ -0,0 +1,111 @@
<script lang="ts">
import type { InlineButton } from "$lib/api/types";
interface Props {
rows: InlineButton[][];
}
let { rows }: Props = $props();
let copied = $state<string | null>(null);
let resetTimer: ReturnType<typeof setTimeout> | undefined;
async function copy(key: string, data: string): Promise<void> {
await navigator.clipboard.writeText(data);
copied = key;
clearTimeout(resetTimer);
resetTimer = setTimeout(() => {
copied = null;
}, 1500);
}
</script>
<div class="InlineButtons">
{#each rows as row, rowIndex (rowIndex)}
<div class="row">
{#each row as button, colIndex (colIndex)}
{@const key = `${rowIndex}:${colIndex}`}
{#if button.kind === "url" && button.url}
<a class="button" href={button.url} target="_blank" rel="noopener">
<span class="label">{button.text}</span>
<span class="corner"></span>
</a>
{:else if button.kind === "callback" && button.data}
<button
class="button"
type="button"
onclick={() => copy(key, button.data ?? "")}
>
<span class="label"
>{copied === key ? "Скопировано" : button.text}</span
>
</button>
{:else}
<span class="button static">
<span class="label">{button.text}</span>
</span>
{/if}
{/each}
</div>
{/each}
</div>
<style lang="scss">
.InlineButtons {
display: flex;
flex-direction: column;
gap: 0.1875rem;
margin-top: 0.25rem;
}
.row {
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(0, 1fr);
gap: 0.1875rem;
}
.button {
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
min-height: 2rem;
padding: 0.25rem 0.5rem;
border: none;
border-radius: var(--border-radius-messages-small);
font-family: inherit;
font-size: 0.875rem;
font-weight: var(--font-weight-medium);
color: var(--color-text);
text-align: center;
text-decoration: none;
background-color: var(--color-message-reaction);
cursor: pointer;
transition: background-color 150ms;
&:hover:not(.static) {
background-color: var(--color-message-reaction-hover);
}
&.static {
cursor: default;
}
}
.label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.corner {
flex-shrink: 0;
font-size: 0.75rem;
opacity: 0.7;
}
</style>