|
| 1 | +<!doctype html> |
| 2 | +<html lang=en-gb> |
| 3 | + <meta charset=utf-8> |
| 4 | + <meta name=viewport content="width=device-width, minimum-scale=1.0"> |
| 5 | + |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | + |
| 10 | + <!-- Optional config: observe live changes in this demo --> |
| 11 | + <script>window.obs = { config: { observeChanges: true } };</script> |
| 12 | + |
| 13 | + <script> |
| 14 | + /*! Obs.js | (c) Harry Roberts, csswizardry.com | MIT */ |
| 15 | + ;(()=>{const e=document.currentScript;if((!e||e.src||e.type&&"module"===e.type.toLowerCase())&&!1===/^(localhost|127\.0\.0\.1|::1)$/.test(location.hostname))return void console.warn("[Obs.js] Skipping: must be an inline, classic <script> in <head>.",e?e.src?"src="+e.src:"type="+e.type:"type=module");const t=document.documentElement,{connection:n}=navigator;window.obs=window.obs||{};const i=!0===(window.obs&&window.obs.config||{}).observeChanges,o=()=>{const e=window.obs||{},n="number"==typeof e.downlinkBucket?e.downlinkBucket:null;e.connectionCapability="low"===e.rttCategory&&null!=n&&n>=8?"strong":"high"===e.rttCategory||null!=n&&n<=5?"weak":"moderate";const i=!0===e.dataSaver||!0===e.batteryLow;e.conservationPreference=i?"conserve":"neutral",e.deliveryMode=i||"strong"!==e.connectionCapability?i||"weak"===e.connectionCapability?"lite":"cautious":"rich",e.canShowRichMedia="rich"===e.deliveryMode,e.shouldAvoidRichMedia="lite"===e.deliveryMode,["strong","moderate","weak"].forEach(e=>{t.classList.remove(`has-connection-capability-${e}`)}),t.classList.add(`has-connection-capability-${e.connectionCapability}`),["conserve","neutral"].forEach(e=>{t.classList.remove(`has-conservation-preference-${e}`)}),t.classList.add(`has-conservation-preference-${e.conservationPreference}`),["rich","cautious","lite"].forEach(e=>{t.classList.remove(`has-delivery-mode-${e}`)}),t.classList.add(`has-delivery-mode-${e.deliveryMode}`)},a=()=>{if(!n)return;const{saveData:e,rtt:i,downlink:a}=n;window.obs.dataSaver=!!e,t.classList.toggle("has-data-saver",!!e);const s=(e=>Number.isFinite(e)?25*Math.ceil(e/25):null)(i);null!=s&&(window.obs.rttBucket=s);const c=(e=>Number.isFinite(e)?e<75?"low":e<=275?"medium":"high":null)(i);c&&(window.obs.rttCategory=c,["low","medium","high"].forEach(e=>t.classList.remove(`has-latency-${e}`)),t.classList.add(`has-latency-${c}`));const r=(l=a,Number.isFinite(l)?Math.ceil(l):null);var l;if(null!=r){window.obs.downlinkBucket=r;const e=r>=8;t.classList.toggle("has-bandwidth-low",r<=5),t.classList.toggle("has-bandwidth-high",e)}"downlinkMax"in n&&(window.obs.downlinkMax=n.downlinkMax),o()};a(),i&&n&&"function"==typeof n.addEventListener&&n.addEventListener("change",a);const s=e=>{if(!e)return;const{level:n,charging:i}=e,a=Number.isFinite(n)?n<=.05:null;window.obs.batteryCritical=a;const s=Number.isFinite(n)?n<=.2:null;window.obs.batteryLow=s,["critical","low"].forEach(e=>t.classList.remove(`has-battery-${e}`)),s&&t.classList.add("has-battery-low"),a&&t.classList.add("has-battery-critical");const c=!!i;window.obs.batteryCharging=c,t.classList.toggle("has-battery-charging",c),o()};"getBattery"in navigator&&navigator.getBattery().then(e=>{s(e),i&&"function"==typeof e.addEventListener&&(e.addEventListener("levelchange",()=>s(e)),e.addEventListener("chargingchange",()=>s(e)))}).catch(()=>{})})(); |
| 16 | + //# sourceURL=obs.inline.js |
| 17 | + </script> |
| 18 | + |
| 19 | + |
| 20 | + |
| 21 | + |
| 22 | + |
| 23 | + <title>Obs.js demo</title> |
| 24 | + |
| 25 | + |
| 26 | + |
| 27 | + |
| 28 | + |
| 29 | + <style> |
| 30 | + |
| 31 | + :root { |
| 32 | + --fg: #333; |
| 33 | + --bg: #f9f9f9; |
| 34 | + --brand: #f43059; |
| 35 | + --ok: #0a0; |
| 36 | + --warn: #a60; |
| 37 | + --bad: #a00; |
| 38 | + } |
| 39 | + |
| 40 | + html { |
| 41 | + color: var(--fg); |
| 42 | + background-color: var(--bg); |
| 43 | + font: 1em/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; |
| 44 | + font-weight: 400; |
| 45 | + } |
| 46 | + |
| 47 | + body { |
| 48 | + margin: 2rem auto; |
| 49 | + max-width: 70ch; |
| 50 | + padding: 0 1rem; |
| 51 | + } |
| 52 | + |
| 53 | + h1, h2 { |
| 54 | + text-wrap: balance; |
| 55 | + color: var(--brand); |
| 56 | + } |
| 57 | + |
| 58 | + code, kbd, samp, output, pre { |
| 59 | + font-family: "Operator Mono", SFMono-Regular, Inconsolata, Monaco, Consolas, "Andale Mono", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace; |
| 60 | + } |
| 61 | + |
| 62 | + a { |
| 63 | + color: var(--brand); |
| 64 | + text-decoration: none; |
| 65 | + font-weight: 600; |
| 66 | + } |
| 67 | + |
| 68 | + a:hover, |
| 69 | + a:active, |
| 70 | + a:focus { |
| 71 | + text-decoration: underline; |
| 72 | + } |
| 73 | + |
| 74 | + .c-pill { |
| 75 | + display: inline-block; |
| 76 | + border: 1px solid #ccc; |
| 77 | + padding: .15rem .5rem; |
| 78 | + border-radius: 999px; |
| 79 | + margin: .15rem .25rem .15rem 0; |
| 80 | + } |
| 81 | + |
| 82 | + .u-good { |
| 83 | + background: #e9f6ea; |
| 84 | + border-color: #cfe9d2; |
| 85 | + color: var(--ok); |
| 86 | + } |
| 87 | + |
| 88 | + .u-warn { |
| 89 | + background: #fff3e0; |
| 90 | + border-color: #ffe0b2; |
| 91 | + color: var(--warn); |
| 92 | + } |
| 93 | + |
| 94 | + .u-bad { |
| 95 | + background: #fdecea; |
| 96 | + border-color: #f5c6cb; |
| 97 | + color: var(--bad); |
| 98 | + } |
| 99 | + |
| 100 | + </style> |
| 101 | + |
| 102 | + |
| 103 | + |
| 104 | + |
| 105 | + |
| 106 | + <h1>Obs.js demo</h1> |
| 107 | + |
| 108 | + <p><a href=https://github.com/csswizardry/Obs.js>Obs.js</a> uses the Navigator |
| 109 | + and Battery APIs to get contextual information about your users’ connection |
| 110 | + strength and battery status.</p> |
| 111 | + |
| 112 | + <p>It is built and maintained by <a href=https://csswizardry.com>Harry |
| 113 | + Roberts</a> under the MIT license.</p> |
| 114 | + |
| 115 | + <p>This page shows the <code>.has-*</code> classes on |
| 116 | + <code><html></code> and the current <code>window.obs</code> object. |
| 117 | + Toggle Data Saver, plug/unplug power, or change networks to see updates (where |
| 118 | + supported).</p> |
| 119 | + |
| 120 | + <h2><code>html.classList</code></h2> |
| 121 | + |
| 122 | + <div id=classes aria-live=polite></div> |
| 123 | + |
| 124 | + <h2><code>window.obs</code></h2> |
| 125 | + |
| 126 | + <pre id=obs aria-live=polite></pre> |
| 127 | + |
| 128 | + <script> |
| 129 | + // Render helpers |
| 130 | + const byId = id => document.getElementById(id); |
| 131 | + const classesEl = byId('classes'); |
| 132 | + const obsEl = byId('obs'); |
| 133 | + |
| 134 | + const interesting = (cls) => cls.startsWith('has-'); |
| 135 | + |
| 136 | + // Map classes to traffic-light colours for quick visual scanning. |
| 137 | + const classify = (name) => { |
| 138 | + // Red (bad) |
| 139 | + if ( |
| 140 | + /battery-critical/.test(name) || |
| 141 | + /connection-capability-weak/.test(name) || |
| 142 | + /delivery-mode-lite/.test(name) |
| 143 | + ) return 'c-pill u-bad'; |
| 144 | + |
| 145 | + // Amber (warn) |
| 146 | + if ( |
| 147 | + /battery-low/.test(name) || |
| 148 | + /has-data-saver/.test(name) || |
| 149 | + /bandwidth-low/.test(name) || |
| 150 | + /latency-high/.test(name) || |
| 151 | + /conservation-preference-conserve/.test(name) || |
| 152 | + /connection-capability-moderate/.test(name) || |
| 153 | + /delivery-mode-cautious/.test(name) |
| 154 | + ) return 'c-pill u-warn'; |
| 155 | + |
| 156 | + // Green (good) |
| 157 | + if ( |
| 158 | + /connection-capability-strong/.test(name) || |
| 159 | + /delivery-mode-rich/.test(name) || |
| 160 | + /bandwidth-high/.test(name) || |
| 161 | + /latency-low/.test(name) |
| 162 | + ) return 'c-pill u-good'; |
| 163 | + |
| 164 | + // Neutral |
| 165 | + return 'c-pill'; |
| 166 | + }; |
| 167 | + |
| 168 | + function renderClasses() { |
| 169 | + const list = (document.documentElement.className || '') |
| 170 | + .split(/\s+/) |
| 171 | + .filter(Boolean) |
| 172 | + .filter(interesting) |
| 173 | + .sort((a,b)=>a.localeCompare(b)); |
| 174 | + |
| 175 | + if (!list.length) { |
| 176 | + classesEl.innerHTML = '<div>No <code>has-*</code> classes present (APIs may be unavailable).</div>'; |
| 177 | + return; |
| 178 | + } |
| 179 | + |
| 180 | + classesEl.innerHTML = ''; |
| 181 | + list.forEach(cls => { |
| 182 | + const span = document.createElement('samp'); |
| 183 | + span.className = classify(cls); |
| 184 | + span.textContent = '.' + cls; |
| 185 | + classesEl.appendChild(span); |
| 186 | + }); |
| 187 | + } |
| 188 | + |
| 189 | + function renderObs() { |
| 190 | + try { |
| 191 | + const snapshot = window.obs ? JSON.parse(JSON.stringify(window.obs)) : {}; |
| 192 | + obsEl.textContent = JSON.stringify(snapshot, null, 2); |
| 193 | + } catch { |
| 194 | + obsEl.textContent = String(window.obs); |
| 195 | + } |
| 196 | + } |
| 197 | + |
| 198 | + function renderAll() { |
| 199 | + renderClasses(); |
| 200 | + renderObs(); |
| 201 | + } |
| 202 | + |
| 203 | + // Initial paint |
| 204 | + renderAll(); |
| 205 | + |
| 206 | + // Repaint on likely changes (best-effort) |
| 207 | + // If observeChanges=true, Obs.js already listens to connection/battery. |
| 208 | + // Here we just repaint when the microtask queue is free. |
| 209 | + const queueRender = () => Promise.resolve().then(renderAll); |
| 210 | + |
| 211 | + // Patch minimal hooks so demo repaints when Obs.js updates: |
| 212 | + // (No-op if props don’t change—cheap.) |
| 213 | + ['change', 'levelchange', 'chargingchange'].forEach(evt => { |
| 214 | + // Connection changes (if available) |
| 215 | + if (navigator.connection?.addEventListener && evt === 'change') { |
| 216 | + navigator.connection.addEventListener('change', queueRender, { passive: true }); |
| 217 | + } |
| 218 | + }); |
| 219 | + |
| 220 | + // Battery changes (if available) |
| 221 | + if ('getBattery' in navigator) { |
| 222 | + navigator.getBattery().then(b => { |
| 223 | + if (typeof b.addEventListener === 'function') { |
| 224 | + b.addEventListener('levelchange', queueRender); |
| 225 | + b.addEventListener('chargingchange', queueRender); |
| 226 | + } |
| 227 | + }).catch(()=>{ /* no-op */ }); |
| 228 | + } |
| 229 | + |
| 230 | + // Also repaint after a tick to catch initial async battery read |
| 231 | + setTimeout(renderAll, 0); |
| 232 | + </script> |
0 commit comments