Skip to content

Commit 93efdd2

Browse files
authored
Merge pull request #3 from csswizardry/demo
Add demo
2 parents c38264b + 86a5935 commit 93efdd2

2 files changed

Lines changed: 235 additions & 3 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,9 @@ Obs.js also stores the following properties on the `window.obs` object:
177177
| `downlinkMax` | number (Mbps) | Max estimated downlink (if exposed) | `navigator.connection.downlinkMax` | Not used for Stances; informational only |
178178
| `connectionCapability` | `'strong' \| 'moderate' \| 'weak'` | Transport assessment | Derived from `rttCategory` and `downlinkBucket` | Strong = low RTT **and** high BW; Weak = high RTT **or** low BW |
179179
| `conservationPreference` | `'conserve' \| 'neutral'` | Frugality signal | `dataSaver === true` **or** `batteryLow === true``conserve` ||
180-
| `deliveryMode` | `'rich' \| 'cautious' \| 'lite'` | How heavy you should go | Derived from capability and conservation | Rich if **strong** and **not** conserving; Lite if **weak** or **conserve**; else Cautious |
181-
| `canShowRichMedia` | boolean | Convenience: `deliveryMode === 'rich'` | Derived from `deliveryMode` | Shorthand for go big |
182-
| `shouldAvoidRichMedia` | boolean | Convenience: `deliveryMode === 'lite'` | Derived from `deliveryMode` | Shorthand for be frugal |
180+
| `deliveryMode` | `'rich' \| 'cautious' \| 'lite'` | How heavy you should go | Derived from capability and conservation | Rich if **strong** and **not** conserving; Lite if **weak** or **conserve**; else Cautious |
181+
| `canShowRichMedia` | boolean | Convenience: `deliveryMode === 'rich'` | Derived from `deliveryMode` | Shorthand for go big |
182+
| `shouldAvoidRichMedia` | boolean | Convenience: `deliveryMode === 'lite'` | Derived from `deliveryMode` | Shorthand for be frugal |
183183
| `batteryLow` | boolean \| null | Battery ≤20% | Battery API | `true` when battery level is ≤20%; `null` if unknown |
184184
| `batteryCritical` | boolean \| null | Battery ≤5% | Battery API | `true` when battery level is ≤5%; `true` in addition to `batteryLow` |
185185
| `batteryCharging` | boolean \| null | On charge | Battery API | `null` if unknown |

demo/index.html

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
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>&lt;html&gt;</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

Comments
 (0)