PageSourceSearch

https://brianstauffer.com/airo-logo-orientation.js

js brianstauffer.com collected 2026-10-02 12:38:42 UTC 6,988 bytes, 190 lines download raw bytes

1/**
2 * airo-logo-orientation.js — Runtime size correction for logo orientation mismatches.
3 *
4 * Fetches /airo-media.json, finds <img> elements pointing at /airo-assets/images/logo/*,
5 * and bumps their rendered height when the slot's `naturalOrientation` doesn't match
6 * the slot's layout (e.g. user uploads a square logo into the horizontal slot — without
7 * this correction the logo renders tiny because the template's `h-* w-auto` sizing was
8 * picked for a wider horizontal logo).
9 *
10 * Mirrors the pattern in airo-video-slots.js: loaded synchronously in <head>, uses a
11 * MutationObserver to catch React-rendered images, polls the manifest in dev mode.
12 */
13;(function () {
14  var SLOT_PREFIX_IMAGES = '/airo-assets/images/'
15  // Must stay aligned with LOGO_NATURAL_SLOT_PATHS in MediaManifestService.ts.
16  var SLOT_TO_EXPECTED_LAYOUT = {
17    'logo/horizontal': 'horizontal',
18    'logo/primary': 'square',
19    'logo/vertical': 'vertical',
20  }
21  var naturalOrientations = {}
22
23  function extractSlotPath(url) {
24    if (!url) return null
25    var idx = url.indexOf(SLOT_PREFIX_IMAGES)
26    if (idx === -1) return null
27    return url.substring(idx + SLOT_PREFIX_IMAGES.length).split('?')[0]
28  }
29
30  var MISMATCH_SCALE = 1.4
31
32  function correctLogo(img) {
33    if (!img.src) return
34    if (img.getAttribute('data-airo-logo-corrected')) return
35    var slotPath = extractSlotPath(img.src)
36    if (!slotPath) return
37    var expectedLayout = SLOT_TO_EXPECTED_LAYOUT[slotPath]
38    if (!expectedLayout) return
39    var naturalOrientation = naturalOrientations[slotPath]
40    if (!naturalOrientation) return
41
42    // Uploads have no transparent layer the agent can rely on — a brightness-0/invert
43    // Tailwind filter would render the opaque upload as a flat blob. Strip it
44    // regardless of whether the URL carries `?variant=` (the agent doesn't always
45    // append one; the underlying invariant is "slot is upload-backed").
46    img.style.filter = 'none'
47
48    if (naturalOrientation === expectedLayout) {
49      img.setAttribute('data-airo-logo-corrected', naturalOrientation)
50      return
51    }
52
53    function apply(retriesLeft) {
54      if (img.getAttribute('data-airo-logo-corrected')) return
55      var h = img.clientHeight
56      if (!h) {
57        if (retriesLeft > 0) {
58          requestAnimationFrame(function () { apply(retriesLeft - 1) })
59        }
60        return
61      }
62      img.style.height = (h * MISMATCH_SCALE) + 'px'
63      img.style.width = 'auto'
64      img.setAttribute('data-airo-logo-corrected', naturalOrientation)
65    }
66
67    if (img.complete && img.clientHeight) {
68      apply(0)
69    } else {
70      img.addEventListener('load', function () { apply(20) }, { once: true })
71      requestAnimationFrame(function () { apply(20) })
72    }
73  }
74
75  function patchAll() {
76    document.querySelectorAll('img').forEach(correctLogo)
77  }
78
79  // Re-read the manifest, sync naturalOrientations, clear stale corrections,
80  // re-patch. Used on src changes (Vite HMR pushes new versions before our 3s poll).
81  function refreshFromManifest() {
82    fetch('/airo-media.json').then(function (r) {
83      return r.ok ? r.json() : {}
84    }).then(function (m) {
85      for (var k in SLOT_TO_EXPECTED_LAYOUT) {
86        var n = m[k] && m[k].naturalOrientation
87        if (n) naturalOrientations[k] = n
88        else delete naturalOrientations[k]
89      }
90      document.querySelectorAll('img[data-airo-logo-corrected]').forEach(function (img) {
91        img.removeAttribute('data-airo-logo-corrected')
92        img.style.height = ''
93        img.style.width = ''
94        img.style.filter = ''
95      })
96      patchAll()
97    }).catch(function () {})
98  }
99
100  fetch('/airo-media.json')
101    .then(function (res) {
102      if (!res.ok) return {}
103      return res.json()
104    })
105    .then(function (manifest) {
106      for (var key in manifest) {
107        if (manifest[key] && manifest[key].naturalOrientation) {
108          naturalOrientations[key] = manifest[key].naturalOrientation
109        }
110      }
111      patchAll()
112
113      var observer = new MutationObserver(function (mutations) {
114        for (var i = 0; i < mutations.length; i++) {
115          var mutation = mutations[i]
116          if (mutation.type === 'childList') {
117            var added = mutation.addedNodes
118            for (var j = 0; j < added.length; j++) {
119              var node = added[j]
120              if (node instanceof HTMLImageElement) {
121                correctLogo(node)
122              } else if (node instanceof HTMLElement) {
123                node.querySelectorAll('img').forEach(correctLogo)
124              }
125            }
126          } else if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
127            var target = mutation.target
128            if (target instanceof HTMLImageElement && extractSlotPath(target.src)) {
129              target.removeAttribute('data-airo-logo-corrected')
130              target.style.height = ''
131              target.style.width = ''
132              target.style.filter = ''
133              // Manifest may have changed alongside the src (Vite HMR pushes versions
134              // instantly; our 3s poll lags). Refresh before correcting.
135              refreshFromManifest()
136            }
137          }
138        }
139      })
140      observer.observe(document.documentElement, {
141        childList: true,
142        subtree: true,
143        attributes: true,
144        attributeFilter: ['src'],
145      })
146
147      var isDevMode = window.__AIRO_DEV_MODE__ === true || location.hostname === 'localhost' || location.hostname === '127.0.0.1'
148      if (isDevMode) {
149        var pollFailures = 0
150        var pollIntervalId = setInterval(function () {
151          fetch('/airo-media.json').then(function (r) {
152            if (!r.ok) return {}
153            return r.json()
154          }).then(function (m) {
155            pollFailures = 0
156            var changed = false
157            for (var k in m) {
158              var n = m[k] && m[k].naturalOrientation
159              if (n && naturalOrientations[k] !== n) {
160                naturalOrientations[k] = n
161                changed = true
162              } else if (!n && naturalOrientations[k]) {
163                delete naturalOrientations[k]
164                changed = true
165              }
166            }
167            if (changed) {
168              document.querySelectorAll('img[data-airo-logo-corrected]').forEach(function (img) {
169                img.removeAttribute('data-airo-logo-corrected')
170                img.style.height = ''
171                img.style.width = ''
172                img.style.filter = ''
173              })
174              patchAll()
175            }
176          }).catch(function (err) {
177            pollFailures++
178            if (pollFailures === 1) {
179              // eslint-disable-next-line no-console
180              console.warn('[airo-logo-orientation] manifest poll failed:', err.message || err)
181            }
182            if (pollFailures >= 5) {
183              clearInterval(pollIntervalId)
184            }
185          })
186        }, 3000)
187      }
188    })
189    .catch(function () {})
190})()

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.