diff --git a/web/package.json b/web/package.json index aa833d577..337bed4fb 100644 --- a/web/package.json +++ b/web/package.json @@ -36,6 +36,8 @@ "@sveltejs/vite-plugin-svelte": "^7.2.0", "@tailwindcss/vite": "^4.3.2", "@types/d3-geo": "3.1.1", + "@types/d3-path": "3.1.1", + "@types/d3-shape": "3.2.0", "@types/markdown-it": "^14.1.2", "@types/markdown-it-emoji": "^3.0.1", "@types/node": "^24.13.3", @@ -86,6 +88,8 @@ "cobe": "2.0.1", "cuelume": "0.2.4", "d3-geo": "3.1.1", + "d3-path": "3.1.0", + "d3-shape": "3.2.0", "markdown-it": "^14.3.0", "markdown-it-anchor": "^9.2.1", "markdown-it-emoji": "^3.1.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index cb49e6382..f1b0a297b 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -69,6 +69,12 @@ importers: d3-geo: specifier: 3.1.1 version: 3.1.1 + d3-path: + specifier: 3.1.0 + version: 3.1.0 + d3-shape: + specifier: 3.2.0 + version: 3.2.0 markdown-it: specifier: ^14.3.0 version: 14.3.0 @@ -142,6 +148,12 @@ importers: '@types/d3-geo': specifier: 3.1.1 version: 3.1.1 + '@types/d3-path': + specifier: 3.1.1 + version: 3.1.1 + '@types/d3-shape': + specifier: 3.2.0 + version: 3.2.0 '@types/markdown-it': specifier: ^14.1.2 version: 14.1.2 @@ -1838,6 +1850,12 @@ packages: '@types/d3-geo@3.1.1': resolution: {integrity: sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==} + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-shape@3.2.0': + resolution: {integrity: sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -2224,6 +2242,14 @@ packages: resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} engines: {node: '>=12'} + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + dayjs@1.11.21: resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} @@ -5070,6 +5096,12 @@ snapshots: dependencies: '@types/geojson': 7946.0.16 + '@types/d3-path@3.1.1': {} + + '@types/d3-shape@3.2.0': + dependencies: + '@types/d3-path': 3.1.1 + '@types/deep-eql@4.0.2': {} '@types/esrecurse@4.3.1': {} @@ -5475,6 +5507,12 @@ snapshots: dependencies: d3-array: 3.2.4 + d3-path@3.1.0: {} + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + dayjs@1.11.21: {} debug@4.4.3(supports-color@10.2.2): diff --git a/web/src/lib/components/home/CircularTimeline.stories.svelte b/web/src/lib/components/home/CircularTimeline.stories.svelte new file mode 100644 index 000000000..fe6d7c16d --- /dev/null +++ b/web/src/lib/components/home/CircularTimeline.stories.svelte @@ -0,0 +1,36 @@ + + + { + const links = [...canvasElement.querySelectorAll("a")]; + await expect(links.length).toBeGreaterThan(0); + await expect(links.every((a) => a.getAttribute("href")?.startsWith("/did:"))).toBe(true); + }} +/> + + + + event.$type === "sh.tangled.feed.getTimeline#starEvent" + ) + }} +/> + + diff --git a/web/src/lib/components/home/CircularTimeline.svelte b/web/src/lib/components/home/CircularTimeline.svelte new file mode 100644 index 000000000..add33a098 --- /dev/null +++ b/web/src/lib/components/home/CircularTimeline.svelte @@ -0,0 +1,457 @@ + + + + {#if size.width} + + + {#each rings as ring, r (r)} + + {/each} + + + + + + + + {#each rings as ring, r (r)} + + + {#each ring.cards as { start, sweep, kind, runs }, i (i)} + {@const Icon = EVENTS[kind].icon} + {#if shown[r]?.[i]} + + + + + + + + + + + {#each runs as run, k (k)} + {#if run.did} + {run.text} + {:else} + {run.text} + {/if} + {/each} + + + + {/if} + {/each} + + + {/each} + + + {/if} + + + diff --git a/web/src/lib/components/home/Hero.stories.svelte b/web/src/lib/components/home/Hero.stories.svelte new file mode 100644 index 000000000..3ee2ecd59 --- /dev/null +++ b/web/src/lib/components/home/Hero.stories.svelte @@ -0,0 +1,13 @@ + + + diff --git a/web/src/lib/components/home/Hero.svelte b/web/src/lib/components/home/Hero.svelte new file mode 100644 index 000000000..1c5785bd8 --- /dev/null +++ b/web/src/lib/components/home/Hero.svelte @@ -0,0 +1,55 @@ + + + + + + + + + + Hack it + + together. + + + + + An open-source code forge on AT Protocol. Self-host it, extend it, and build it with + the people you trust. + + + + + (paused = !paused)} + /> + + diff --git a/web/src/lib/components/home/content.ts b/web/src/lib/components/home/content.ts index 2703d30ae..14e645f87 100644 --- a/web/src/lib/components/home/content.ts +++ b/web/src/lib/components/home/content.ts @@ -4,7 +4,11 @@ import GitPullRequestClosed from "$icon/git-pull-request-closed"; import HardDrive from "$icon/hard-drive"; import ShieldCheck from "$icon/shield-check"; import Snowflake from "$icon/snowflake"; +import type { Handle } from "@atcute/lexicons"; import type { PullSubmission } from "$lib/api"; +import type * as ShTangledActorDefs from "$lib/api/lexicons/types/sh/tangled/actor/defs"; +import type * as ShTangledFeedGetTimeline from "$lib/api/lexicons/types/sh/tangled/feed/getTimeline"; +import type * as ShTangledRepoDefs from "$lib/api/lexicons/types/sh/tangled/repo/defs"; import type { ChatCue, ChatScript } from "$lib/components/home/chat"; import type { FeatureCardVariants } from "$lib/components/home/FeatureCard.svelte"; import ShieldDirectDenounce from "$lib/components/icons/ShieldDirectDenounce.svelte"; @@ -363,3 +367,103 @@ export const vouchChats = { ] } } satisfies Record; + +type TimelineItem = ShTangledFeedGetTimeline.TimelineItem; + +const CID = "bafyreib2rxk3rybk3aobmv5cjuql3bm2twh4jo5uxgf2tsxh6o4kbmpjaa"; +const EVENT_AT = "2026-10-01T12:00:00Z"; + +const user = ( + handle: Handle, + id: string +): Pick => ({ + did: `did:plc:${id}`, + handle +}); + +const users = { + oppi: user("oppi.li", "qfpnj4og54vl56wngdriaxug"), + juniper: user("juniper.dev", "k3vxmd2xnqhy6ftlgruyw4ze"), + sage: user("sage.sh", "b7ijzdg4kxaf2pnwcxq5h3lo"), + fennel: user("fennel.moe", "4mwzrnqc6jsg7tdlxk2yoe5v"), + pixel: user("pixel.pet", "z2hxtla6qrybm3ce7vpkon4j"), + clover: user("clover.tngl.sh", "wd5gqhnb3xkp7rlyzo2aejc6"), + wren: user("wren.tngl.sh", "r6oyjvk2tdm4qczxbnf7ugl3"), + kestrel: user("kestrel.sh", "h4nlqe3cxzu7vgbyjdo6wkrt"), + birch: user("birch.tngl.sh", "y5ktcwj7oafmz2xhgrvl3pdn") +}; + +const repo = ( + owner: ShTangledActorDefs.ProfileViewBasic, + slug: string, + id: string +): ShTangledRepoDefs.RepoViewBasic => ({ + did: `did:plc:${id}`, + slug, + owner, + createdAt: EVENT_AT +}); + +const repos = { + core: repo(users.oppi, "core", "ox7vldp2kgymwq4c3jzanrht"), + spindle: repo(users.oppi, "spindle", "jl3mbzwe6dqtgrxk5fhov7ya"), + zap: repo(users.kestrel, "zap", "c2qahfnrvx7okt4mlwjgz6ye"), + dotfiles: repo(users.juniper, "dotfiles", "t5gyxkoehz3dbcwrn7maq4lj"), + lumen: repo(users.sage, "lumen", "v6dnoe3rzjgktmyq2wbhxc7a"), + sprites: repo(users.pixel, "sprites", "m7rcqwgjthx5bklzad4yfo2n"), + cloverDotfiles: repo(users.clover, "dotfiles", "fq2zkvyh6mxcjr3odwgt5anb"), + kestrelSpindle: repo(users.kestrel, "spindle", "n3xbqcwh7krzlvgy5emotf2d"), + wrenZap: repo(users.wren, "zap", "u4jgkdye2wvqmhto7zcxnr6s") +}; + +const item = (event: TimelineItem["event"]): TimelineItem => ({ event, eventAt: EVENT_AT }); + +const star = ( + actor: ShTangledActorDefs.ProfileViewBasic, + starred: ShTangledRepoDefs.RepoViewBasic +) => + item({ + $type: "sh.tangled.feed.getTimeline#starEvent", + uri: `at://${actor.did}/sh.tangled.feed.star/${starred.slug}`, + cid: CID, + actor, + repo: starred + }); + +const follow = ( + actor: ShTangledActorDefs.ProfileViewBasic, + subject: ShTangledActorDefs.ProfileViewDetailed +) => + item({ + $type: "sh.tangled.feed.getTimeline#followEvent", + uri: `at://${actor.did}/sh.tangled.graph.follow/${subject.handle}`, + cid: CID, + actor, + subject + }); + +const create = ( + created: ShTangledRepoDefs.RepoViewBasic, + source?: ShTangledRepoDefs.RepoViewBasic +) => + item({ + $type: "sh.tangled.feed.getTimeline#repoEvent", + uri: `at://${created.owner.did}/sh.tangled.repo/${created.slug}`, + cid: CID, + repo: created, + source + }); + +export const timelineFeed: ShTangledFeedGetTimeline.$output["feed"] = [ + star(users.juniper, repos.zap), + create(repos.lumen), + follow(users.fennel, users.oppi), + create(repos.cloverDotfiles, repos.dotfiles), + star(users.wren, repos.core), + follow(users.birch, users.sage), + create(repos.sprites), + create(repos.kestrelSpindle, repos.spindle), + star(users.oppi, repos.lumen), + follow(users.juniper, users.pixel), + create(repos.wrenZap, repos.zap) +]; diff --git a/web/src/lib/components/home/halftone.ts b/web/src/lib/components/home/halftone.ts index c50afad81..467751c33 100644 --- a/web/src/lib/components/home/halftone.ts +++ b/web/src/lib/components/home/halftone.ts @@ -2,8 +2,22 @@ export const PITCH = 18; const ROW = PITCH / 2; -const dot = (x: number, y: number, r: number) => - `M${x - r} ${y}a${r} ${r} 0 1 0 ${2 * r} 0a${r} ${r} 0 1 0 ${-2 * r} 0z`; +export interface Normal { + x: number; + y: number; + z: number; +} + +const round = (n: number) => +n.toFixed(2); + +const dot = (x: number, y: number, r: number, squash = 1, angle = 0) => { + const rx = round(r * squash); + const ry = round(r); + const dx = round(Math.cos(angle) * rx); + const dy = round(Math.sin(angle) * rx); + const tilt = round((angle * 180) / Math.PI); + return `M${round(x) - dx} ${round(y) - dy}a${rx} ${ry} ${tilt} 1 0 ${2 * dx} ${2 * dy}a${rx} ${ry} ${tilt} 1 0 ${-2 * dx} ${-2 * dy}z`; +}; export const halftone = ( width: number, @@ -15,7 +29,38 @@ export const halftone = ( const y = row * ROW; for (let x = (row % 2) * ROW; x <= width; x += PITCH) { const r = radius(x, y); - if (r > 0) out.push(dot(x, y, +r.toFixed(2))); + if (r > 0) out.push(dot(x, y, r)); + } + } + return out.join(""); +}; + +export const sphere = ( + width: number, + height: number, + center: { x: number; y: number }, + radius: number, + size: (normal: Normal) => number, + pitch = PITCH +): string => { + const step = pitch / radius; + const span = Math.floor(Math.PI / 2 / step); + const out: string[] = []; + for (let row = -2 * span; row <= 2 * span; row++) { + const turn = (row * step) / 2; + const shift = Math.abs(row % 2) / 2; + for (let col = -span; col <= span; col++) { + const tilt = (col + shift) * step; + const ring = Math.cos(tilt); + const nx = Math.sin(tilt); + const ny = ring * Math.sin(turn); + const nz = ring * Math.cos(turn); + const x = center.x + radius * nx; + const y = center.y + radius * ny; + if (nz <= 0 || x < -pitch || x > width + pitch || y < -pitch || y > height + pitch) + continue; + const r = size({ x: nx, y: ny, z: nz }); + if (r > 0) out.push(dot(x, y, r, nz, Math.atan2(ny, nx))); } } return out.join("");
+ An open-source code forge on AT Protocol. Self-host it, extend it, and build it with + the people you trust. +