Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

fix(profile): responsive layout + fix HUD corner label overlap

Redesign @handle profile pages with portrait/landscape responsive
layouts, visual stat bars with per-category colors, status indicator
dots for online/offline presence, activity type color coding, and
section dividers. Content now starts below the HUD corner label
(y=22) so it never overlaps the prompt label. Refreshing indicator
also moved below HUD area. Small screens get compact stat display,
very small screens skip stats entirely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+218 -92
+218 -92
system/public/aesthetic.computer/disks/profile.mjs
··· 7 7 const MOOD_LIMIT = 40; 8 8 const CHAT_LIMIT = 120; 9 9 10 + const { max, floor } = Math; 11 + 10 12 let debug; 11 13 let visiting; 12 14 let profile; ··· 123 125 124 126 function paint({ api, wipe, help, ink, screen, ui, pen }) { 125 127 if (!pen?.drawing) wipe(98); 126 - ink(127).line(); 127 128 128 129 if (!visiting) { 129 130 paintNoProfileState({ api, help, ink, screen, ui }); 130 131 return; 131 132 } 132 133 133 - let y = 6; 134 + // Reserve vertical space so content never overlaps the HUD corner label. 135 + const HUD_H = 22; 136 + const M = 4; 137 + const BTN_H = 16; 138 + const isLandscape = screen.width > screen.height && screen.width >= 200; 134 139 135 - ink(255).write(visiting, { x: 4, y }, "black", screen.width - 8); 136 - y += 10; 137 - 140 + // Loading / error states (centered, no layout needed) 138 141 if (!profile && noprofile === FETCHING) { 139 142 ink(255).write( 140 143 `${FETCHING}${ellipsisTicker.text(help.repeat)}`, ··· 149 152 return; 150 153 } 151 154 152 - if (profile?.mood) { 153 - ink(255).write(`Mood: ${profile.mood}`, { x: 4, y }, "black", screen.width - 8); 154 - } else { 155 - ink(127).write("Mood: -", { x: 4, y }, "black", screen.width - 8); 156 - } 157 - y += 12; 158 - 159 - const counts = scorecard.counts; 160 - ink(255).write( 161 - `Paint ${counts.paintings} Piece ${counts.pieces} Kid ${counts.kidlisp} Clock ${counts.clocks} Tape ${counts.tapes}`, 162 - { x: 4, y }, 163 - "black", 164 - screen.width - 8, 165 - ); 166 - y += 10; 167 - ink(255).write( 168 - `Mood ${counts.moods} Chat ${counts.chats}`, 169 - { x: 4, y }, 170 - "black", 171 - screen.width - 8, 172 - ); 173 - y += 12; 174 - 175 - ink("yellow").write("Live", { x: 4, y }, "black", screen.width - 8); 176 - y += 10; 155 + // Thin divider below HUD area 156 + ink(50).line(0, HUD_H - 2, screen.width, HUD_H - 2); 177 157 178 - const onlineText = presence.online ? "online" : "offline"; 179 - const currentPiece = formatPieceLabel(presence.currentPiece) || "-"; 180 - const pingText = presence.ping ? ` ${presence.ping}ms` : ""; 181 - ink(255).write( 182 - `${onlineText} piece ${currentPiece}${pingText}`, 183 - { x: 4, y }, 184 - "black", 185 - screen.width - 8, 186 - ); 187 - y += 10; 158 + const maxY = screen.height - BTN_H - 2; 188 159 189 - if (presence.worldPiece || presence.showing) { 190 - ink(255).write( 191 - `world ${presence.worldPiece || "-"} showing ${presence.showing || "-"}`, 192 - { x: 4, y }, 193 - "black", 194 - screen.width - 8, 195 - ); 196 - y += 10; 160 + if (isLandscape) { 161 + paintLandscapeLayout({ help, ink, screen, HUD_H, M, maxY }); 162 + } else { 163 + paintPortraitLayout({ help, ink, screen, HUD_H, M, maxY }); 197 164 } 198 165 199 - if (!presence.online && presence.lastSeenAt) { 200 - ink(127).write( 201 - `last seen ${formatTimeAgo(presence.lastSeenAt)}`, 202 - { x: 4, y }, 203 - "black", 204 - screen.width - 8, 205 - ); 206 - y += 10; 207 - } 166 + // Bottom action buttons 167 + paintingsBtn ||= new ui.TextButton("Paintings", { 168 + x: M, 169 + y: screen.height - BTN_H + 1, 170 + }); 171 + refreshBtn ||= new ui.TextButton("Refresh", { 172 + x: screen.width - 47, 173 + y: screen.height - BTN_H + 1, 174 + }); 175 + paintingsBtn.paint(api); 176 + refreshBtn.paint(api); 208 177 209 - y += 2; 210 - ink("yellow").write("Recent Activity", { x: 4, y }, "black", screen.width - 8); 211 - y += 10; 212 - 213 - if ((loading || refreshing) && scorecard.activity.length === 0) { 214 - ink(255).write( 215 - `${FETCHING}${ellipsisTicker.text(help.repeat)}`, 216 - { x: 4, y }, 217 - "black", 218 - screen.width - 8, 219 - ); 220 - y += 10; 221 - } else if (scorecard.activity.length === 0) { 222 - const emptyText = dataError || "No activity yet."; 223 - ink(127).write(emptyText, { x: 4, y }, "black", screen.width - 8); 224 - y += 10; 225 - } else { 226 - const maxRows = Math.max(3, Math.floor((screen.height - y - 24) / 10)); 227 - const rows = scorecard.activity.slice(0, maxRows); 228 - rows.forEach((item) => { 229 - const when = item.when ? formatTimeAgo(item.when) : "recent"; 230 - ink(255).write(`${when} ${item.label}`, { x: 4, y }, "black", screen.width - 8); 231 - y += 10; 232 - }); 233 - } 234 - 178 + // Refreshing indicator (below HUD, right-aligned) 235 179 if (refreshing) { 236 - ink(127).write( 180 + ink(100).write( 237 181 `Refreshing${ellipsisTicker.text(help.repeat)}`, 238 - { x: screen.width - 84, y: 2 }, 182 + { x: screen.width - 84, y: HUD_H }, 239 183 "black", 240 - 82, 184 + 80, 241 185 ); 242 186 } 243 - 244 - paintingsBtn ||= new ui.TextButton("Paintings", { x: 4, y: screen.height - 14 }); 245 - refreshBtn ||= new ui.TextButton("Refresh", { x: screen.width - 47, y: screen.height - 14 }); 246 - 247 - paintingsBtn.paint(api); 248 - refreshBtn.paint(api); 249 187 } 250 188 251 189 function act({ event: e, jump, store, user }) { ··· 308 246 309 247 noprofileBtn ||= new ui.TextButton(label, { center: "xy", screen }); 310 248 noprofileBtn.paint(api); 249 + } 250 + 251 + // --- Responsive layout helpers --- 252 + 253 + function paintPortraitLayout({ help, ink, screen, HUD_H, M, maxY }) { 254 + let y = HUD_H; 255 + const w = screen.width - M * 2; 256 + const useCompactStats = screen.height < 250; 257 + 258 + // Mood 259 + if (profile?.mood) { 260 + ink(220).write(profile.mood, { x: M, y }, "black", w); 261 + } else { 262 + ink(80).write("no mood", { x: M, y }, "black", w); 263 + } 264 + y += 12; 265 + 266 + // Presence 267 + y = paintPresenceBlock(ink, y, M, w); 268 + 269 + // Stats (skip on very small screens) 270 + if (screen.height >= 180) { 271 + ink(50).line(M, y, screen.width - M, y); 272 + y += 4; 273 + y = paintStatsBlock(ink, y, M, w, useCompactStats); 274 + } 275 + 276 + // Divider before activity 277 + ink(50).line(M, y, screen.width - M, y); 278 + y += 4; 279 + 280 + paintActivityBlock(help, ink, y, maxY, M, w); 281 + } 282 + 283 + function paintLandscapeLayout({ help, ink, screen, HUD_H, M, maxY }) { 284 + const midX = floor(screen.width / 2); 285 + const leftW = midX - M * 2; 286 + const rightX = midX + M; 287 + const rightW = screen.width - midX - M * 2; 288 + 289 + // Vertical divider 290 + ink(50).line(midX - 1, HUD_H, midX - 1, maxY); 291 + 292 + // Left column: mood + presence + stats 293 + let y = HUD_H; 294 + 295 + if (profile?.mood) { 296 + ink(220).write(profile.mood, { x: M, y }, "black", leftW); 297 + } else { 298 + ink(80).write("no mood", { x: M, y }, "black", leftW); 299 + } 300 + y += 12; 301 + 302 + y = paintPresenceBlock(ink, y, M, leftW); 303 + ink(50).line(M, y, midX - M, y); 304 + y += 4; 305 + paintStatsBlock(ink, y, M, leftW, false); 306 + 307 + // Right column: activity 308 + paintActivityBlock(help, ink, HUD_H, maxY, rightX, rightW); 309 + } 310 + 311 + function paintPresenceBlock(ink, y, x, w) { 312 + const on = presence.online; 313 + 314 + // Colored status indicator dot 315 + ink(on ? 80 : 70, on ? 220 : 70, on ? 80 : 70).box(x, y + 2, 5, 5); 316 + 317 + const status = on ? "online" : "offline"; 318 + const piece = formatPieceLabel(presence.currentPiece) || "-"; 319 + const ping = presence.ping ? ` ${presence.ping}ms` : ""; 320 + ink(on ? 255 : 120).write( 321 + `${status} ${piece}${ping}`, 322 + { x: x + 8, y }, 323 + "black", 324 + w - 8, 325 + ); 326 + y += 10; 327 + 328 + if (presence.worldPiece || presence.showing) { 329 + ink(160).write( 330 + `world ${presence.worldPiece || "-"} showing ${presence.showing || "-"}`, 331 + { x: x + 8, y }, 332 + "black", 333 + w - 8, 334 + ); 335 + y += 10; 336 + } 337 + 338 + if (!on && presence.lastSeenAt) { 339 + ink(90).write( 340 + `seen ${formatTimeAgo(presence.lastSeenAt)}`, 341 + { x: x + 8, y }, 342 + "black", 343 + w - 8, 344 + ); 345 + y += 10; 346 + } 347 + 348 + y += 2; 349 + return y; 350 + } 351 + 352 + function paintStatsBlock(ink, y, x, w, compact) { 353 + const c = scorecard.counts; 354 + 355 + if (compact) { 356 + ink(255).write( 357 + `P${c.paintings} Pc${c.pieces} K${c.kidlisp} Cl${c.clocks} T${c.tapes}`, 358 + { x, y }, 359 + "black", 360 + w, 361 + ); 362 + y += 10; 363 + ink(255).write(`M${c.moods} Ch${c.chats}`, { x, y }, "black", w); 364 + y += 12; 365 + return y; 366 + } 367 + 368 + ink("yellow").write("Stats", { x, y }, "black", w); 369 + y += 10; 370 + 371 + const rows = [ 372 + { label: "Paint", val: c.paintings, r: 100, g: 200, b: 255 }, 373 + { label: "Piece", val: c.pieces, r: 255, g: 180, b: 80 }, 374 + { label: "Kid", val: c.kidlisp, r: 120, g: 255, b: 120 }, 375 + { label: "Clock", val: c.clocks, r: 255, g: 200, b: 80 }, 376 + { label: "Tape", val: c.tapes, r: 200, g: 140, b: 255 }, 377 + { label: "Mood", val: c.moods, r: 255, g: 140, b: 180 }, 378 + { label: "Chat", val: c.chats, r: 200, g: 200, b: 200 }, 379 + ]; 380 + 381 + const hi = max(1, ...rows.map((s) => s.val)); 382 + const labelEnd = x + 60; 383 + const barMax = max(8, w - 64); 384 + 385 + rows.forEach((s) => { 386 + ink(255).write(`${s.label} ${s.val}`, { x, y }, "black", 58); 387 + if (s.val > 0) { 388 + const barW = max(1, floor((s.val / hi) * barMax)); 389 + ink(s.r, s.g, s.b).box(labelEnd, y + 2, barW, 5); 390 + } 391 + y += 9; 392 + }); 393 + 394 + y += 2; 395 + return y; 396 + } 397 + 398 + const ACTIVITY_COLORS = { 399 + mood: { r: 180, g: 120, b: 255 }, 400 + painting: { r: 100, g: 200, b: 255 }, 401 + kidlisp: { r: 120, g: 255, b: 120 }, 402 + clock: { r: 255, g: 200, b: 80 }, 403 + chat: { r: 200, g: 200, b: 200 }, 404 + }; 405 + 406 + function paintActivityBlock(help, ink, y, maxY, x, w) { 407 + ink("yellow").write("Recent Activity", { x, y }, "black", w); 408 + y += 10; 409 + 410 + if ((loading || refreshing) && scorecard.activity.length === 0) { 411 + ink(200).write( 412 + `${FETCHING}${ellipsisTicker.text(help.repeat)}`, 413 + { x, y }, 414 + "black", 415 + w, 416 + ); 417 + return; 418 + } 419 + 420 + if (scorecard.activity.length === 0) { 421 + ink(80).write(dataError || "No activity yet.", { x, y }, "black", w); 422 + return; 423 + } 424 + 425 + const lineH = 10; 426 + const maxRows = max(3, floor((maxY - y) / lineH)); 427 + const rows = scorecard.activity.slice(0, maxRows); 428 + 429 + rows.forEach((item) => { 430 + if (y + lineH > maxY) return; 431 + const c = ACTIVITY_COLORS[item.type] || { r: 180, g: 180, b: 180 }; 432 + ink(c.r, c.g, c.b).box(x, y + 3, 3, 3); 433 + const when = item.when ? formatTimeAgo(item.when) : "recent"; 434 + ink(200).write(`${when} ${item.label}`, { x: x + 6, y }, "black", w - 6); 435 + y += lineH; 436 + }); 311 437 } 312 438 313 439 async function refreshProfile(force = false) {