atproto pds in zig pds.zat.dev
pds atproto
24

Configure Feed

Select the types of activity you want to include in your feed.

landing: add activity timescale

zzstoatzz (May 27, 2026, 10:36 PM -0500) 3dd13e50 cf53eb63

+123 -11
+12 -5
src/http/landing/assets.zig
··· 81 81 \\h1 { margin: 0; color: var(--text); font-size: clamp(36px, 11vw, 74px); line-height: 0.95; letter-spacing: 0; } 82 82 \\.lede { margin: 14px 0 0; max-width: 47ch; color: var(--muted); font-size: 15px; } 83 83 \\.lede a, .lede code { color: var(--green); } 84 - \\.entropy { width: min(100%, 300px); min-height: 64px; margin-top: 19px; display: grid; gap: 8px; } 85 - \\.entropy-label { color: var(--dim); font-size: 10px; opacity: 0; transform: translateY(3px); transition: opacity .14s ease, transform .14s ease; } 86 - \\.entropy:hover .entropy-label, .entropy:focus-within .entropy-label { opacity: 1; transform: translateY(0); } 84 + \\.entropy { width: min(100%, 330px); min-height: 76px; margin-top: 19px; display: grid; gap: 6px; } 85 + \\.entropy-label { color: var(--muted); font-size: 10px; } 87 86 \\.entropy-bars { 88 87 \\ position: relative; display: flex; align-items: end; gap: 4px; min-height: 43px; padding-bottom: 5px; 89 88 \\ border-bottom: 1px solid color-mix(in srgb, var(--line) 78%, transparent); ··· 102 101 \\ transition: opacity .16s ease, transform .16s ease, filter .16s ease; 103 102 \\} 104 103 \\.entropy-bars i:hover { opacity: 1; transform: translateY(-3px); filter: saturate(1.18) brightness(1.05); } 104 + \\.entropy-bars i:hover::after { 105 + \\ content: attr(data-label); position: absolute; left: 50%; bottom: calc(100% + 8px); z-index: 2; 106 + \\ width: max-content; max-width: min(72vw, 260px); padding: 5px 7px; border: 1px solid var(--line); 107 + \\ border-radius: 8px; background: color-mix(in srgb, var(--panel) 94%, transparent); color: var(--text); 108 + \\ font-size: 10px; line-height: 1.25; white-space: normal; transform: translateX(-50%); 109 + \\ box-shadow: 0 12px 28px rgba(0,0,0,.22); 110 + \\} 111 + \\.entropy-axis { display: flex; justify-content: space-between; color: var(--dim); font-size: 9px; } 105 112 \\.signal-empty { color: var(--dim); font-size: 11px; } 106 113 \\@media (prefers-reduced-motion: reduce) { .entropy-label, .entropy-bars i { transition: none; } } 107 114 \\.pulse { ··· 210 217 \\ .icon-link, .settings { width: 32px; height: 32px; border-radius: 10px; } 211 218 \\ .logo-link::after { inset: 5px; border-radius: 8px; } 212 219 \\ .icon-link img { width: 20px; height: 20px; } 213 - \\ .entropy { width: min(100%, 230px); min-height: 54px; } 214 - \\ .entropy-label { opacity: 1; transform: none; } 220 + \\ .entropy { width: min(100%, 250px); min-height: 68px; } 215 221 \\ .entropy-bars { gap: 3px; min-height: 36px; } 222 + \\ .entropy-bars i:hover::after { display: none; } 216 223 \\} 217 224 ; 218 225
+111 -6
src/http/landing/entropy.zig
··· 51 51 max_count = @max(max_count, bucket.count); 52 52 total_count += bucket.count; 53 53 } 54 - try html.writer.print("<span class=\"entropy-label\">{d} writes over time</span>", .{total_count}); 55 - try html.writer.writeAll("<span class=\"entropy-bars\" aria-hidden=\"true\">"); 54 + const first_label = try formatDate(allocator, activity[0].start_us); 55 + const last_label = try formatDate(allocator, activity[activity.len - 1].end_us); 56 + const range_label = try formatRange(allocator, activity[0].start_us, activity[activity.len - 1].end_us); 57 + try html.writer.print( 58 + \\<span class="entropy-label">{d} writes · {s}</span> 59 + \\<span class="entropy-bars" aria-hidden="true"> 60 + , .{ total_count, range_label }); 56 61 var i: usize = 0; 57 62 while (i < activity.len) : (i += 1) { 58 63 const bucket = activity[i]; 59 - const height = 6 + (bucket.count * 34 / max_count); 64 + const height = scaledHeight(bucket.count, max_count); 60 65 const offset = i % 3; 66 + const bucket_label = try formatBucketLabel(allocator, bucket); 61 67 try html.writer.print( 62 - \\<i style="--h:{d}px;--y:{d}px" title="{d} writes"></i> 63 - , .{ height, offset, bucket.count }); 68 + \\<i style="--h:{d}px;--y:{d}px" title="{s}" data-label="{s}"></i> 69 + , .{ height, offset, bucket_label, bucket_label }); 64 70 } 65 - try html.writer.writeAll("</span>"); 71 + try html.writer.print( 72 + \\</span> 73 + \\<span class="entropy-axis"><span>{s}</span><span>{s}</span></span> 74 + , .{ first_label, last_label }); 66 75 } 67 76 try html.writer.writeAll("</div>"); 68 77 return .{ .style = style, .html = try html.toOwnedSlice() }; 69 78 } 79 + 80 + fn scaledHeight(count: u64, max_count: u64) u64 { 81 + if (count == 0) return 5; 82 + const count_root = std.math.sqrt(@as(f64, @floatFromInt(count))); 83 + const max_root = std.math.sqrt(@as(f64, @floatFromInt(@max(max_count, 1)))); 84 + return 7 + @as(u64, @intFromFloat(@round(count_root * 34.0 / max_root))); 85 + } 86 + 87 + fn formatRange(allocator: std.mem.Allocator, first_us: u64, last_us: u64) ![]const u8 { 88 + const first = civilFromMicros(first_us); 89 + const last = civilFromMicros(last_us); 90 + if (first.year == last.year and first.month == last.month and first.day == last.day) { 91 + return std.fmt.allocPrint(allocator, "{s} {d}", .{ monthName(first.month), first.day }); 92 + } 93 + if (first.year == last.year) { 94 + return std.fmt.allocPrint(allocator, "{s} {d}-{s} {d}", .{ monthName(first.month), first.day, monthName(last.month), last.day }); 95 + } 96 + return std.fmt.allocPrint(allocator, "{s} {d} {d}-{s} {d} {d}", .{ monthName(first.month), first.day, first.year, monthName(last.month), last.day, last.year }); 97 + } 98 + 99 + fn formatDate(allocator: std.mem.Allocator, timestamp_us: u64) ![]const u8 { 100 + const civil = civilFromMicros(timestamp_us); 101 + return std.fmt.allocPrint(allocator, "{s} {d}", .{ monthName(civil.month), civil.day }); 102 + } 103 + 104 + fn formatBucketLabel(allocator: std.mem.Allocator, bucket: store.ActivityBucket) ![]const u8 { 105 + const start = civilFromMicros(bucket.start_us); 106 + const end = civilFromMicros(bucket.end_us); 107 + if (start.year == end.year and start.month == end.month and start.day == end.day) { 108 + return std.fmt.allocPrint( 109 + allocator, 110 + "{d} writes · {s} {d}, {d:0>2}:{d:0>2}-{d:0>2}:{d:0>2} UTC", 111 + .{ bucket.count, monthName(start.month), start.day, start.hour, start.minute, end.hour, end.minute }, 112 + ); 113 + } 114 + return std.fmt.allocPrint( 115 + allocator, 116 + "{d} writes · {s} {d}-{s} {d}", 117 + .{ bucket.count, monthName(start.month), start.day, monthName(end.month), end.day }, 118 + ); 119 + } 120 + 121 + const Civil = struct { 122 + year: i64, 123 + month: u8, 124 + day: u8, 125 + hour: u8, 126 + minute: u8, 127 + }; 128 + 129 + fn civilFromMicros(timestamp_us: u64) Civil { 130 + const timestamp_s: i64 = @intCast(timestamp_us / std.time.us_per_s); 131 + const days = @divFloor(timestamp_s, std.time.s_per_day); 132 + const seconds_of_day = @mod(timestamp_s, std.time.s_per_day); 133 + const date = civilFromDays(days); 134 + return .{ 135 + .year = date.year, 136 + .month = date.month, 137 + .day = date.day, 138 + .hour = @intCast(@divFloor(seconds_of_day, std.time.s_per_hour)), 139 + .minute = @intCast(@divFloor(@mod(seconds_of_day, std.time.s_per_hour), std.time.s_per_min)), 140 + }; 141 + } 142 + 143 + fn civilFromDays(days_since_epoch: i64) struct { year: i64, month: u8, day: u8 } { 144 + const z = days_since_epoch + 719468; 145 + const era = if (z >= 0) @divFloor(z, 146097) else @divFloor(z - 146096, 146097); 146 + const doe: u64 = @intCast(z - era * 146097); 147 + const yoe = @divFloor(doe - @divFloor(doe, 1460) + @divFloor(doe, 36524) - @divFloor(doe, 146096), 365); 148 + var year: i64 = @intCast(yoe); 149 + year += era * 400; 150 + const doy = doe - (365 * yoe + @divFloor(yoe, 4) - @divFloor(yoe, 100)); 151 + const mp = @divFloor(5 * doy + 2, 153); 152 + const day = doy - @divFloor(153 * mp + 2, 5) + 1; 153 + const month = if (mp < 10) mp + 3 else mp - 9; 154 + year += if (month <= 2) 1 else 0; 155 + return .{ .year = year, .month = @intCast(month), .day = @intCast(day) }; 156 + } 157 + 158 + fn monthName(month: u8) []const u8 { 159 + return switch (month) { 160 + 1 => "Jan", 161 + 2 => "Feb", 162 + 3 => "Mar", 163 + 4 => "Apr", 164 + 5 => "May", 165 + 6 => "Jun", 166 + 7 => "Jul", 167 + 8 => "Aug", 168 + 9 => "Sep", 169 + 10 => "Oct", 170 + 11 => "Nov", 171 + 12 => "Dec", 172 + else => "?", 173 + }; 174 + }