maarcadetweet: initial commit

AT Protocol PDS + AppView + Tauri Desktop Client, 160-char post limit.

- PDS (Rust + axum + sqlx)
  - Auth: createAccount, createSession, refreshSession
  - Records: createRecord, deleteRecord (race-safe via SELECT FOR UPDATE)
  - Feed: feed.like.create, feed.repost.create
  - Sync: getRepo, getBlocks, getLatestCommit, getRecord (with MST proof), listRepos
  - Identity: resolveHandle
  - MST: spec-conformant (at-mst crate, 27 tests)
  - Repo: signed commits, TID counter (monotonic, 4096 wrap safe)

- AppView (Rust + axum + sqlx)
  - Jetstream consumer (WebSocket, exponential backoff, 38k+ events indexed)
  - REST API: timeline/home (graph-aware), profile, search, post (with thread hydration)
  - Handle-sync worker (did:plc + did:web)
  - JSONB embed storage + thread columns (migration 0003)
  - Like/repost counter cache (migration 0004)

- Tauri 2 + Svelte 5 Desktop Client
  - System tray (Show/Compose/Quit menu)
  - OS notifications (tauri-plugin-notification)
  - Auto-update (tauri-plugin-updater, placeholder endpoint)
  - Window-state (tauri-plugin-window-state)
  - 160-char compose with live counter
  - Image/Link embed rendering
  - LocalStorage-persisted like state
  - Timeline with poll (prepend new posts)
  - Custom TitleBar (transparent, no decorations)
  - Orange/IBM Plex Mono maarcade design

Tests: 231 Rust + 9 vitest = 240 passed.
This commit is contained in:
tomdebone
2026-07-05 20:01:31 +02:00
commit c586fd39c9
134 changed files with 35279 additions and 0 deletions
@@ -0,0 +1,91 @@
<script lang="ts">
type View = "home" | "compose" | "profile" | "search";
let { current = $bindable<View>("home") }: { current: View } = $props();
const items: Array<{ id: View; label: string; key: string; icon: string }> = [
{ id: "home", label: "home", key: "g h", icon: "home" },
{ id: "compose", label: "compose", key: "c", icon: "compose" },
{ id: "profile", label: "profile", key: "p", icon: "profile" },
{ id: "search", label: "search", key: "/", icon: "search" },
];
</script>
<nav class="rail" aria-label="navigation">
{#each items as item}
<button
class="rail__btn"
class:active={current === item.id}
onclick={() => (current = item.id)}
title={`${item.label} (${item.key})`}
aria-current={current === item.id ? "page" : undefined}
>
<span class="icon">
{#if item.icon === "home"}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
<path d="M3 11l9-8 9 8v9a2 2 0 0 1-2 2h-3v-7H8v7H5a2 2 0 0 1-2-2z"/>
</svg>
{:else if item.icon === "compose"}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
<text x="3" y="17" font-family="ui-monospace,monospace" font-size="14" font-weight="700" fill="currentColor" stroke="none">&gt;_</text>
</svg>
{:else if item.icon === "profile"}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
<circle cx="12" cy="8" r="4"/><path d="M4 21c0-4 4-7 8-7s8 3 8 7"/>
</svg>
{:else}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
<circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/>
</svg>
{/if}
</span>
<span class="label">{item.label}</span>
</button>
{/each}
</nav>
<style>
.rail {
width: 88px;
background: var(--bg);
border-right: 1px solid var(--line);
display: flex;
flex-direction: column;
gap: var(--s-1);
padding: var(--s-3) 0;
flex-shrink: 0;
}
.rail__btn {
background: transparent;
border: 0;
color: var(--text-dim);
font-family: var(--font-mono);
font-size: var(--fs-50);
padding: var(--s-3) var(--s-2);
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
border-bottom: 1px solid transparent;
transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.rail__btn:hover, .rail__btn:focus-visible {
color: var(--text);
}
.rail__btn.active {
color: var(--orange);
border-bottom-color: var(--orange);
}
.icon {
width: 24px;
height: 24px;
display: grid;
place-items: center;
}
.icon :global(svg) {
width: 22px;
height: 22px;
}
.label { letter-spacing: 0.04em; }
</style>