1/** 2 * @file shared/skeleton.js 3 * @description Skeleton loader for async surfaces. 4 * 5 * Replaces lone "spinner with no shape" loading states with content 6 * placeholders that match the rough geometry of what is about to 7 * arrive. Mirrors the pattern users know from Instagram / Facebook / 8 * LinkedIn / Stripe; the brain doesn't have to re-shape its mental 9 * model when the content arrives, the perceived load time drops. 10 * 11 * Touches no protocol code. No localStorage. No network. Pure DOM. 12 * 13 * API: 14 * Skeleton.show(container, { kind, count, height }) 15 * container: Element or "#id" string. Inner HTML is replaced. 16 * kind : "row" (default) | "card" | "score" | "tile" 17 * count : number of placeholder rows / cards (default 3) 18 * 19 * Skeleton.hide(container) 20 * Fades out the placeholders and clears them. The caller is 21 * expected to render real content after this returns. 22 * 23 * Skeleton.replace(container, htmlOrElement) 24 * Convenience: hide() then insertAdjacentHTML / appendChild the 25 * new content with a fade-in. The caller passes either an HTML 26 * string or a DocumentFragment. 27 * 28 * Motion: a 1.5s shimmer animation. Honours prefers-reduced-motion; 29 * the placeholder still shows but doesn't animate. 30 * 31 * © 2026 The AI Lab Intelligence Unobscured, Inc. 32 */ 33(function () { 34 "use strict"; 35 36 var STYLE_ID = "tip-skeleton-styles"; 37 38 function injectStyles() { 39 if (document.getElementById(STYLE_ID)) return; 40 var s = document.createElement("style"); 41 s.id = STYLE_ID; 42 s.textContent = [ 43 ".tip-skeleton{", 44 " background:linear-gradient(90deg,", 45 " rgba(8,15,32,.05) 25%,", 46 " rgba(8,15,32,.10) 50%,", 47 " rgba(8,15,32,.05) 75%);", 48 " background-size:200% 100%;", 49 " animation:tip-skel-shimmer 1.5s ease-in-out infinite;", 50 " border-radius:8px;", 51 "}", 52 "@keyframes tip-skel-shimmer{", 53 " 0%{background-position:200% 0}", 54 " 100%{background-position:-200% 0}", 55 "}", 56 ".tip-skeleton-wrap{", 57 " display:flex;flex-direction:column;gap:12px;", 58 " transition:opacity .2s ease;", 59 "}", 60 ".tip-skeleton-wrap.tip-skel-hide{opacity:0;}", 61 ".tip-skel-row{", 62 " display:flex;align-items:center;gap:12px;", 63 " padding:14px 0;border-bottom:1px solid rgba(8,15,32,.06);", 64 "}", 65 ".tip-skel-row:last-child{border-bottom:none;}", 66 ".tip-skel-avatar{", 67 " width:40px;height:40px;border-radius:10px;flex-shrink:0;", 68 "}", 69 ".tip-skel-lines{flex:1;min-width:0;display:flex;flex-direction:column;gap:8px;}", 70 ".tip-skel-line{height:12px;}", 71 ".tip-skel-line.full{width:90%;}", 72 ".tip-skel-line.short{width:55%;}", 73 ".tip-skel-card{", 74 " padding:18px;border-radius:16px;", 75 " background:#fff;border:1px solid rgba(8,15,32,.06);", 76 " box-shadow:0 1px 3px rgba(8,15,32,.04);", 77 " display:flex;flex-direction:column;gap:10px;", 78 "}", 79 ".tip-skel-card .tip-skel-line.h{height:18px;width:60%;}", 80 ".tip-skel-card .tip-skel-line.p{height:12px;width:88%;}", 81 ".tip-skel-card .tip-skel-line.p.s{width:72%;}", 82 ".tip-skel-tile{", 83 " display:flex;flex-direction:column;align-items:center;gap:8px;", 84 " padding:14px 6px;", 85 "}", 86 ".tip-skel-tile .tip-skel-icon{", 87 " width:48px;height:48px;border-radius:14px;", 88 "}", 89 ".tip-skel-tile .tip-skel-line{height:11px;width:74%;}", 90 ".tip-skel-score{", 91 " display:flex;align-items:center;justify-content:space-between;", 92 " padding:18px;border-radius:16px;", 93 " background:#fff;border:1px solid rgba(8,15,32,.06);", 94 " box-shadow:0 1px 3px rgba(8,15,32,.04);", 95 "}",
96 ".tip-skel-score .tip-skel-num{width:64px;height:32px;}", 97 ".tip-skel-score .tip-skel-label{width:130px;height:14px;}", 98 "@media(prefers-reduced-motion:reduce){", 99 " .tip-skeleton{animation:none;opacity:.7;}", 100 "}", 101 ].join(""); 102 document.head.appendChild(s); 103 } 104 105 function _resolve(target) { 106 if (!target) return null; 107 if (typeof target === "string") return document.querySelector(target); 108 return target; 109 } 110 111 function _rowHTML() { 112 return [ 113 '<div class="tip-skel-row">', 114 ' <div class="tip-skeleton tip-skel-avatar"></div>', 115 ' <div class="tip-skel-lines">', 116 ' <div class="tip-skeleton tip-skel-line full"></div>', 117 ' <div class="tip-skeleton tip-skel-line short"></div>', 118 ' </div>', 119 '</div>', 120 ].join(""); 121 } 122 function _cardHTML() { 123 return [ 124 '<div class="tip-skel-card">', 125 ' <div class="tip-skeleton tip-skel-line h"></div>', 126 ' <div class="tip-skeleton tip-skel-line p"></div>', 127 ' <div class="tip-skeleton tip-skel-line p s"></div>', 128 '</div>', 129 ].join(""); 130 } 131 function _tileHTML() { 132 return [ 133 '<div class="tip-skel-tile">', 134 ' <div class="tip-skeleton tip-skel-icon"></div>', 135 ' <div class="tip-skeleton tip-skel-line"></div>', 136 '</div>', 137 ].join(""); 138 } 139 function _scoreHTML() { 140 return [ 141 '<div class="tip-skel-score">', 142 ' <div>', 143 ' <div class="tip-skeleton tip-skel-label" style="margin-bottom:10px;"></div>', 144 ' <div class="tip-skeleton tip-skel-num"></div>', 145 ' </div>', 146 ' <div class="tip-skeleton tip-skel-avatar" style="width:48px;height:48px;border-radius:50%;"></div>', 147 '</div>', 148 ].join(""); 149 } 150 151 function show(target, opts) { 152 injectStyles(); 153 var el = _resolve(target); 154 if (!el) return null; 155 opts = opts || {}; 156 var kind = opts.kind || "row"; 157 var count = (typeof opts.count === "number") ? opts.count : 3; 158 var make = 159 (kind === "card") ? _cardHTML : 160 (kind === "tile") ? _tileHTML : 161 (kind === "score") ? _scoreHTML : 162 _rowHTML; 163 var html = '<div class="tip-skeleton-wrap" data-tip-skel="1" role="status" aria-live="polite" aria-busy="true" aria-label="Loading">'; 164 for (var i = 0; i < count; i++) html += make(); 165 html += '</div>'; 166 el.innerHTML = html; 167 el.style.display = ""; 168 return el.querySelector(".tip-skeleton-wrap"); 169 } 170 171 function hide(target) { 172 var el = _resolve(target); 173 if (!el) return; 174 var wrap = el.querySelector('[data-tip-skel="1"]'); 175 if (!wrap) return; 176 wrap.classList.add("tip-skel-hide"); 177 setTimeout(function () { 178 if (wrap && wrap.parentNode) wrap.parentNode.removeChild(wrap); 179 }, 220); 180 } 181 182 function replace(target, htmlOrEl) { 183 var el = _resolve(target); 184 if (!el) return; 185 hide(el); 186 setTimeout(function () { 187 if (typeof htmlOrEl === "string") { 188 el.innerHTML = htmlOrEl; 189 } else if (htmlOrEl && htmlOrEl.nodeType) { 190 el.innerHTML = ""; 191 el.appendChild(htmlOrEl); 192 } 193 }, 230); 194 } 195 196 var api = Object.freeze({ 197 show: show, 198 hide: hide, 199 replace: replace, 200 }); 201 if (typeof window !== "undefined") window.Skeleton = api; 202 if (typeof globalThis !== "undefined") globalThis.Skeleton = api; 203})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.