Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c7c78fc
chore: orbit rail contract and stubs
ArtMin96 Oct 24, 2026
af6bcdc
feat: share the go-live dialog, ring the aside live list, document th…
ArtMin96 Oct 24, 2026
6e40aac
feat: reusable live primitives and the rotating lane ring
ArtMin96 Oct 24, 2026
c12d8fe
docs: live ring rule in DESIGN.md
ArtMin96 Oct 24, 2026
57cc662
feat: orbit rail on the log pages
ArtMin96 Oct 24, 2026
3f6fa57
perf: index users by when their stream ends
ArtMin96 Oct 24, 2026
c6c29b2
feat: share who is live with their stream, player and latest commit
ArtMin96 Oct 24, 2026
6dd2d2c
merge: orbit rail backend
ArtMin96 Oct 24, 2026
954bb50
merge: orbit rail live primitives
ArtMin96 Oct 24, 2026
a889793
merge: orbit rail go-live flow and docs
ArtMin96 Oct 24, 2026
1830646
merge: orbit rail on the log pages
ArtMin96 Oct 24, 2026
9d121f2
fix: orbit rail refreshes on every log tab and shares its live pieces
ArtMin96 Oct 24, 2026
0d24c68
fix: feed lines pass behind a live node's LIVE pill
ArtMin96 Oct 24, 2026
e3f5425
docs: one live ring rule and the shipped Orbit Rail behaviour
ArtMin96 Oct 24, 2026
f5198b8
fix: keep the stream card's actions on one row
ArtMin96 Oct 24, 2026
cae9ad9
refactor: drop the rail's follow count
ArtMin96 Oct 24, 2026
2868d01
fix: rail handles clear the LIVE pill when a ring lifts
ArtMin96 Oct 24, 2026
7f7f566
fix: the rail ends where its last ring does
ArtMin96 Oct 24, 2026
f60a32d
perf: pause live rings while they are off screen
ArtMin96 Oct 24, 2026
2de2e18
perf: share one clock formatter across the rail's rings
ArtMin96 Oct 24, 2026
0204935
perf: build embed providers only until one plays the link
ArtMin96 Oct 24, 2026
f2e9575
perf: send the rail only the fields it draws
ArtMin96 Oct 24, 2026
64ae8bb
perf: fetch twitch thumbnails at 640x360
ArtMin96 Oct 24, 2026
ed25c59
refactor: drop the live ring's unused options
ArtMin96 Oct 24, 2026
1b2fbff
fix: show live end times in the viewer's chosen timezone
ArtMin96 Oct 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ Every post is a commit and the timeline is the log. The system draws a literal `

The register is a well-lit terminal rather than a dark hacker cosplay. A cool near-white shell holds a white feed column cut by hairline cool-gray rules; ink is near-black. Geist carries prose, Geist Mono carries everything a machine would print: handles, short hashes, relative times, counts, character budgets, `fatal:` lines, and `null` values. Dark mode is a faithful mirror on graphite, with the lanes and primary lifted for luminance rather than redesigned.

Color is rationed. Six author lane colors exist only inside the graph and its direct echoes (profile ring, handle dot). One blue does the work of action and location: the primary button, the current nav item, the active tab underline, links. Motion is the graph's own: a node pops onto the rail when you commit, rail segments draw downward on the landing page, and a deleted commit prints `rm {hash} → /dev/null` before its row collapses. Navigation only moves to say where you went: pages crossfade in 160ms, the Blue Wash behind the current nav item and the active tab underline slide to their new place, and switching between Log and Everyone, or between the Profile and Appearance settings, slides the content toward the tab you picked while the header (and the composer) stays put.
Color is rationed. Six author lane colors exist only inside the graph and its direct echoes (profile ring, live ring, handle dot). One blue does the work of action and location: the primary button, the current nav item, the active tab underline, links. Motion is the graph's own: a node pops onto the rail when you commit, rail segments draw downward on the landing page, and a deleted commit prints `rm {hash} → /dev/null` before its row collapses. Navigation only moves to say where you went: pages crossfade in 160ms, the Blue Wash behind the current nav item and the active tab underline slide to their new place, and switching between Log and Everyone, or between the Profile and Appearance settings, slides the content toward the tab you picked while the header (and the composer) stays put.

**Key Characteristics:**
- A vertical graph rail with lane-colored nodes and segments is the signature and the only decorative device.
Expand All @@ -191,7 +191,7 @@ A cool, nearly colorless shell with one working blue and six author lanes reserv
- **Blue Wash** (`primary-soft`): fill behind the active nav item (the side nav item and the bottom bar's rounded icon background) and the selected option of the settings switches (theme, who to follow); also the text selection color.

### Secondary
- **Author Lanes** (`lane-0` through `lane-5`, burnt orange, green, violet, teal, rose, ochre; each with a `-dark` lift): chosen by the user in Settings → Profile, otherwise assigned by hashing the user id (`User::automaticLane()`); every serialized user carries the effective `lane` (0–5) and the frontend paints it with `laneColor(user.lane)`. Used only for graph nodes, rail segments, the staged composer node, the 8px dot before a handle in profile contexts, and the 2px ring around a profile avatar. Never text, never fills behind text, never buttons.
- **Author Lanes** (`lane-0` through `lane-5`, burnt orange, green, violet, teal, rose, ochre; each with a `-dark` lift): chosen by the user in Settings → Profile, otherwise assigned by hashing the user id (`User::automaticLane()`); every serialized user carries the effective `lane` (0–5) and the frontend paints it with `laneColor(user.lane)`. Used only for graph nodes, rail segments, the staged composer node, the 8px dot before a handle in profile contexts, the 2px ring around a profile avatar, and the live ring. Never text, never fills behind text, never buttons.

### Tertiary
- **Rm Red** (`destructive`): delete affordances (trash icon on hover, inline confirm wash at 8% opacity, destructive button), "Unfollow" on hover, the composer counter at 0.
Expand All @@ -208,6 +208,8 @@ A cool, nearly colorless shell with one working blue and six author lanes reserv
### Named Rules
**The Lanes Are Identity Rule.** A lane color appears only where it marks an author inside or beside the graph. If a lane color is carrying meaning that isn't "this person", it is wrong.

**The Live Ring Rule.** Someone who is live wears one look everywhere (feed node, profile header, user preview, aside, the Orbit Rail): their avatar inside the `LiveRing`, a 2px conic ring that starts in their own lane for the first fifth and runs through the other lanes back into theirs, a 2px see-through gap, then the avatar, with the red mono LIVE pill on the ring's bottom edge. One component (`LiveAvatar` in `components/live/live-avatar.tsx`) draws it. It turns once every 9s, by rotation only, and holds still under reduced motion. No other avatar ring spins, live is never a plain red ring, and there is no second live marker.

**The One Blue Rule.** Blue means "you can act here" or "you are here". It is never decorative and never assigned to an author lane.

## Typography
Expand Down Expand Up @@ -323,8 +325,13 @@ Under the graph, a mono 13px strip prints the streaks, `longest streak 12 days
- **Thread keys:** on the focused commit, `r` opens its reply composer with the cursor in it, `q` opens its quote dialog, and `o` shows or hides its replies; focus stays on the commit (Esc or replying returns it there), so `j`/`k` keep stepping through top-level commits. The commit's own Reply and replies buttons carry `aria-keyshortcuts` and a `title` hint, with no caps in the action row.
- **Activity tooltip:** one dark tooltip (Ink on Background inverted) follows the hovered cell, edge-aligned near the ends ("3 commits on Tue, Oct 14", "2 commits between 14:00 and 15:00 today"); hovering a day or hour tints that period's commits in the profile feed with Primary Soft.

### Orbit Rail
Signed in, every log tab carries one 84px row between the sticky tabs and the composer: the people you follow who are live, soonest-ending first, at most 24. The first slot is always You, a dashed Rail Gray ring with a plus that opens the Go live dialog, or your own live ring while you stream, so the row never pops in or changes height. Each item is 56px wide with 4px gaps: a 44px live ring (2px conic lane ring, 2px gap, 36px avatar) with an 8px mono LIVE pill on its bottom edge, and a 10px mono @handle under it. The row uses the column's full width and carries no count: the rings say who is live. Overflow scrolls sideways with scroll-snap, faded edges and no scrollbar chrome; ←/→/Home/End move between items, which are buttons labelled "{name} is live on {Platform} until 18:00".

Hovering an item (mouse only, 170ms intent) opens one 520px Popover card anchored to it and clamped inside the feed column: it floats and never pushes content, swaps instantly to another avatar while open, and closes 200ms after the pointer leaves. It blooms from the ring (≈620ms, off under reduced motion) while a lane line drops from the ring to a node on the card's top edge. Click, Enter, or pressing play pins it and moves focus in; Esc closes it and returns focus. The card holds a 16:9 thumbnail whose play button swaps in the platform's player (only when embeds allow it; otherwise it links out), the name, mono handle and platform glyph, the latest commit (lane dot, relative time, short hash, three lines, linking to it), "live until 18:00 · ends in 1h 08m" in tabular mono, Watch on YouTube or Join on Twitch, and View profile. Only real data: no viewer counts, titles, or chat. The rail refreshes itself every 30s on every log tab, paused while the tab is hidden, and drops a stream the moment it ends. On touch a tap opens the same card in a sheet. The aside's Live now list is left out on log pages, since the rail shows the same people; elsewhere it lists the five streams ending soonest.

### User Preview
Hovering an avatar, name, or handle (mouse only, 450ms delay, 200ms grace to move into it) opens a 304px Popover preview below the trigger, flipping above near the viewport bottom: a 56px avatar ringed in the user's lane, Follow / Following (Unfollow on hover) top right, name, mono handle with lane dot and a "Follows you" chip, a 3-line bio, and mono counts linking to following and followers. Follow toggles optimistically and rolls back on error. Closes on scroll, Escape, or leaving.
Hovering an avatar, name, or handle (mouse only, 450ms delay, 200ms grace to move into it) opens a 304px Popover preview below the trigger, flipping above near the viewport bottom: a 56px avatar ringed in the user's lane (the live ring while they stream), Follow / Following (Unfollow on hover) top right, name, mono handle with lane dot and a "Follows you" chip, a 3-line bio, and mono counts linking to following and followers. While they stream, a hairline-divided stream card follows: thumbnail, "{name} is live", the platform, "live until 18:00 · ends in 1h 08m", and Watch on YouTube or Join on Twitch. Its thumbnail links out instead of playing, because the preview closes on scroll. Follow toggles optimistically and rolls back on error. Closes on scroll, Escape, or leaving.

### Notifications
The inbox is a log too: every row hangs on a Rail Gray rail whose node is a 36px hollow circle holding the type icon (person-plus, reply, quote, star, at, check, cross, chevrons-up). A sticky header carries the "Notifications" title over a row of page tabs (All, Replies, Quotes, Stars, Mentions, Follows, Moderation) with the sliding Commit Blue underline; on phones the row scrolls sideways with the current filter kept in view. Day markers ("Today", "Yesterday", "Wed, Oct 14") split the rows the way they split the log. A row opens with a line of stacked 32px avatars (up to three, then a mono `+N` disc) and a mono relative time pushed right, then the sentence: the actor's name (600), their mono handle when they act alone, otherwise "and N others", and the action in plain prose. Words someone else wrote (a reply, quote, or mention) follow at 15px like a commit body; your own commit (a star, an approval, a rejection) sits underneath as a hairline-bordered, muted block clamped to three lines. A level-up row replaces the avatars with a mono `Lv. 5 · wheel` chip tinted in the user's lane, says "You reached level 5: wheel." over the rank's description, and opens the profile. Moderation rows replace the avatars with a mono `approved` or `rejected` chip, and a rejection prints a mono `reason:` key before the moderator's words. Groups expand in place from a Commit Blue toggle ("5 stars" / "Hide stars"), listing one compact row per event behind a 1px Rail Gray guide line, each follower with their own Follow button. Rows the inbox marked read on this visit ring their node in Commit Blue and sit above a `HEAD@{last visit}` marker. While you read, polling adds new rows above without moving the ones on screen, and a Commit Blue button ("↑ 1 new notification") takes you to them. Empty filters speak in git's voice (`$ cat /dev/notifications | grep stars`) with a link back to all notifications; the end of the list is a hollow root node: "That's the oldest notification. There's nothing before it."
Expand Down
8 changes: 5 additions & 3 deletions KNOWLEDGE_BASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,13 @@ Sources: [profile page](resources/js/pages/user/show.tsx), [profile settings](re

### Going live

Share a stream you are running from the **…** menu on your own profile: **Go live** takes a link to a YouTube or Twitch stream (on YouTube: a `/live/` link, a watch link, an embed, or one with a start time all work; on Twitch: your channel link) and how long to show it, from **1 to 12 hours** (two by default). YouTube and Twitch are supported. While you are live, your avatar gets a red ring and a **Live** marker on your profile, in logs, and in hover cards. Hovering it (or tapping it on a touch screen) shows the stream's live thumbnail with a **Watch** button; the stream itself plays on the provider in a new tab, never embedded. On your profile, the avatar links straight to the stream.
Share a stream you are running from the **…** menu on your own profile: **Go live** takes a link to a YouTube or Twitch stream (on YouTube: a `/live/` link, a watch link, an embed, or one with a start time all work; on Twitch: your channel link) and how long to show it, from **1 to 12 hours** (two by default). YouTube and Twitch are supported. While you are live, your avatar gets a slowly turning ring in your lane color and a **Live** marker on your profile, in logs, in hover cards, and in the side column. Hovering a live avatar on a profile, or tapping one on a profile or in a log on a touch screen, shows the stream's live thumbnail with when it ends and a **Watch** button (**Join** on Twitch). Pressing play on the thumbnail plays the stream right there, through the same click-to-play [embedded players](#links-link-cards-and-embedded-players) as links, so the same embed configuration applies: when embeds are off or the platform isn't allowed, the stream opens on the provider in a new tab instead. The hover card you get from an avatar or name in a log shows the same thumbnail and button, but there the thumbnail only opens the stream on the provider, since the card closes as you scroll. On your profile, the avatar links straight to the stream.

Signed in, every log tab (Following, Latest and Trending) opens with the **Orbit Rail**: one row of the people you follow who are live right now, soonest-ending first, up to 24 (muted and blocked people are left out). Your own slot always comes first: it opens the same **Go live** dialog, and while you stream it shows your own card with **End stream**. Hovering someone (or tapping on a touch screen) opens their card: the stream's thumbnail, which plays the stream in place like above, their name and platform, their latest commit (when they have one), when the stream ends in your time zone ("live until 18:00 · ends in 1h 08m"), and links to watch it and to their profile. Clicking someone keeps the card open until you press Esc, click elsewhere, or click them again. The rail refreshes itself every 30 seconds while the tab is in front, and drops streams as they end. Other pages show the five streams ending soonest in the side column's **Live now** box, each linking out to the stream with when it ends; logs leave that box out, since the rail already shows them. Signed out, neither appears.

**End stream** in the same menu stops it at once; otherwise it stops showing when the chosen time runs out, and the scheduler clears it within five minutes. Only the platform, the link, and the end time are stored, and they are included in your export.

Sources: [go-live menu](resources/js/components/profile-actions-menu.tsx), [live avatar and card](resources/js/components/live-stream-card.tsx), [validation](app/Http/Requests/UpdateUserLiveRequest.php), [YouTube links](app/Services/LivePlatforms/YouTube.php), [Twitch links](app/Services/LivePlatforms/Twitch.php), [ending streams](app/Jobs/EndLiveStreams.php).
Sources: [go-live menu](resources/js/components/profile-actions-menu.tsx), [go-live dialog](resources/js/components/go-live-dialog.tsx), [live avatar](resources/js/components/live/live-avatar.tsx), [live card](resources/js/components/live-stream-card.tsx), [Orbit Rail](resources/js/components/orbit-rail/orbit-rail.tsx), [who is live](app/Queries/LiveFollowingQuery.php), [validation](app/Http/Requests/UpdateUserLiveRequest.php), [YouTube links](app/Services/LivePlatforms/YouTube.php), [Twitch links](app/Services/LivePlatforms/Twitch.php), [ending streams](app/Jobs/EndLiveStreams.php).

### Achievement badges and XP

Expand Down Expand Up @@ -144,7 +146,7 @@ Sources: [badge definitions](app/Enums/Achievement.php), [unlock conditions and

### Following developers and suggestions

Follow or unfollow from a developer's profile or a follow control elsewhere in the app. Following puts their commits in your Following log. Following or unfollowing from a log updates the controls, counts, and live-following sidebar without navigating away: Latest and Trending keep their loaded commits and ranking, while Following updates its commits and empty-state suggestions. You cannot follow yourself or follow across a block. GitHub follows are imported once: connections to developers already on /dev/null are added in both directions, with up to 1,000 GitHub connections checked each way. This is not continuous synchronization, so unfollowing someone on /dev/null sticks.
Follow or unfollow from a developer's profile or a follow control elsewhere in the app. Following puts their commits in your Following log. Following or unfollowing from a log updates the controls, counts, and the Orbit Rail of who is live without navigating away: Latest and Trending keep their loaded commits and ranking, while Following updates its commits and empty-state suggestions. You cannot follow yourself or follow across a block. GitHub follows are imported once: connections to developers already on /dev/null are added in both directions, with up to 1,000 GitHub connections checked each way. This is not continuous synchronization, so unfollowing someone on /dev/null sticks.

**Who to follow** suggests developers in the side column or within the log on smaller screens, each with the first line of their bio and why they are suggested (how many people you follow follow them, or their follower count). Follow from the icon next to each suggestion; for someone who already follows you, it follows back. The X icon marks a suggestion **Not interested**: that developer is never suggested to you again, without muting them, and the next suggestion takes their place. Refresh for different suggestions, or use **… → Don't show again** to hide the block, with an immediate **Undo** option. Show or hide these suggestions in Settings → Appearance. Suggested developers must have a visible top-level commit. On profiles, suggestions prioritize the people that developer follows; on commit pages, they prioritize visible thread participants. Regular suggestions fill any remaining places. You can also follow back from the notifications list.

Expand Down
3 changes: 2 additions & 1 deletion PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ What exists, one line each (details in the knowledge base):
- **Commits:** text with code, images, link cards and embedded players, or a poll; replies, quotes, threads, scheduling, translation.
- **Logs:** Following, Latest, and Trending.
- **Engagement:** follows, stars, saved commits, pins, mentions, and pastéis (an opt-in daily allowance of pastries).
- **Profiles:** mirrored from GitHub, with an activity graph, streaks, achievements, a Unix rank, and a live marker while you stream on YouTube.
- **Profiles:** mirrored from GitHub, with an activity graph, streaks, achievements, a Unix rank, and a live marker while you stream on YouTube or Twitch.
- **Live:** the Orbit Rail above every log shows who you follow is streaming right now, with their latest commit and the stream one click from playing. Only what the link and the clock can tell: no viewer counts, titles, or chat.
- **Remotes:** private one-to-one and group messages (orgs).
- **Notifications:** in-app activity, plus independently controlled browser and sound alerts for Remotes while the site is open, enabled by default; only browser banners require notification permission.
- **Search:** developers by name, commits by meaning.
Expand Down
6 changes: 4 additions & 2 deletions app/Actions/UpdateUserLive.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
{
public function handle(User $user, LivePlatform $platform, string $url, int $hours): void
{
$endsAt = now()->addHours($hours);

$user->update([
'live_platform' => $platform,
'live_url' => $platform->driver()->stream($url)->url,
'live_ends_at' => now()->addHours($hours),
'live_url' => $platform->driver()->stream($url, $endsAt)->url,
'live_ends_at' => $endsAt,
]);
}
}
5 changes: 3 additions & 2 deletions app/Contracts/Services/LivePlatformDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
namespace App\Contracts\Services;

use App\Dto\LiveStream;
use Carbon\CarbonInterface;

interface LivePlatformDriver
{
public function accepts(string $url): bool;

/**
* The stream an accepted link points to, worked out from the link alone.
* The stream an accepted link points to, worked out from the link alone, live until the given time.
*/
public function stream(string $url): LiveStream;
public function stream(string $url, CarbonInterface $endsAt): LiveStream;
}
Loading