PageSourceSearch

https://vp.theailab.org/shared/skeleton.js

js theailab.org collected 2026-09-24 12:25:43 UTC 7,109 bytes, 203 lines download raw bytes

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.