Archipelago — open-source initial import

This commit is contained in:
Archipelago
2026-08-12 10:55:50 +00:00
commit 07c37b83e8
1578 changed files with 333070 additions and 0 deletions
@@ -0,0 +1,182 @@
<template>
<div ref="containerRef" class="network-map-container">
<svg ref="svgRef" class="w-full h-full"></svg>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import * as d3 from 'd3'
interface MapNode {
did: string
label: string
trust_level: 'trusted' | 'observer' | 'untrusted'
online: boolean
app_count: number
is_self: boolean
}
interface MapLink {
source: string
target: string
}
const props = defineProps<{
nodes: MapNode[]
links: MapLink[]
}>()
const containerRef = ref<HTMLDivElement>()
const svgRef = ref<SVGSVGElement>()
type SimNode = MapNode & d3.SimulationNodeDatum
type SimLink = d3.SimulationLinkDatum<SimNode> & { source: string | SimNode; target: string | SimNode }
let simulation: d3.Simulation<SimNode, SimLink> | null = null
let resizeObserver: ResizeObserver | null = null
const graphSignature = computed(() => JSON.stringify({
nodes: props.nodes.map(n => [n.did, n.label, n.trust_level, n.online, n.app_count, n.is_self]),
links: props.links.map(l => [l.source, l.target]),
}))
function trustColor(level: string): string {
switch (level) {
case 'trusted': return '#4ade80'
case 'observer': return '#fb923c'
case 'untrusted': return '#ef4444'
default: return '#9ca3af'
}
}
function nodeRadius(n: MapNode): number {
return n.is_self ? 18 : Math.max(10, Math.min(16, 8 + n.app_count * 0.5))
}
function render() {
simulation?.stop()
const svg = d3.select(svgRef.value!)
svg.selectAll('*').remove()
const container = containerRef.value!
const width = container.clientWidth
const height = container.clientHeight
svg.attr('viewBox', `0 0 ${width} ${height}`)
const simNodes: SimNode[] = props.nodes.map(n => ({ ...n }))
const simLinks: SimLink[] = props.links.map(l => ({ ...l }))
// Center the self-node
const selfNode = simNodes.find(n => n.is_self)
if (selfNode) {
selfNode.fx = width / 2
selfNode.fy = height / 2
}
simulation = d3.forceSimulation(simNodes)
.force('link', d3.forceLink<SimNode, SimLink>(simLinks).id(d => d.did).distance(120))
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide<SimNode>().radius(d => nodeRadius(d) + 5))
const g = svg.append('g')
// Links
const link = g.append('g')
.selectAll('line')
.data(simLinks)
.join('line')
.attr('stroke', (d: SimLink) => {
const src = typeof d.source === 'object' ? d.source : simNodes.find(n => n.did === d.source)
const tgt = typeof d.target === 'object' ? d.target : simNodes.find(n => n.did === d.target)
return (src as MapNode)?.online && (tgt as MapNode)?.online ? '#4ade8060' : '#6b728050'
})
.attr('stroke-width', 2)
.attr('stroke-dasharray', (d: SimLink) => {
const src = typeof d.source === 'object' ? d.source : simNodes.find(n => n.did === d.source)
const tgt = typeof d.target === 'object' ? d.target : simNodes.find(n => n.did === d.target)
return (src as MapNode)?.online && (tgt as MapNode)?.online ? 'none' : '6 4'
})
// Node groups
const node = g.append('g')
.selectAll<SVGGElement, SimNode>('g')
.data(simNodes)
.join('g')
.attr('cursor', 'pointer')
.call(d3.drag<SVGGElement, SimNode>()
.on('start', (event, d) => {
if (!event.active) simulation!.alphaTarget(0.3).restart()
d.fx = d.x
d.fy = d.y
})
.on('drag', (event, d) => {
d.fx = event.x
d.fy = event.y
})
.on('end', (event, d) => {
if (!event.active) simulation!.alphaTarget(0)
if (!d.is_self) { d.fx = null; d.fy = null }
})
)
// Node circles
node.append('circle')
.attr('r', d => nodeRadius(d))
.attr('fill', d => trustColor(d.trust_level))
.attr('fill-opacity', d => d.online ? 0.8 : 0.3)
.attr('stroke', d => d.is_self ? '#fb923c' : trustColor(d.trust_level))
.attr('stroke-width', d => d.is_self ? 3 : 1.5)
.attr('stroke-opacity', d => d.online ? 1 : 0.4)
// Node labels
node.append('text')
.text(d => d.label)
.attr('dy', d => nodeRadius(d) + 14)
.attr('text-anchor', 'middle')
.attr('fill', 'rgba(255,255,255,0.7)')
.attr('font-size', '11px')
.attr('font-family', "'Avenir Next', sans-serif")
// Tooltip
node.append('title')
.text(d => `${d.did}\nApps: ${d.app_count}\n${d.online ? 'Online' : 'Offline'}`)
simulation.on('tick', () => {
link
.attr('x1', d => (d.source as SimNode).x!)
.attr('y1', d => (d.source as SimNode).y!)
.attr('x2', d => (d.target as SimNode).x!)
.attr('y2', d => (d.target as SimNode).y!)
node.attr('transform', d => `translate(${d.x},${d.y})`)
})
}
onMounted(() => {
render()
resizeObserver = new ResizeObserver(() => render())
if (containerRef.value) resizeObserver.observe(containerRef.value)
})
onUnmounted(() => {
simulation?.stop()
resizeObserver?.disconnect()
})
watch(graphSignature, () => render())
</script>
<style scoped>
.network-map-container {
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(24px);
border-radius: 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
min-height: 400px;
width: 100%;
}
</style>
@@ -0,0 +1,66 @@
<template>
<BaseModal
:show="show"
title="Request to Peer"
max-width="max-w-md"
:z-index="'z-[3200]'"
@close="$emit('cancel')"
>
<p class="text-white/70 text-sm mb-1">
Send a connection request to
<span class="text-white font-medium">{{ targetLabel }}</span>
</p>
<p class="text-white/45 text-xs mb-4">
They'll see your request and approve or decline it. Approved peers connect
at the Peer level — never trusted automatically.
</p>
<label class="block text-xs text-white/60 mb-1">Message (optional)</label>
<textarea
v-model="message"
rows="3"
maxlength="280"
placeholder="Hey — mind if we peer?"
class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm focus:outline-none focus:border-orange-400/60 resize-none"
></textarea>
<p class="text-[11px] text-white/30 text-right mt-1">{{ message.length }}/280</p>
<div class="flex gap-2 mt-4">
<button class="flex-1 glass-button px-4 py-2 rounded-lg text-sm" @click="$emit('cancel')">
Cancel
</button>
<button
class="flex-1 glass-button glass-button-warning px-4 py-2 rounded-lg text-sm font-medium disabled:opacity-50"
:disabled="sending"
@click="$emit('send', message.trim() || undefined)"
>
{{ sending ? 'Sending' : 'Send Request' }}
</button>
</div>
</BaseModal>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import BaseModal from '@/components/BaseModal.vue'
const props = defineProps<{
show: boolean
targetLabel: string
sending?: boolean
}>()
defineEmits<{
send: [message: string | undefined]
cancel: []
}>()
const message = ref('')
watch(
() => props.show,
(visible) => {
if (visible) message.value = ''
},
)
</script>