Initial commit on Forgejo
All checks were successful
CI and deploy / Test (push) Successful in 4m52s
CI and deploy / Deploy production (push) Successful in 23s

Fresh repository history for elektrine/tarakan hosted at
https://git.elektrine.com/elektrine/tarakan.
This commit is contained in:
Maxfield Luke 2026-07-29 04:43:40 -04:00
commit af6077b9c3
455 changed files with 78366 additions and 0 deletions

223
assets/js/app.js Normal file
View file

@ -0,0 +1,223 @@
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"
// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
// import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
// import "some-package"
//
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
// To load it, simply add a second `<link>` to your `root.html.heex` file.
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Theme: no data-theme attribute means "follow the OS"; a manual choice is
// stamped on <html> and persisted.
const syncThemeToggle = theme => {
document.querySelectorAll("[data-theme-option]").forEach(button => {
button.setAttribute("aria-pressed", button.dataset.themeOption === theme ? "true" : "false")
})
}
const setTheme = theme => {
if (theme === "system") {
localStorage.removeItem("tarakan:theme")
document.documentElement.removeAttribute("data-theme")
} else {
localStorage.setItem("tarakan:theme", theme)
document.documentElement.setAttribute("data-theme", theme)
}
syncThemeToggle(theme)
}
setTheme(localStorage.getItem("tarakan:theme") || "system")
window.addEventListener("tarakan:set-theme", ({detail}) => setTheme(detail.theme))
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import {hooks as colocatedHooks} from "phoenix-colocated/tarakan"
import topbar from "../vendor/topbar"
import InfestationField from "./hooks/infestation_field.js"
const AutoDismiss = {
mounted() {
this.pause = () => this.clearTimer()
this.resume = () => this.schedule()
this.el.addEventListener("mouseenter", this.pause)
this.el.addEventListener("mouseleave", this.resume)
this.el.addEventListener("focusin", this.pause)
this.el.addEventListener("focusout", this.resume)
this.schedule()
},
updated() {
this.schedule()
},
destroyed() {
this.clearTimer()
this.el.removeEventListener("mouseenter", this.pause)
this.el.removeEventListener("mouseleave", this.resume)
this.el.removeEventListener("focusin", this.pause)
this.el.removeEventListener("focusout", this.resume)
},
clearTimer() {
if (this.timer) window.clearTimeout(this.timer)
this.timer = null
},
schedule() {
this.clearTimer()
const delay = Number(this.el.dataset.autoDismissMs || 5000)
this.timer = window.setTimeout(() => this.el.click(), delay)
},
}
const PinToBottom = {
mounted() {
this.scrollToBottom()
},
beforeUpdate() {
const distance = this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight
this.pinned = distance < 60
},
updated() {
if (this.pinned) this.scrollToBottom()
},
scrollToBottom() {
this.el.scrollTop = this.el.scrollHeight
},
}
const SearchShortcut = {
mounted() {
this.onKeydown = event => {
if (event.key !== "/" || event.defaultPrevented) return
const target = event.target
if (target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName)) return
event.preventDefault()
this.el.focus()
}
window.addEventListener("keydown", this.onKeydown)
},
destroyed() {
window.removeEventListener("keydown", this.onKeydown)
},
}
const CopyLink = {
mounted() {
this.onClick = async () => {
const text = this.el.dataset.copyText
if (!text) return
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text)
} else {
const ta = document.createElement("textarea")
ta.value = text
ta.setAttribute("readonly", "")
ta.style.position = "fixed"
ta.style.left = "-9999px"
document.body.appendChild(ta)
ta.select()
document.execCommand("copy")
document.body.removeChild(ta)
}
const label = this.el.querySelector("[data-copy-label]")
if (label) {
const previous = label.textContent
label.textContent = this.el.dataset.copiedLabel || "Copied"
window.clearTimeout(this.resetTimer)
this.resetTimer = window.setTimeout(() => {
label.textContent = previous
}, 1600)
}
} catch (_err) {
// Silent: clipboard may be blocked; the visible cite URL remains.
}
}
this.el.addEventListener("click", this.onClick)
},
destroyed() {
this.el.removeEventListener("click", this.onClick)
window.clearTimeout(this.resetTimer)
},
}
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken},
hooks: {...colocatedHooks, AutoDismiss, PinToBottom, SearchShortcut, CopyLink, InfestationField},
})
// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#e60012"}, shadowColor: "rgba(0, 0, 0, .35)"})
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => {
topbar.hide()
syncThemeToggle(localStorage.getItem("tarakan:theme") || "system")
})
// connect if there are any LiveViews on the page
liveSocket.connect()
// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket
// The lines below enable quality of life phoenix_live_reload
// development features:
//
// 1. stream server logs to the browser console
// 2. click on elements to jump to their definitions in your code editor
//
if (process.env.NODE_ENV === "development") {
window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
// Enable server log streaming to client.
// Disable with reloader.disableServerLogs()
reloader.enableServerLogs()
// Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
//
// * click with "c" key pressed to open at caller location
// * click with "d" key pressed to open at function component definition location
let keyDown
window.addEventListener("keydown", e => keyDown = e.key)
window.addEventListener("keyup", _e => keyDown = null)
window.addEventListener("click", e => {
if(keyDown === "c"){
e.preventDefault()
e.stopImmediatePropagation()
reloader.openEditorAtCaller(e.target)
} else if(keyDown === "d"){
e.preventDefault()
e.stopImmediatePropagation()
reloader.openEditorAtDef(e.target)
}
}, true)
window.liveReloader = reloader
})
}

View file

@ -0,0 +1,292 @@
// A slowly drifting wireframe of the cross-repository graph: one point per
// repository that carries a multi-repo pattern, one line for every pattern two
// repositories share. Rendered rather than tabulated because entanglement is
// the one shape on the record a table cannot show.
//
// Deliberately flat and line-based - no lighting, no materials, no depth fog -
// so it reads as an oscilloscope trace rather than a product render.
//
// Degrades to nothing: no WebGL, no data, or reduced-motion preference all
// leave the surrounding markup untouched, which already carries the real
// numbers.
const POINT_SIZE = 5.5
const RADIUS = 26
const DRIFT = 0.00035
export default {
mounted() {
this.start()
},
updated() {
// The graph is re-sent on registry changes; rebuild rather than mutate.
this.stop()
this.start()
},
// three.js is ~132 KB gzipped - four times the rest of the bundle combined.
// Importing it dynamically keeps it in its own chunk, so only a visitor who
// actually reaches a page with the field pays for it, and every other page
// loads the same 43 KB it always did.
async loadThree() {
if (!window.__tarakanThree) {
window.__tarakanThree = import("../../vendor/three.module.js")
}
return window.__tarakanThree
},
destroyed() {
this.stop()
},
async start() {
const graph = this.readGraph()
if (!graph || graph.nodes.length < 2) return
let THREE
try {
THREE = await this.loadThree()
} catch (_error) {
return
}
// The hook can be destroyed while the chunk is in flight.
if (!this.el.isConnected) return
this.THREE = THREE
this.graph = graph
this.draw()
// The field is hidden below the lg breakpoint, so at mount it can have no
// size at all and there is nothing to render into. Watch for it gaining
// one instead of giving up permanently.
if (!this.renderer && "ResizeObserver" in window) {
this.observer = new ResizeObserver(() => {
if (!this.renderer && this.el.clientWidth > 0 && this.el.clientHeight > 0) {
this.draw()
}
})
this.observer.observe(this.el)
}
},
draw() {
if (!this.graph || !this.THREE) return
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
this.renderStill(this.graph)
} else {
this.renderAnimated(this.graph)
}
},
readGraph() {
try {
const raw = this.el.dataset.graph
if (!raw) return null
const parsed = JSON.parse(raw)
if (!Array.isArray(parsed.nodes) || !Array.isArray(parsed.edges)) return null
return parsed
} catch (_error) {
return null
}
},
// Colours come from the stylesheet so the field follows the theme toggle
// instead of hard-coding a palette that only works in dark mode.
token(name, fallback) {
const value = getComputedStyle(this.el).getPropertyValue(name).trim()
return value || fallback
},
build(graph) {
const { nodes, edges } = graph
// Fibonacci sphere: even coverage without clustering at the poles, and
// deterministic, so the same record always draws the same figure.
const positions = nodes.map((_node, i) => {
const offset = 2 / nodes.length
const y = i * offset - 1 + offset / 2
const r = Math.sqrt(Math.max(0, 1 - y * y))
const phi = i * Math.PI * (3 - Math.sqrt(5))
return [Math.cos(phi) * r * RADIUS, y * RADIUS, Math.sin(phi) * r * RADIUS]
})
const group = new this.THREE.Group()
const pointGeometry = new this.THREE.BufferGeometry()
pointGeometry.setAttribute(
"position",
new this.THREE.Float32BufferAttribute(positions.flat(), 3)
)
group.add(
new this.THREE.Points(
pointGeometry,
new this.THREE.PointsMaterial({
color: new this.THREE.Color(this.token("--color-ink-faint", "#8a8f93")),
size: POINT_SIZE,
sizeAttenuation: false,
transparent: true,
opacity: 0.9
})
)
)
if (edges.length > 0) {
const linePoints = []
edges.forEach(([a, b]) => {
if (positions[a] && positions[b]) linePoints.push(...positions[a], ...positions[b])
})
if (linePoints.length > 0) {
const lineGeometry = new this.THREE.BufferGeometry()
lineGeometry.setAttribute(
"position",
new this.THREE.Float32BufferAttribute(linePoints, 3)
)
group.add(
new this.THREE.LineSegments(
lineGeometry,
new this.THREE.LineBasicMaterial({
color: new this.THREE.Color(this.token("--color-phosphor", "#4ade80")),
transparent: true,
opacity: 0.42
})
)
)
}
}
return group
},
setup(graph) {
const width = this.el.clientWidth
const height = this.el.clientHeight
if (width === 0 || height === 0) return null
let renderer
try {
renderer = new this.THREE.WebGLRenderer({ alpha: true, antialias: true })
} catch (_error) {
return null
}
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.setSize(width, height, false)
renderer.domElement.setAttribute("aria-hidden", "true")
this.el.appendChild(renderer.domElement)
const scene = new this.THREE.Scene()
const camera = new this.THREE.PerspectiveCamera(45, width / height, 1, 400)
camera.position.z = 78
const group = this.build(graph)
scene.add(group)
this.renderer = renderer
this.scene = scene
this.camera = camera
this.group = group
this.onResize = () => {
const w = this.el.clientWidth
const h = this.el.clientHeight
if (w === 0 || h === 0) return
camera.aspect = w / h
camera.updateProjectionMatrix()
renderer.setSize(w, h, false)
renderer.render(scene, camera)
}
window.addEventListener("resize", this.onResize)
return { renderer, scene, camera, group }
},
renderStill(graph) {
const context = this.setup(graph)
if (!context) return
context.group.rotation.set(0.35, 0.6, 0)
context.renderer.render(context.scene, context.camera)
},
renderAnimated(graph) {
const context = this.setup(graph)
if (!context) return
const { renderer, scene, camera, group } = context
let last = performance.now()
const frame = (now) => {
// Rotation is time-based, not frame-based, so a 120Hz display does not
// spin twice as fast as a 60Hz one.
const delta = now - last
last = now
group.rotation.y += DRIFT * delta
group.rotation.x = Math.sin(now * 0.00007) * 0.22
renderer.render(scene, camera)
this.frameId = requestAnimationFrame(frame)
}
this.frameId = requestAnimationFrame(frame)
// A hidden tab still fires rAF in some browsers; stop drawing when the
// page is not visible so a background tab costs nothing.
this.onVisibility = () => {
if (document.hidden) {
this.cancelFrame()
} else if (!this.frameId) {
last = performance.now()
this.frameId = requestAnimationFrame(frame)
}
}
document.addEventListener("visibilitychange", this.onVisibility)
},
cancelFrame() {
if (this.frameId) {
cancelAnimationFrame(this.frameId)
this.frameId = null
}
},
stop() {
this.cancelFrame()
if (this.observer) {
this.observer.disconnect()
this.observer = null
}
if (this.onResize) {
window.removeEventListener("resize", this.onResize)
this.onResize = null
}
if (this.onVisibility) {
document.removeEventListener("visibilitychange", this.onVisibility)
this.onVisibility = null
}
// GPU buffers are not garbage collected with the JS objects that own them,
// and LiveView navigation destroys hooks constantly.
if (this.group) {
this.group.traverse((child) => {
if (child.geometry) child.geometry.dispose()
if (child.material) child.material.dispose()
})
this.group = null
}
if (this.renderer) {
this.renderer.dispose()
this.renderer.domElement.remove()
this.renderer = null
}
this.scene = null
this.camera = null
this.graph = null
}
}

21
assets/js/theme.js Normal file
View file

@ -0,0 +1,21 @@
// Runs blocking in <head>, before the first paint.
//
// app.js is a deferred module, so anything it does happens after the browser
// has already drawn the page - which is why the theme toggle used to flash the
// wrong option on load, and why a manually chosen theme would show a frame of
// the OS one first. This has to stay a classic script with no defer/async, and
// it has to stay small enough that blocking on it is free.
//
// Absent attribute means "follow the OS", so the only work here is restoring a
// manual choice. Everything else - the pressed styling, the toggle handlers -
// is CSS and app.js respectively.
(function () {
try {
var theme = localStorage.getItem("tarakan:theme")
if (theme === "light" || theme === "dark") {
document.documentElement.setAttribute("data-theme", theme)
}
} catch (_error) {
// Private mode or blocked storage: fall through to the OS preference.
}
})()