Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

fix: remove uniticker from prompt for now

Removes the unified ticker system (chat, clock, media, commits, moods)
and its content tooltip/preview system (~970 lines).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+1 -1016
+1 -1016
system/public/aesthetic.computer/disks/prompt.mjs
··· 214 214 let contentTickerButton; // Button for content ticker hover interaction 215 215 let mediaPreviewBox; // Shared preview box renderer for all media types 216 216 217 - // 🎰 UNITICKER - Unified ticker combining chat, laer-klokken, and media content 218 - let uniticker; // Single ticker for all combined content 219 - let unitickerButton; // Button for hover interaction 220 - let unitickerItems = []; // Combined items: {type: 'chat'|'clock'|'media', text: string, code: string, ...} 221 - let unitickerHoveredItem = null; // Currently hovered item for tooltip 222 - let unitickerTooltipVisible = false; // Whether to show the "Enter code" tooltip 223 - // Idle auto-select state 224 - let unitickerIdleFrames = 0; // Frames since last pen movement 225 - let unitickerLastPenX = -1; // Last pen X position 226 - let unitickerLastPenY = -1; // Last pen Y position 227 - let unitickerAutoSelectedItem = null; // Auto-selected item when idle 228 - let unitickerAutoSelectedX = 0; // X position of auto-selected item 229 - let unitickerAutoSelectedWidth = 0; // Width of auto-selected item 230 - const UNITICKER_IDLE_THRESHOLD = 120; // 2 seconds at 60fps before auto-selecting 231 217 232 218 const tinyTickers = true; // Use MatrixChunky8 font for tighter, smaller tickers 233 219 let contentItems = []; // Store fetched content: {type: 'kidlisp'|'painting'|'tape', code: string, source?: string} ··· 5825 5811 }); 5826 5812 } 5827 5813 5828 - // Stats / Analytics - UNITICKER System (unified ticker combining chat, laer-klokken, and media) 5829 - $.layer(2); // Render ticker on top of tooltips 5830 - 5831 - const loginY = screen.height / 2; 5832 - 5833 - // Calculate dynamic positioning 5834 - const tickerHeight = 8; // MatrixChunky8 is 8px 5835 - const tickerPadding = 5; // Padding around ticker 5836 - const tickerFont = "MatrixChunky8"; 5837 - 5838 - // Helper to ensure ticker text is long enough to fill screen without gaps 5839 - // Repeats the content until it's at least 4x screen width for seamless looping 5840 - const ensureMinTickerLength = (text, separator = " · ") => { 5841 - if (!text) return text; 5842 - const charWidth = 4; // MatrixChunky8 char width estimate 5843 - const minWidth = screen.width * 4; // 4x screen width for smooth seamless loop 5844 - const textWidthApprox = text.length * charWidth; 5845 - if (textWidthApprox < minWidth) { 5846 - const repeats = Math.ceil(minWidth / textWidthApprox) + 1; 5847 - return Array(repeats).fill(text).join(separator); 5848 - } 5849 - return text; 5850 - }; 5851 - 5852 - const unitickerY = loginY + 44; // Position below login 5853 - let contentTickerY = unitickerY; // Y position of content ticker (declared here for tooltip access) 5854 - 5855 - // Build combined uniticker items from all sources 5856 - // Types: 'chat' (blue), 'clock' (orange), 'kidlisp' ($), 'painting' (#), 'tape' (!) 5857 - const hasChatMessages = $.chat?.messages && $.chat.messages.length > 0; 5858 - const hasClockMessages = clockChatMessages && clockChatMessages.length > 0; 5859 - const hasMediaItems = contentItems.length > 0; 5860 - 5861 - // Build unified items array - collect all items first, then interleave for frecency-style mixing 5862 - const chatItems = []; 5863 - const clockItems = []; 5864 - const mediaItems = []; 5865 - const commitItems = []; 5866 - const statsItems = []; 5867 - const moodItems = []; 5868 - 5869 - // Collect chat messages (blue) - code: 'chat' 5870 - if (hasChatMessages) { 5871 - const numMessages = Math.min(6, $.chat.messages.length); 5872 - const recentMessages = $.chat.messages.slice(-numMessages); 5873 - recentMessages.forEach(msg => { 5874 - const sanitizedText = msg.text.replace(/[\r\n]+/g, ' '); 5875 - const displayText = msg.from + ": " + sanitizedText.slice(0, 50) + (sanitizedText.length > 50 ? "..." : ""); 5876 - chatItems.push({ 5877 - type: 'chat', 5878 - text: displayText, 5879 - code: 'chat', 5880 - color: [100, 200, 255], // Bright blue 5881 - }); 5882 - }); 5883 - } 5884 - 5885 - // Collect clock messages (orange) - code: 'laer-klokken' 5886 - if (hasClockMessages) { 5887 - const numClockMessages = Math.min(4, clockChatMessages.length); 5888 - const recentClockMessages = clockChatMessages.slice(-numClockMessages); 5889 - recentClockMessages.forEach(msg => { 5890 - const sanitizedText = (msg.text || msg.message || '').replace(/[\r\n]+/g, ' '); 5891 - const from = msg.from || msg.handle || msg.author || 'anon'; 5892 - const displayText = from + ": " + sanitizedText.slice(0, 50) + (sanitizedText.length > 50 ? "..." : ""); 5893 - clockItems.push({ 5894 - type: 'clock', 5895 - text: displayText, 5896 - code: 'laer-klokken', 5897 - color: [255, 180, 80], // Bright orange 5898 - }); 5899 - }); 5900 - } 5901 - 5902 - // Collect media items (kidlisp, painting, tape) 5903 - if (hasMediaItems) { 5904 - contentItems.forEach(item => { 5905 - let prefix, color, code; 5906 - if (item.type === 'kidlisp') { 5907 - prefix = '$'; 5908 - color = [150, 255, 150]; // Bright lime green 5909 - code = `$${item.code}`; 5910 - } else if (item.type === 'painting') { 5911 - prefix = '#'; 5912 - color = [255, 150, 255]; // Bright magenta 5913 - // Determine proper code for paintings 5914 - if (!item.handle || item.handle === 'undefined' || item.handle === 'null') { 5915 - code = `painting#${item.code}`; 5916 - } else { 5917 - code = `#${item.code}`; 5918 - } 5919 - } else { // tape 5920 - prefix = '!'; 5921 - color = [255, 200, 100]; // Bright orange/yellow 5922 - code = `!${item.code}`; 5923 - } 5924 - mediaItems.push({ 5925 - type: item.type, 5926 - text: `${prefix}${item.code}`, 5927 - code: code, 5928 - color: color, 5929 - mediaItem: item, // Keep reference to original item for media preview 5930 - }); 5931 - }); 5932 - } 5933 - 5934 - // Collect recent commits (lime green for hash and author visibility) 5935 - if (recentCommits.length > 0) { 5936 - const numCommits = Math.min(5, recentCommits.length); 5937 - recentCommits.slice(0, numCommits).forEach(commit => { 5938 - // Map known authors to their handles 5939 - let author = commit.author; 5940 - if (author === 'Jeffrey Alan Scudder') author = '@jeffrey'; 5941 - const displayText = `[${commit.hash}] ${commit.message} ~${author}`; 5942 - commitItems.push({ 5943 - type: 'commit', 5944 - text: displayText, 5945 - code: 'commits', 5946 - color: [100, 255, 100], // Lime green for commits 5947 - }); 5948 - }); 5949 - } 5950 - 5951 - // Collect handles count (magenta/pink for visibility) 5952 - if (handles) { 5953 - const handlesText = `${handles.toLocaleString()} handles`; 5954 - statsItems.push({ 5955 - type: 'stats', 5956 - text: handlesText, 5957 - code: 'handles', 5958 - color: [255, 100, 255], // Magenta/pink for handles stat 5959 - }); 5960 - } 5961 - 5962 - // Collect moods (cyan/teal) - code: 'mood' 5963 - if (motdCandidates.length > 0) { 5964 - motdCandidates.forEach(candidate => { 5965 - if (!candidate?.mood) return; 5966 - const moodText = candidate.mood.replace(/[\r\n]+/g, ' ').slice(0, 60); 5967 - const handle = candidate.handle || candidate.handleInfo?.handle || candidate.owner?.handle || candidate.user?.handle || null; 5968 - const from = handle ? (handle.startsWith("@") ? handle : `@${handle}`) : 'anon'; 5969 - moodItems.push({ 5970 - type: 'mood', 5971 - text: `${from}: ${moodText}`, 5972 - code: 'mood', 5973 - color: [100, 255, 220], // Cyan/teal for moods 5974 - }); 5975 - }); 5976 - } 5977 - 5978 - // 🎲 Interleave items for frecency-style distribution 5979 - // Round-robin through all item types to ensure good mixing 5980 - // Currently limited to: chats, laer-klokkens, and moods 5981 - unitickerItems = []; 5982 - const allBuckets = [chatItems, clockItems, moodItems].filter(b => b.length > 0); 5983 - const bucketIndices = allBuckets.map(() => 0); 5984 - const totalItems = allBuckets.reduce((sum, b) => sum + b.length, 0); 5985 - 5986 - // Distribute items by cycling through buckets 5987 - let placed = 0; 5988 - let bucketCursor = 0; 5989 - while (placed < totalItems) { 5990 - // Find next bucket with items remaining 5991 - let attempts = 0; 5992 - while (bucketIndices[bucketCursor] >= allBuckets[bucketCursor].length && attempts < allBuckets.length) { 5993 - bucketCursor = (bucketCursor + 1) % allBuckets.length; 5994 - attempts++; 5995 - } 5996 - if (attempts >= allBuckets.length) break; // All buckets exhausted 5997 - 5998 - // Take item from current bucket 5999 - const bucket = allBuckets[bucketCursor]; 6000 - const idx = bucketIndices[bucketCursor]; 6001 - if (idx < bucket.length) { 6002 - unitickerItems.push(bucket[idx]); 6003 - bucketIndices[bucketCursor]++; 6004 - placed++; 6005 - } 6006 - 6007 - // Move to next bucket for round-robin 6008 - bucketCursor = (bucketCursor + 1) % allBuckets.length; 6009 - } 6010 - 6011 - // Show uniticker if we have any items 6012 - const showUniticker = screen.height >= 180 && unitickerItems.length > 0; 6013 - 6014 - if (showUniticker) { 6015 - // Build full text for ticker (just for width calculation) 6016 - const fullText = unitickerItems.map(item => item.text).join(" · "); 6017 - 6018 - // Create or update uniticker instance 6019 - if (!uniticker) { 6020 - mediaPreviewBox = new MediaPreviewBox(); // Initialize shared preview box 6021 - uniticker = new $.gizmo.Ticker(fullText, { 6022 - speed: 1, // 1px per frame 6023 - separator: " · ", 6024 - }); 6025 - uniticker.paused = false; 6026 - uniticker.offset = 0; 6027 - } else { 6028 - uniticker.setText(fullText); 6029 - } 6030 - 6031 - // Update ticker animation 6032 - if (uniticker && !uniticker.paused) { 6033 - uniticker.update($); 6034 - } 6035 - 6036 - // Create or update invisible button over ticker area 6037 - const boxHeight = tickerHeight + (tickerPadding * 2); 6038 - const boxY = unitickerY - tickerPadding; 6039 - 6040 - if (!unitickerButton) { 6041 - unitickerButton = new $.ui.Button({ 6042 - x: 0, 6043 - y: boxY, 6044 - w: screen.width, 6045 - h: boxHeight, 6046 - }); 6047 - unitickerButton.noEdgeDetection = true; 6048 - unitickerButton.noRolloverActivation = true; 6049 - unitickerButton.stickyScrubbing = true; 6050 - } else { 6051 - unitickerButton.box.x = 0; 6052 - unitickerButton.box.y = boxY; 6053 - unitickerButton.box.w = screen.width; 6054 - unitickerButton.box.h = boxHeight; 6055 - } 6056 - 6057 - // Paint background and borders 6058 - if (!unitickerButton.disabled) { 6059 - // Dark background for high contrast (brighter when hovering) 6060 - const isTickerHover = unitickerButton.over && !unitickerButton.down; 6061 - const bgAlpha = unitickerButton.down ? 100 : (isTickerHover ? 80 : 60); 6062 - ink([20, 20, 30, bgAlpha]).box(0, boxY, screen.width, boxHeight - 1); 6063 - 6064 - // Subtle top and bottom borders (purple/pink tint - brighter on hover) 6065 - const borderAlpha = isTickerHover ? 230 : 180; 6066 - const borderColor = isTickerHover ? [180, 120, 230, borderAlpha] : [150, 100, 200, borderAlpha]; 6067 - ink(borderColor).line(0, boxY, screen.width, boxY); 6068 - ink(borderColor).line(0, boxY + boxHeight - 1, screen.width, boxY + boxHeight - 1); 6069 - 6070 - // Render items with individual colors and hover detection 6071 - const textY = unitickerY; 6072 - const tickerAlpha = unitickerButton.down ? 255 : 220; 6073 - 6074 - // Calculate per-item widths and total cycle width 6075 - const separator = " · "; 6076 - const separatorWidth = $.text.box(separator, undefined, undefined, undefined, undefined, tickerFont).box.width; 6077 - let itemWidths = []; 6078 - let totalCycleWidth = 0; 6079 - unitickerItems.forEach((item, idx) => { 6080 - const textWidth = $.text.box(item.text, undefined, undefined, undefined, undefined, tickerFont).box.width; 6081 - itemWidths.push(textWidth); 6082 - totalCycleWidth += textWidth; 6083 - if (idx < unitickerItems.length - 1) { 6084 - totalCycleWidth += separatorWidth; 6085 - } 6086 - }); 6087 - totalCycleWidth += separatorWidth; // Add trailing separator for seamless loop 6088 - 6089 - // Use modulo offset for seamless looping 6090 - const rawOffset = uniticker.getOffset(); 6091 - const loopedOffset = totalCycleWidth > 0 ? (rawOffset % totalCycleWidth) : 0; 6092 - 6093 - // Track hovered item and visible items for auto-selection 6094 - let hoveredItem = null; 6095 - let hoveredItemX = 0; 6096 - let hoveredItemWidth = 0; 6097 - let visibleItemsForAutoSelect = []; // Track all visible items with positions for idle auto-select 6098 - 6099 - // Track idle state - detect pen movement 6100 - const penX = $.pen?.x ?? -1; 6101 - const penY = $.pen?.y ?? -1; 6102 - if (penX !== unitickerLastPenX || penY !== unitickerLastPenY) { 6103 - unitickerIdleFrames = 0; 6104 - unitickerLastPenX = penX; 6105 - unitickerLastPenY = penY; 6106 - // Clear auto-selection when user moves mouse 6107 - if (hoveredItem) { 6108 - unitickerAutoSelectedItem = null; 6109 - } 6110 - } else { 6111 - unitickerIdleFrames++; 6112 - } 6113 - 6114 - // Calculate starting X position with looped offset 6115 - const startMargin = 6; 6116 - let baseX = startMargin - loopedOffset; 6117 - 6118 - // Render enough cycles to fill screen plus buffer 6119 - const numCycles = Math.ceil((screen.width + totalCycleWidth) / totalCycleWidth) + 1; 6120 - 6121 - for (let cycle = 0; cycle < numCycles; cycle++) { 6122 - let currentX = baseX + (cycle * totalCycleWidth); 6123 - 6124 - unitickerItems.forEach((item, idx) => { 6125 - const text = item.text; 6126 - const color = item.color; 6127 - const textWidth = itemWidths[idx]; 6128 - 6129 - // Check if mouse is hovering over this item 6130 - const mouseX = $.pen?.x ?? -1; 6131 - const mouseY = $.pen?.y ?? -1; 6132 - const isHovered = mouseX >= currentX && 6133 - mouseX < currentX + textWidth && 6134 - mouseY >= boxY && 6135 - mouseY < boxY + boxHeight; 6136 - 6137 - // Track hovered item (prefer items visible on screen) 6138 - if (isHovered && currentX >= 0 && currentX < screen.width) { 6139 - hoveredItem = item; 6140 - hoveredItemX = currentX; 6141 - hoveredItemWidth = textWidth; 6142 - } 6143 - 6144 - // Track visible items for auto-selection (items entering from right side) 6145 - if (currentX >= 0 && currentX < screen.width) { 6146 - visibleItemsForAutoSelect.push({ 6147 - item, 6148 - x: currentX, 6149 - width: textWidth, 6150 - }); 6151 - } 6152 - 6153 - // Only render if visible on screen (with small buffer) 6154 - const isVisible = (currentX + textWidth) > -10 && currentX < (screen.width + 10); 6155 - if (isVisible) { 6156 - if (isHovered && !unitickerButton.down) { 6157 - // Highlight background on hover 6158 - $.ink([...color, 50]).box(currentX - 1, boxY + 1, textWidth + 2, boxHeight - 3); 6159 - // Brighter text when hovered 6160 - $.ink(color, 255).write(text, { x: currentX, y: textY }, undefined, undefined, false, tickerFont); 6161 - } else { 6162 - $.ink(color, tickerAlpha).write(text, { x: currentX, y: textY }, undefined, undefined, false, tickerFont); 6163 - } 6164 - } 6165 - 6166 - // Move to next position 6167 - currentX += textWidth; 6168 - 6169 - // Add separator after each item 6170 - const sepVisible = (currentX + separatorWidth) > -10 && currentX < (screen.width + 10); 6171 - if (sepVisible) { 6172 - const blinkAlpha = 120 + Math.sin(motdFrame * 0.2) * 60; 6173 - ink([180, 180, 200], blinkAlpha).write(separator, { x: currentX, y: textY }, undefined, undefined, false, tickerFont); 6174 - } 6175 - currentX += separatorWidth; 6176 - }); 6177 - } 6178 - 6179 - // Store hovered item for click handler and tooltip 6180 - unitickerHoveredItem = hoveredItem; 6181 - unitickerButton.hoveredItem = hoveredItem; 6182 - 6183 - // 🎯 Auto-select on idle: pick a "future" item (from right side) when idle 6184 - let displayItem = hoveredItem; 6185 - let displayItemX = hoveredItemX; 6186 - let displayItemWidth = hoveredItemWidth; 6187 - 6188 - if (!hoveredItem && unitickerIdleFrames >= UNITICKER_IDLE_THRESHOLD && visibleItemsForAutoSelect.length > 0) { 6189 - // Sort by X position to find rightmost items ("future" items coming from the right) 6190 - visibleItemsForAutoSelect.sort((a, b) => a.x - b.x); 6191 - 6192 - // Check if current auto-selected item is still visible and hasn't scrolled off left 6193 - if (unitickerAutoSelectedItem) { 6194 - const stillVisible = visibleItemsForAutoSelect.find( 6195 - v => v.item === unitickerAutoSelectedItem && v.x >= -v.width * 0.3 6196 - ); 6197 - if (stillVisible) { 6198 - // Keep tracking the auto-selected item (tooltip scrolls with it) 6199 - displayItem = stillVisible.item; 6200 - displayItemX = stillVisible.x; 6201 - displayItemWidth = stillVisible.width; 6202 - unitickerAutoSelectedX = stillVisible.x; 6203 - unitickerAutoSelectedWidth = stillVisible.width; 6204 - } else { 6205 - // Item scrolled off to the left - select next "future" item 6206 - unitickerAutoSelectedItem = null; 6207 - } 6208 - } 6209 - 6210 - // If no auto-selected item, pick one from center-right area (not too eager) 6211 - if (!unitickerAutoSelectedItem && visibleItemsForAutoSelect.length > 0) { 6212 - // Pick item from center-right of screen (40%-70% range) - not too eager 6213 - const minThreshold = screen.width * 0.4; // Don't pick items too far left 6214 - const maxThreshold = screen.width * 0.7; // Don't pick items too far right (let them settle in) 6215 - const centeredItems = visibleItemsForAutoSelect.filter(v => v.x >= minThreshold && v.x <= maxThreshold); 6216 - 6217 - let targetItem; 6218 - if (centeredItems.length > 0) { 6219 - // Pick the rightmost centered item (newest arrival that's settled) 6220 - targetItem = centeredItems[centeredItems.length - 1]; 6221 - } else { 6222 - // Fallback: pick item closest to center-right 6223 - const idealX = screen.width * 0.55; 6224 - targetItem = visibleItemsForAutoSelect.reduce((best, v) => { 6225 - const bestDist = Math.abs(best.x - idealX); 6226 - const vDist = Math.abs(v.x - idealX); 6227 - return vDist < bestDist ? v : best; 6228 - }); 6229 - } 6230 - 6231 - unitickerAutoSelectedItem = targetItem.item; 6232 - unitickerAutoSelectedX = targetItem.x; 6233 - unitickerAutoSelectedWidth = targetItem.width; 6234 - displayItem = targetItem.item; 6235 - displayItemX = targetItem.x; 6236 - displayItemWidth = targetItem.width; 6237 - } 6238 - } else if (hoveredItem) { 6239 - // User is hovering - clear auto-selection 6240 - unitickerAutoSelectedItem = null; 6241 - } 6242 - 6243 - // 🎯 Draw "Enter [code]" tooltip below hovered or auto-selected item (scrolls with it) 6244 - if (displayItem && !unitickerButton.down) { 6245 - // Generate contextual tooltip text based on item type and docs 6246 - let tooltipPrefix, tooltipCode, tooltipSuffix; 6247 - const doc = tooltipDocs?.[displayItem.code]; 6248 - if (doc?.desc) { 6249 - // Use doc description with action prefix 6250 - tooltipPrefix = "Enter '"; 6251 - tooltipCode = displayItem.code; 6252 - tooltipSuffix = `' to ${doc.desc.toLowerCase().replace(/\.$/, '')}`; 6253 - } else if (displayItem.type === 'kidlisp') { 6254 - tooltipPrefix = "Enter '"; 6255 - tooltipCode = displayItem.code; 6256 - tooltipSuffix = "' to run"; 6257 - } else if (displayItem.type === 'painting') { 6258 - tooltipPrefix = "Enter '"; 6259 - tooltipCode = displayItem.code; 6260 - tooltipSuffix = "' to view"; 6261 - } else if (displayItem.type === 'tape') { 6262 - tooltipPrefix = "Enter '"; 6263 - tooltipCode = displayItem.code; 6264 - tooltipSuffix = "' to listen"; 6265 - } else if (displayItem.type === 'commit') { 6266 - tooltipPrefix = "Enter '"; 6267 - tooltipCode = displayItem.code; 6268 - tooltipSuffix = "' to browse"; 6269 - } else if (displayItem.type === 'stats') { 6270 - tooltipPrefix = "Enter '"; 6271 - tooltipCode = displayItem.code; 6272 - tooltipSuffix = "' to browse"; 6273 - } else if (displayItem.type === 'mood') { 6274 - tooltipPrefix = "Enter '"; 6275 - tooltipCode = displayItem.code; 6276 - tooltipSuffix = "' to set yours"; 6277 - } else { 6278 - tooltipPrefix = "Enter '"; 6279 - tooltipCode = displayItem.code; 6280 - tooltipSuffix = "'"; 6281 - } 6282 - const tooltipText = tooltipPrefix + tooltipCode + tooltipSuffix; 6283 - const tooltipWidth = $.text.box(tooltipText, undefined, undefined, undefined, undefined, tickerFont).box.width; 6284 - const tooltipHeight = 10; 6285 - const tooltipPadding = 3; 6286 - 6287 - // Position tooltip below the item, centered 6288 - let tooltipX = displayItemX + (displayItemWidth / 2) - (tooltipWidth / 2) - tooltipPadding; 6289 - const tooltipY = boxY + boxHeight + 10; // Below the ticker with more room for arrow 6290 - 6291 - // Clamp to screen bounds 6292 - tooltipX = Math.max(4, Math.min(tooltipX, screen.width - tooltipWidth - tooltipPadding * 2 - 4)); 6293 - 6294 - // Draw tooltip background (slightly dimmer for auto-selected to indicate passive state) 6295 - const alphaMultiplier = hoveredItem ? 1 : 0.7; 6296 - const tooltipBgColor = [...displayItem.color, Math.round(180 * alphaMultiplier)]; 6297 - ink([10, 10, 20, Math.round(220 * alphaMultiplier)]).box(tooltipX, tooltipY, tooltipWidth + tooltipPadding * 2, tooltipHeight + tooltipPadding * 2); 6298 - ink(tooltipBgColor).box(tooltipX, tooltipY, tooltipWidth + tooltipPadding * 2, tooltipHeight + tooltipPadding * 2, "inline"); 6299 - 6300 - // Draw tooltip text with syntax highlighting for the quoted code 6301 - const textY = tooltipY + tooltipPadding + 1; 6302 - let textX = tooltipX + tooltipPadding; 6303 - const baseAlpha = Math.round(255 * alphaMultiplier); 6304 - const dimAlpha = Math.round(180 * alphaMultiplier); 6305 - 6306 - // Draw prefix in dimmer white 6307 - ink([255, 255, 255, dimAlpha]).write(tooltipPrefix, { x: textX, y: textY }, undefined, undefined, false, tickerFont); 6308 - textX += $.text.box(tooltipPrefix, undefined, undefined, undefined, undefined, tickerFont).box.width; 6309 - 6310 - // Draw code in bright highlight color (use item's color for consistency) 6311 - ink([...displayItem.color, baseAlpha]).write(tooltipCode, { x: textX, y: textY }, undefined, undefined, false, tickerFont); 6312 - textX += $.text.box(tooltipCode, undefined, undefined, undefined, undefined, tickerFont).box.width; 6313 - 6314 - // Draw suffix in dimmer white 6315 - ink([255, 255, 255, dimAlpha]).write(tooltipSuffix, { x: textX, y: textY }, undefined, undefined, false, tickerFont); 6316 - 6317 - // Draw small arrow pointing up to the item 6318 - const arrowX = displayItemX + (displayItemWidth / 2); 6319 - const arrowY = tooltipY; 6320 - const arrowAlpha = Math.round(200 * alphaMultiplier); 6321 - ink([...displayItem.color, arrowAlpha]).line(arrowX, arrowY, arrowX - 3, arrowY - 3); 6322 - ink([...displayItem.color, arrowAlpha]).line(arrowX, arrowY, arrowX + 3, arrowY - 3); 6323 - 6324 - // Draw subtle highlight on auto-selected item (when not hovering) 6325 - if (!hoveredItem && unitickerAutoSelectedItem) { 6326 - $.ink([...displayItem.color, 30]).box(displayItemX - 1, boxY + 1, displayItemWidth + 2, boxHeight - 3); 6327 - } 6328 - } 6329 - } 6330 - } else { 6331 - uniticker = null; 6332 - unitickerButton = null; 6333 - unitickerHoveredItem = null; 6334 - unitickerAutoSelectedItem = null; // Clear auto-selection when ticker is hidden 6335 - unitickerIdleFrames = 0; 6336 - currentTooltipItem = null; // Clear when ticker is hidden 6337 - tooltipTimer = 0; 6338 - if (activeTapePreview) { 6339 - releaseActiveTapePreview("ticker-hidden"); 6340 - } 6341 - } 6342 - 6343 - $.layer(1); // Reset layer back to 1 for tooltips 6344 - 6345 - // 🎨 CONTENT TOOLTIP (ambient floating preview) 6346 - // Automatically cycles through content items (KidLisp + Paintings), showing previews 6347 - // Now uses uniticker instead of the old contentTicker 6348 - if (!DISABLE_CONTENT_PREVIEWS && showUniticker && contentItems.length > 0) { 6349 - // Filter to only media items (not chat/clock messages) for preview 6350 - const visibleItems = []; 6351 - 6352 - if (uniticker) { 6353 - const offset = uniticker.getOffset(); 6354 - let currentX = 6 - offset; 6355 - 6356 - // Only show previews for media items (kidlisp, painting, tape) 6357 - const mediaItems = unitickerItems.filter(item => 6358 - item.type === 'kidlisp' || item.type === 'painting' || item.type === 'tape' 6359 - ); 6360 - 6361 - mediaItems.forEach(item => { 6362 - const text = item.text; 6363 - const textWidth = $.text.box(text, undefined, undefined, undefined, undefined, tickerFont).box.width; 6364 - 6365 - // Check if this item is visible on screen (prefer right side) 6366 - const rightBiasStart = (screen.width) * 0.3; 6367 - if (currentX >= rightBiasStart && currentX < screen.width) { 6368 - // Use original media item object if available 6369 - const mediaItem = item.mediaItem || item; 6370 - mediaItem.screenX = currentX; 6371 - visibleItems.push(mediaItem); 6372 - } 6373 - 6374 - currentX += textWidth + $.text.box(" · ", undefined, undefined, undefined, undefined, tickerFont).box.width; 6375 - }); 6376 - } 6377 - 6378 - if (visibleItems.length > 0) { 6379 - if (activeTapePreview && currentTooltipItem && activeTapePreview !== currentTooltipItem) { 6380 - releaseActiveTapePreview("tooltip-switch"); 6381 - } 6382 - 6383 - // Tape-specific durations (tapes are heavy - need more time to load and display) 6384 - const baseDuration = 180; // Show kidlisp/paintings for 3 seconds (at 60fps) 6385 - const tapeDuration = 480; // Show tapes for 8 seconds (at 60fps) 6386 - const displayDuration = currentTooltipItem?.type === 'tape' ? tapeDuration : baseDuration; 6387 - const fadeDuration = 20; // Fade in/out over ~0.33 seconds 6388 - 6389 - // Initialize ONLY if we don't have a tooltip yet 6390 - if (!currentTooltipItem) { 6391 - // Find first item that hasn't failed or try first one 6392 - let startItem = visibleItems.find(i => !i.fetchFailed) || visibleItems[0]; 6393 - currentTooltipItem = startItem; 6394 - tooltipItemIndex = visibleItems.indexOf(startItem); 6395 - tooltipTimer = 0; 6396 - 6397 - // Fetch source/image/audio for current item 6398 - if (currentTooltipItem.type === 'kidlisp' && !currentTooltipItem.source && !currentTooltipItem.fetchAttempted) { 6399 - fetchKidlispSource(currentTooltipItem, $); 6400 - } else if (currentTooltipItem.type === 'painting' && !currentTooltipItem.image && !currentTooltipItem.fetchAttempted) { 6401 - fetchPaintingImage(currentTooltipItem, $); 6402 - } else if (currentTooltipItem.type === 'tape' && !currentTooltipItem.audioUrl && !currentTooltipItem.fetchAttempted) { 6403 - enqueueTapePreview(currentTooltipItem, $); 6404 - } 6405 - 6406 - console.log(`🎨 Tooltip initialized with ${currentTooltipItem.type} item: ${currentTooltipItem.type === 'kidlisp' ? '$' : currentTooltipItem.type === 'painting' ? '#' : '!'}${currentTooltipItem.code}`); 6407 - } 6408 - 6409 - // Pre-fetch next items (always do this, even while waiting for current) 6410 - for (let i = 1; i <= 2; i++) { 6411 - const nextIndex = (tooltipItemIndex + i) % visibleItems.length; 6412 - const nextItem = visibleItems[nextIndex]; 6413 - if (nextItem) { 6414 - if (nextItem.type === 'kidlisp' && !nextItem.source && !nextItem.fetchAttempted && !nextItem.fetchFailed) { 6415 - fetchKidlispSource(nextItem, $); 6416 - } else if (nextItem.type === 'painting' && !nextItem.image && !nextItem.fetchAttempted && !nextItem.fetchFailed) { 6417 - fetchPaintingImage(nextItem, $); 6418 - } else if (nextItem.type === 'tape' && !nextItem.audioUrl && !nextItem.fetchAttempted && !nextItem.fetchFailed) { 6419 - enqueueTapePreview(nextItem, $); 6420 - } 6421 - } 6422 - } 6423 - 6424 - // Only increment timer if current item has source/image or is a loading/loaded tape 6425 - if (currentTooltipItem.source || currentTooltipItem.image || currentTooltipItem.isLoading || currentTooltipItem.framesLoaded) { 6426 - tooltipTimer++; 6427 - } 6428 - 6429 - // Switch to next item after display duration OR if current item failed 6430 - if (tooltipTimer > displayDuration || currentTooltipItem.fetchFailed) { 6431 - // Find next item with source already loaded (don't switch until ready) 6432 - let attempts = 0; 6433 - let foundNext = false; 6434 - const startIndex = tooltipItemIndex; 6435 - 6436 - while (attempts < visibleItems.length && !foundNext) { 6437 - const checkIndex = (startIndex + attempts + 1) % visibleItems.length; 6438 - const nextItem = visibleItems[checkIndex]; 6439 - 6440 - // Only switch to items with source/image/audio or loading tapes 6441 - const hasContent = (nextItem.type === 'kidlisp' && nextItem.source) || 6442 - (nextItem.type === 'painting' && nextItem.image) || 6443 - (nextItem.type === 'tape' && (nextItem.isLoading || nextItem.framesLoaded)); 6444 - if (hasContent && !nextItem.fetchFailed) { 6445 - tooltipTimer = 0; 6446 - tooltipItemIndex = checkIndex; 6447 - currentTooltipItem = nextItem; 6448 - foundNext = true; 6449 - const prefix = nextItem.type === 'kidlisp' ? '$' : nextItem.type === 'painting' ? '#' : '!'; 6450 - console.log(`🎨 Tooltip cycling to ${nextItem.type} item ${tooltipItemIndex + 1}/${visibleItems.length}: ${prefix}${currentTooltipItem.code}`); 6451 - } else if (!nextItem.fetchAttempted && !nextItem.fetchFailed) { 6452 - // Try to fetch if not attempted yet 6453 - if (nextItem.type === 'kidlisp') { 6454 - fetchKidlispSource(nextItem, $); 6455 - } else if (nextItem.type === 'painting') { 6456 - fetchPaintingImage(nextItem, $); 6457 - } else if (nextItem.type === 'tape') { 6458 - enqueueTapePreview(nextItem, $); 6459 - } 6460 - } 6461 - 6462 - attempts++; 6463 - } 6464 - 6465 - // If no loaded item found, wait at full display (don't fade out) 6466 - if (!foundNext) { 6467 - tooltipTimer = displayDuration; // Hold at full opacity 6468 - console.log(`🎨 Waiting for next item to load...`); 6469 - } 6470 - } else if (!currentTooltipItem.source && !currentTooltipItem.image && !currentTooltipItem.isLoading && !currentTooltipItem.framesLoaded) { 6471 - // Waiting for source/image/tape to load - check if it's taking too long 6472 - if (currentTooltipItem.fetchAttempted && !currentTooltipItem.fetchFailed) { 6473 - // Tapes need longer timeout (heavy ZIPs with many frames) 6474 - const baseWaitTime = 120; // 2 seconds at 60fps for kidlisp/paintings 6475 - const tapeWaitTime = 600; // 10 seconds at 60fps for tapes (heavy ZIPs) 6476 - const waitTime = currentTooltipItem.type === 'tape' ? tapeWaitTime : baseWaitTime; 6477 - if (tooltipTimer > waitTime) { 6478 - const prefix = currentTooltipItem.type === 'kidlisp' ? '$' : currentTooltipItem.type === 'painting' ? '#' : '!'; 6479 - console.log(`🎨 Timeout waiting for ${prefix}${currentTooltipItem.code}, marking as failed`); 6480 - currentTooltipItem.fetchFailed = true; 6481 - // Switch logic will run on next frame 6482 - } else { 6483 - tooltipTimer++; // Keep counting while waiting 6484 - } 6485 - } 6486 - // If not yet attempted, don't increment timer - just wait for fetch to start 6487 - } 6488 - 6489 - // Calculate fade in/out ONLY when we have source or image or audioUrl or loading tape 6490 - if (currentTooltipItem.source || currentTooltipItem.image || currentTooltipItem.isLoading || currentTooltipItem.framesLoaded) { 6491 - if (tooltipTimer < fadeDuration) { 6492 - // Fade in 6493 - tooltipFadeIn = tooltipTimer / fadeDuration; 6494 - } else if (tooltipTimer > displayDuration - fadeDuration) { 6495 - // Fade out (only if we have a next item ready) 6496 - tooltipFadeIn = (displayDuration - tooltipTimer) / fadeDuration; 6497 - } else { 6498 - // Full opacity 6499 - tooltipFadeIn = 1; 6500 - } 6501 - } else { 6502 - tooltipFadeIn = 0; // Don't show until loaded 6503 - } 6504 - 6505 - // Render media preview as centered, faded background element 6506 - if (currentTooltipItem && (currentTooltipItem.source || currentTooltipItem.image || currentTooltipItem.isLoading || currentTooltipItem.framesLoaded) && tooltipFadeIn > 0) { 6507 - $.layer(0); // Render behind everything 6508 - 6509 - // Calculate preview dimensions based on type 6510 - let tooltipWidth, tooltipHeight; 6511 - 6512 - // Pre-calculate metadata text to determine width 6513 - let timestampText = ''; 6514 - if (currentTooltipItem.timestamp) { 6515 - const now = new Date(); 6516 - const past = new Date(currentTooltipItem.timestamp); 6517 - const seconds = Math.floor((now - past) / 1000); 6518 - 6519 - const units = [ 6520 - { name: "year", seconds: 31536000 }, 6521 - { name: "month", seconds: 2592000 }, 6522 - { name: "week", seconds: 604800 }, 6523 - { name: "day", seconds: 86400 }, 6524 - { name: "hour", seconds: 3600 }, 6525 - { name: "minute", seconds: 60 }, 6526 - { name: "second", seconds: 1 }, 6527 - ]; 6528 - 6529 - for (const unit of units) { 6530 - const count = Math.floor(seconds / unit.seconds); 6531 - if (count >= 1) { 6532 - timestampText = `${count} ${unit.name}${count > 1 ? "s" : ""} ago`; 6533 - break; 6534 - } 6535 - } 6536 - if (!timestampText) timestampText = "just now"; 6537 - } 6538 - 6539 - const authorHandle = 6540 - currentTooltipItem.author || 6541 - currentTooltipItem.handle || 6542 - currentTooltipItem.owner?.handle || 6543 - null; 6544 - const authorText = authorHandle 6545 - ? authorHandle.startsWith("@") 6546 - ? authorHandle 6547 - : `@${authorHandle}` 6548 - : null; 6549 - 6550 - // Add tape code to metadata for tape items 6551 - const tapeCodeText = currentTooltipItem.type === 'tape' ? `!${currentTooltipItem.code}` : null; 6552 - 6553 - // Build metadata with code for tapes: "!code · timestamp · @author" 6554 - let metadataText; 6555 - if (currentTooltipItem.type === 'tape') { 6556 - const parts = [tapeCodeText, timestampText, authorText].filter(Boolean); 6557 - metadataText = parts.join(' · '); 6558 - } else { 6559 - metadataText = timestampText && authorText 6560 - ? `${timestampText} · ${authorText}` 6561 - : timestampText || authorText || ''; 6562 - } 6563 - 6564 - // Calculate metadata text width using MatrixChunky8 6565 - const metadataWidth = metadataText ? $.text.box(metadataText, undefined, undefined, undefined, undefined, "MatrixChunky8").box.width : 0; 6566 - 6567 - // Standardized tooltip dimensions across all types 6568 - const padding = 6; // Consistent padding for all tooltips 6569 - const metadataHeight = 20; // Consistent metadata section height 6570 - const metadataGap = 6; // Consistent gap before metadata 6571 - 6572 - // Calculate available space below ticker for tooltip 6573 - const tickerBottomY = contentTickerY + tickerHeight + (tickerPadding * 2); 6574 - const tooltipTopMargin = 32; // Space between ticker and tooltip 6575 - const tooltipBottomMargin = 4; // Margin from screen bottom 6576 - const availableHeight = screen.height - tickerBottomY - tooltipTopMargin - tooltipBottomMargin; 6577 - const availableWidth = screen.width - 8; // 4px margin on each side 6578 - 6579 - // Maximum dimensions that respect available space 6580 - const maxTooltipWidth = Math.min(availableWidth, 500); // Cap at 500px or available width 6581 - const maxContentHeight = availableHeight - padding - metadataGap - metadataHeight; // Reserve space for metadata 6582 - 6583 - if (currentTooltipItem.type === 'kidlisp' && currentTooltipItem.source) { 6584 - // KidLisp tooltip: use shared MediaPreviewBox dimensions 6585 - const boxDims = mediaPreviewBox.getBoxDimensions(); 6586 - tooltipWidth = boxDims.width; 6587 - tooltipHeight = boxDims.height; // Just the box, metadata goes outside 6588 - } else if (currentTooltipItem.type === 'painting' && currentTooltipItem.image) { 6589 - // Painting tooltip: use shared MediaPreviewBox dimensions 6590 - const boxDims = mediaPreviewBox.getBoxDimensions(); 6591 - tooltipWidth = boxDims.width; 6592 - tooltipHeight = boxDims.height; // Just the box, metadata goes outside 6593 - } else if (currentTooltipItem.type === 'tape' && currentTooltipItem.audioUrl) { 6594 - // Tape tooltip: show title and audio visualization - fit to available space 6595 - const tapeTitle = currentTooltipItem.title || `Tape !${currentTooltipItem.code}`; 6596 - const tapeTitleWidth = $.text.box(tapeTitle, undefined, undefined, undefined, undefined, "MatrixChunky8").box.width; 6597 - const visualizerHeight = Math.min(120, maxContentHeight - 8); // Fit visualizer, reserve 8px for title 6598 - 6599 - const minWidthForMetadata = metadataWidth + padding * 2; 6600 - const minWidthForTitle = tapeTitleWidth + padding * 2; 6601 - tooltipWidth = Math.max(Math.min(150, maxTooltipWidth), minWidthForMetadata, minWidthForTitle); 6602 - tooltipHeight = 8 + padding + visualizerHeight + padding; // Title + viz only, metadata goes outside 6603 - } else if (currentTooltipItem.type === 'tape' && (currentTooltipItem.isLoading || currentTooltipItem.framesLoaded)) { 6604 - // Tape tooltip: use shared MediaPreviewBox dimensions 6605 - const boxDims = mediaPreviewBox.getBoxDimensions(); 6606 - tooltipWidth = boxDims.width; 6607 - tooltipHeight = boxDims.height; // Just the box, progress/time/metadata go outside 6608 - } else { 6609 - return; // No valid content to show 6610 - } 6611 - 6612 - // Update drift animation (smooth organic movement) - time-based 6613 - const now = performance.now(); 6614 - if (!lastTooltipTime) lastTooltipTime = now; 6615 - const deltaTooltip = (now - lastTooltipTime) / 1000; // Convert to seconds 6616 - lastTooltipTime = now; 6617 - tooltipDriftPhase += deltaTooltip * 1.2; // ~0.02 per frame at 60fps 6618 - const driftSpeed = 0.5; 6619 - tooltipDriftX = Math.sin(tooltipDriftPhase) * 15 * driftSpeed; 6620 - tooltipDriftY = Math.cos(tooltipDriftPhase * 0.7) * 10 * driftSpeed; 6621 - 6622 - // Find the position of the highlighted item in the ticker 6623 - // We need to calculate where the item appears in the scrolling ticker 6624 - const tickerY = contentTickerY; // Use contentTickerY (where ticker is actually rendered) 6625 - const tickerBoxY = contentTickerY - tickerPadding; // Top of ticker box 6626 - const tickerBoxHeight = tickerHeight + (tickerPadding * 2); // Height including padding 6627 - let highlightedItemX = -1; 6628 - let highlightedItemWidth = -1; 6629 - 6630 - // Calculate the offset in the ticker for our current item 6631 - if (contentTicker) { 6632 - const offset = contentTicker.getOffset(); 6633 - let currentX = tickerContentX - offset; 6634 - const prefix = currentTooltipItem.type === 'kidlisp' ? '$' : currentTooltipItem.type === 'painting' ? '#' : '!'; 6635 - const targetText = `${prefix}${currentTooltipItem.code}`; 6636 - 6637 - // Find where this item appears in the ticker 6638 - for (let i = 0; i < contentItems.length; i++) { 6639 - const item = contentItems[i]; 6640 - const itemPrefix = item.type === 'kidlisp' ? '$' : item.type === 'painting' ? '#' : '!'; 6641 - const itemText = `${itemPrefix}${item.code}`; 6642 - const textWidth = $.text.box(itemText).box.width; 6643 - 6644 - if (item === currentTooltipItem) { 6645 - // Check if this position is on screen 6646 - if (currentX >= 0 && currentX < screen.width) { 6647 - highlightedItemX = currentX; 6648 - highlightedItemWidth = textWidth; 6649 - } 6650 - break; 6651 - } 6652 - 6653 - currentX += textWidth + $.text.box(" · ").box.width; 6654 - } 6655 - } 6656 - 6657 - // Position preview centered on screen with subtle fade 6658 - const baseTooltipX = (screen.width - tooltipWidth) / 2; 6659 - const baseTooltipY = (screen.height - tooltipHeight) / 2; 6660 - let tooltipX = baseTooltipX; 6661 - let tooltipY = baseTooltipY; 6662 - 6663 - // Reduce opacity for background effect (30% max) 6664 - const bgFadeMultiplier = 0.3; 6665 - 6666 - // Calculate total height including metadata/progress/time 6667 - let totalTooltipHeight = tooltipHeight + metadataGap + metadataHeight; 6668 - 6669 - // For tapes with frames, add progress bar and time 6670 - if (currentTooltipItem.type === 'tape' && (currentTooltipItem.isLoading || currentTooltipItem.framesLoaded)) { 6671 - const progressBarHeight = 1; 6672 - const timeDisplayHeight = 8; 6673 - totalTooltipHeight = tooltipHeight + 2 + progressBarHeight + 2 + timeDisplayHeight + metadataGap + metadataHeight; 6674 - } 6675 - 6676 - // Clamp tooltip to stay on screen (including metadata below) 6677 - tooltipX = Math.max(4, Math.min(tooltipX, screen.width - tooltipWidth - 4)); 6678 - tooltipY = Math.max(4, Math.min(tooltipY, screen.height - totalTooltipHeight - 4)); 6679 - 6680 - // Draw subtle background 6681 - const bgAlpha = Math.floor(60 * tooltipFadeIn * bgFadeMultiplier); 6682 - $.ink([10, 20, 15, bgAlpha]).box(tooltipX, tooltipY, tooltipWidth, tooltipHeight); 6683 - 6684 - // Draw border matching media type color 6685 - let borderColor; 6686 - if (currentTooltipItem.type === 'kidlisp') { 6687 - borderColor = [120, 255, 160]; // Green 6688 - } else if (currentTooltipItem.type === 'painting') { 6689 - borderColor = [255, 120, 255]; // Magenta 6690 - } else { // tape 6691 - borderColor = [255, 200, 120]; // Orange 6692 - } 6693 - const borderAlpha = Math.floor(50 * tooltipFadeIn * bgFadeMultiplier); 6694 - $.ink([...borderColor, borderAlpha]).box(tooltipX, tooltipY, tooltipWidth, tooltipHeight, "inline"); 6695 - 6696 - // Render content based on type with reduced opacity 6697 - const textAlpha = Math.floor(80 * tooltipFadeIn * bgFadeMultiplier); 6698 - 6699 - if (currentTooltipItem.type === 'kidlisp' && currentTooltipItem.source) { 6700 - // Render KidLisp source with proper syntax highlighting 6701 - renderKidlispSource( 6702 - $, 6703 - currentTooltipItem.source, 6704 - tooltipX + padding, 6705 - tooltipY + padding, 6706 - tooltipWidth - padding * 2, 6707 - 5, // maxLines 6708 - textAlpha 6709 - ); 6710 - } else if (currentTooltipItem.type === 'painting' && currentTooltipItem.image) { 6711 - // Render painting using shared MediaPreviewBox 6712 - mediaPreviewBox.render($, currentTooltipItem, tooltipX, tooltipY, tooltipFadeIn * bgFadeMultiplier); 6713 - } else if (currentTooltipItem.type === 'tape' && (currentTooltipItem.isLoading || currentTooltipItem.framesLoaded)) { 6714 - // Render tape using shared MediaPreviewBox 6715 - mediaPreviewBox.render($, currentTooltipItem, tooltipX, tooltipY, tooltipFadeIn * bgFadeMultiplier); 6716 - } 6717 - 6718 - $.layer(1); // Reset to main layer 6719 - 6720 - // Skip metadata rendering for background preview 6721 - /* 6722 - // Render metadata (timestamp and @author) OUTSIDE the box, below it 6723 - // For KidLisp: after the box + gap 6724 - // For Painting: after the box + gap 6725 - // For Tape with frames: after the box + progress bar + time + gap 6726 - // For Tape with audio or loading: after the box + gap 6727 - let metadataY; 6728 - if (currentTooltipItem.type === 'kidlisp' && currentTooltipItem.source) { 6729 - metadataY = tooltipY + tooltipHeight + metadataGap; 6730 - } else if (currentTooltipItem.type === 'painting' && currentTooltipItem.image) { 6731 - metadataY = tooltipY + tooltipHeight + metadataGap; 6732 - } else if (currentTooltipItem.type === 'tape' && (currentTooltipItem.isLoading || currentTooltipItem.framesLoaded)) { 6733 - // For tape with frames: metadata goes after progress bar (2px gap) + 1px bar + time (8px) + gap 6734 - const progressBarHeight = 1; 6735 - const timeDisplayHeight = 8; 6736 - metadataY = tooltipY + tooltipHeight + 2 + progressBarHeight + 2 + timeDisplayHeight + metadataGap; 6737 - } else if (currentTooltipItem.type === 'tape' && currentTooltipItem.audioUrl) { 6738 - metadataY = tooltipY + tooltipHeight + metadataGap; 6739 - } 6740 - 6741 - // Render metadata in dimmer color using MatrixChunky8 6742 - if (metadataText) { 6743 - const metadataAlpha = Math.floor(180 * tooltipFadeIn); 6744 - $.ink([100, 180, 120, metadataAlpha]).write( 6745 - metadataText, 6746 - { x: tooltipX + padding, y: metadataY }, 6747 - undefined, 6748 - undefined, 6749 - false, 6750 - "MatrixChunky8" 6751 - ); 6752 - } 6753 - */ 6754 - } 6755 - } 6756 - } 6757 - 6758 5814 // 📦 Commit hash button - shows version status / update availability 6759 5815 // Hide commits button when KidLisp button is active (they share the same screen area) 6760 5816 if (versionInfo && versionInfo.deployed && !(kidlispBtn && !kidlispBtn.btn.disabled)) { ··· 8126 7182 } 8127 7183 } 8128 7184 8129 - // 🎰 UNITICKER button handler (unified ticker combining all content) 8130 - if (unitickerButton && !unitickerButton.disabled) { 8131 - unitickerButton.act(e, { 8132 - down: () => { 8133 - downSound(); 8134 - if (uniticker) { 8135 - uniticker.paused = true; 8136 - unitickerButton.scrubStartX = e.x; 8137 - unitickerButton.scrubInitialOffset = uniticker.getOffset(); 8138 - unitickerButton.hasScrubbed = false; 8139 - } 8140 - needsPaint(); 8141 - }, 8142 - scrub: (btn) => { 8143 - if (uniticker && e.x !== undefined && e.y !== undefined) { 8144 - const scrubDelta = e.x - unitickerButton.scrubStartX; 8145 - let newOffset = unitickerButton.scrubInitialOffset - scrubDelta; 8146 - 8147 - if (newOffset < 0) { 8148 - newOffset = newOffset * 0.3; // Elastic effect 8149 - } 8150 - 8151 - uniticker.setOffset(newOffset); 8152 - unitickerButton.hasScrubbed = Math.abs(scrubDelta) > 5; 8153 - 8154 - synth({ 8155 - type: "sine", 8156 - tone: 1200 + Math.abs(scrubDelta) * 2, 8157 - attack: 0.005, 8158 - decay: 0.9, 8159 - volume: 0.08, 8160 - duration: 0.01, 8161 - }); 8162 - 8163 - needsPaint(); 8164 - } 8165 - }, 8166 - push: () => { 8167 - if (!unitickerButton.hasScrubbed) { 8168 - pushSound(); 8169 - } 8170 - 8171 - if (!unitickerButton.hasScrubbed && unitickerButton.hoveredItem) { 8172 - // Jump to the hovered item's destination 8173 - const item = unitickerButton.hoveredItem; 8174 - const destination = item.code; 8175 - 8176 - // Set prompt input text to show what's loading 8177 - system.prompt.input.text = destination; 8178 - system.prompt.input.snap(); 8179 - 8180 - // Jump to the destination 8181 - jump(destination); 8182 - } else { 8183 - if (uniticker) { 8184 - uniticker.paused = false; 8185 - } 8186 - } 8187 - unitickerButton.hasScrubbed = false; 8188 - }, 8189 - cancel: () => { 8190 - cancelSound(); 8191 - if (uniticker) { 8192 - uniticker.paused = false; 8193 - } 8194 - unitickerButton.hasScrubbed = false; 8195 - }, 8196 - }); 8197 - } 8198 - 8199 - // (DEPRECATED - Now using uniticker) Chat ticker button 7185 + // (DEPRECATED) Chat ticker button 8200 7186 if (chatTickerButton && !chatTickerButton.disabled) { 8201 7187 chatTickerButton.act(e, { 8202 7188 down: () => { ··· 8428 7414 (osBtn?.btn?.disabled === false && osBtn?.btn?.box.contains(e)) || 8429 7415 (blankAdBtn?.btn?.disabled === false && blankAdBtn?.btn?.box.contains(e)) || 8430 7416 (products.getShopBoxButton()?.disabled === false && products.getShopBoxButton()?.box.contains(e)) || 8431 - (unitickerButton?.disabled === false && unitickerButton?.box.contains(e)) || 8432 7417 (chatTickerButton?.disabled === false && chatTickerButton?.box.contains(e)) || 8433 7418 (contentTickerButton?.disabled === false && contentTickerButton?.box.contains(e)) || 8434 7419 isOverMotdHandle ||