openvibers/openvibe.shared purse.js

purse.js

main 21.8 KB · 265 lines · javascript

1/* purse.js — the balances in the bar as one pill (4.189.0). The owner, 2026-11-10: "we just show one, but it can
2 animate between all three, especially when you spend some … first load it should scroll through them all, or every once
3 in a while … a custom animation for gaining and losing, with coins coming out of it … nice icons that are animated,
4 designed for the three currencies … it only needs to show the things you have."
5
6 navbar.js draws the pill (#openvibe-chips-all: one .ovnav-chip-mini per balance the person has, the first one showing)
7 and loads this file beside itself. This file gives each currency its own drawn icon, rolls the pill through the
8 balances on the first page of a visit and now and then after, and, when a balance changes, turns to it, counts the
9 number up or down and sends a few of its pieces in or out. catchUp reads the person's change window.
10
11 OpenVibePurse.mount(button) the pill, after each draw of the bar
12 OpenVibePurse.set(id, value) a balance changed ('coins', '1,234')
13 OpenVibePurse.icon(id, size) the currency's icon as SVG text ('coins' | 'vibes' | 'balance'), for other surfaces
14 OpenVibePurse.play(id, delta) the gain or spend effect at the bar's pill
15 OpenVibePurse.burst(el, id, delta) the same effect at any element: a buy button, a card, a tip
16 OpenVibePurse.count(el, from, to, format) count a number from one value to another
17 A holder with data-purse="coins|vibes|balance" has the currency's colour as --pc; .ovp-play plays its icon's move. */
18(function (root) {
19 'use strict';
20 if (!root || !root.document || root.OpenVibePurse) return;
21 const doc = root.document;
22 const svg = (size, body) => `<svg class="ovp-ic" viewBox="0 0 24 24" width="${size}" height="${size}" aria-hidden="true">${body}</svg>`;
23 // Each icon: the currency's colour as its body (--pc), the detail cut in a darker mix of it, one light edge.
24 const ICONS = {
25 // OpenCoins: a coin with the network's ring struck in it.
26 coins: '<g class="ovp-spin"><circle cx="12" cy="12" r="9.6" fill="var(--pc)"/><circle cx="12" cy="12" r="9.6" fill="none" stroke="var(--pd)" stroke-width="1.3" opacity=".55"/><circle cx="12" cy="12" r="4.4" fill="none" stroke="var(--pd)" stroke-width="2.5"/><path d="M5.6 9.2a7 7 0 0 1 4.6-4.1" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round" opacity=".7"/></g>',
27 // Vibes: a cut gem (bought, then given).
28 vibes: '<g class="ovp-gem"><path d="M7.2 3.6h9.6l4.4 5.6L12 21 2.8 9.2z" fill="var(--pc)" stroke="var(--pc)" stroke-width="1.6" stroke-linejoin="round"/><path d="M2.8 9.2h18.4M8.9 9.2 12 20.4l3.1-11.2M7.2 3.6l1.7 5.6L12 3.6l3.1 5.6 1.7-5.6" fill="none" stroke="var(--pd)" stroke-width="1.25" stroke-linejoin="round" opacity=".8"/><path class="ovp-glint" d="M6.3 7.3 8 5.2" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round" opacity=".75"/></g>',
29 // The money balance: a note with a round seal.
30 balance: '<g class="ovp-note"><rect x="2.2" y="5.6" width="19.6" height="12.8" rx="3.2" fill="var(--pc)"/><circle cx="12" cy="12" r="3.1" fill="none" stroke="var(--pd)" stroke-width="2.1"/><path d="M5.6 9.2v.01M18.4 14.8v.01" stroke="var(--pd)" stroke-width="2.2" stroke-linecap="round"/><path d="M5 8.4h3.4" stroke="#fff" stroke-width="1.3" stroke-linecap="round" opacity=".6"/></g>',
31 'channel-points': '<path d="m12 2.5 2.8 6.6 7.1.6-5.4 4.7 1.7 7-6.2-3.7-6.2 3.7 1.7-7-5.4-4.7 7.1-.6z" fill="var(--pc)" stroke="var(--pd)" stroke-width="1.2"/>',
32 };
33 const CSS = `
34[data-purse], .ovnav-chip-mini, .ovp-bit, .ovp-delta, .ovp-icw { --pc: var(--text-secondary, #a8b3c4); --pd: color-mix(in oklab, var(--pc) 42%, #000); }
35/* Each colour leans toward the page's text, so it is brighter on a dark theme and deeper on a light one. */
36[data-purse="coins"] { --pc: color-mix(in oklab, #f6b21d 88%, var(--text-primary, #e6edf7)); }
37[data-purse="vibes"] { --pc: color-mix(in oklab, #1fc78c 86%, var(--text-primary, #e6edf7)); }
38[data-purse="balance"] { --pc: color-mix(in oklab, var(--accent, #3b82f6) 78%, var(--text-primary, #e6edf7)); }
39[data-purse="channel-points"] { --pc: color-mix(in oklab, #e899fb 85%, var(--text-primary, #e6edf7)); }
40.ovp-ic { display: block; flex: none; overflow: visible; }
41.openvibe-navbar .ovnav-chip.ovnav-chip--all.is-purse { height: 32px; padding: 0 3px 0 2px; border-radius: 999px; overflow: hidden; font-size: 13.5px; font-weight: 750; letter-spacing: -.1px; transition: background-color .2s, border-color .3s, box-shadow .3s, transform .15s; }
42.openvibe-navbar .ovnav-chip--all.is-purse:active { transform: scale(.95); }
43.is-purse .ovnav-chip-mini { gap: 6px; padding: 0 8px 0 5px; font-variant-numeric: tabular-nums; transform: translateY(calc(85% * var(--ov-move, 1))); transition: transform .5s cubic-bezier(.3, 1.35, .45, 1), opacity .28s; }
44.is-purse .ovnav-chip-mini.is-on { transform: none; }
45.is-purse .ovnav-chip-mini.is-out { transform: translateY(calc(-85% * var(--ov-move, 1))); }
46.is-purse .ovnav-chip-mini .ovp-ic { width: 19px; height: 19px; filter: drop-shadow(0 1px 1.5px color-mix(in srgb, var(--pd) 55%, transparent)); }
47/* Each icon moves its own way as its balance turns up. */
48.is-purse .is-on .ovp-spin { transform-origin: 12px 12px; animation: ovpSpin .7s cubic-bezier(.3, .9, .3, 1) both; }
49.is-purse .is-on .ovp-gem { transform-origin: 12px 12px; animation: ovpGem .7s cubic-bezier(.3, 1.4, .4, 1) both; }
50.is-purse .is-on .ovp-note { transform-origin: 12px 12px; animation: ovpNote .6s cubic-bezier(.3, 1.3, .4, 1) both; }
51.is-purse .is-on .ovp-glint { animation: ovpGlint 2.8s ease-in-out .6s infinite; }
52/* Anywhere else: .ovp-play on an icon's holder plays its move once (take the class off and put it back to replay). */
53.ovp-play .ovp-spin { transform-origin: 12px 12px; animation: ovpSpin .7s cubic-bezier(.3, .9, .3, 1) both; }
54.ovp-play .ovp-gem { transform-origin: 12px 12px; animation: ovpGem .7s cubic-bezier(.3, 1.4, .4, 1) both; }
55.ovp-play .ovp-note { transform-origin: 12px 12px; animation: ovpNote .6s cubic-bezier(.3, 1.3, .4, 1) both; }
56.ovp-play .ovp-glint { animation: ovpGlint 2.8s ease-in-out .6s infinite; }
57@keyframes ovpSpin { from { transform: scaleX(calc(1 - 2 * var(--ov-move, 1))); } 50% { transform: scaleX(calc(1 - .9 * var(--ov-move, 1))); } }
58@keyframes ovpGem { from { transform: rotate(calc(-28deg * var(--ov-move, 1))) scale(.6); } }
59@keyframes ovpNote { from { transform: translateX(calc(-5px * var(--ov-move, 1))) rotate(calc(-12deg * var(--ov-move, 1))); } }
60@keyframes ovpGlint { 0%, 70%, 100% { opacity: .75; } 82% { opacity: .1; } }
61/* A balance went up or down: the pill takes that balance's colour for a moment and its icon jumps or dips. */
62.openvibe-navbar .ovnav-chip--all.is-purse.is-gain, .openvibe-navbar .ovnav-chip--all.is-purse.is-loss { border-color: color-mix(in srgb, var(--pf, currentColor) 60%, transparent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--pf, currentColor) 16%, transparent); }
63.is-purse.is-gain .is-on .ovp-ic { animation: ovpHop .55s cubic-bezier(.3, 1.6, .4, 1); }
64.is-purse.is-loss .is-on .ovp-ic { animation: ovpDip .5s ease-out; }
65@keyframes ovpHop { 35% { transform: translateY(calc(-4px * var(--ov-move, 1))) scale(1.22); } }
66@keyframes ovpDip { 35% { transform: translateY(calc(2px * var(--ov-move, 1))) scale(.84); } }
67.ovp-bit, .ovp-delta { position: fixed; z-index: 2147483000; pointer-events: none; left: 0; top: 0; will-change: transform, opacity; }
68.ovp-delta { padding: 2px 8px; border-radius: 999px; font: 800 12.5px/1.3 var(--font, system-ui, sans-serif); font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--pc);
69 background: color-mix(in srgb, var(--bg-elevated, var(--bg-secondary, #111826)) 92%, var(--pc)); border: 1px solid color-mix(in srgb, var(--pc) 45%, transparent); box-shadow: 0 6px 18px rgba(0, 0, 0, .28); }
70@media (max-width: 400px) { .openvibe-navbar .ovnav-chip.ovnav-chip--all.is-purse { font-size: 13px; letter-spacing: -.2px; } .is-purse .ovnav-chip-mini { gap: 4px; padding: 0 6px 0 4px; } .is-purse .ovnav-chip-mini .ovp-ic { width: 17px; height: 17px; } }
71:root:root[data-ov-motion=off] .is-purse .ovnav-chip-mini, :root:root[data-ov-motion=off] .is-purse .ovnav-chip-mini * { animation: none !important; transition: none !important; }`;
72
73 const num = (v) => { const s = v == null ? '' : String(v).trim(); if (!/^-?[\d,]+(\.\d+)?$/.test(s)) return null; const n = Number(s.replace(/,/g, '')); return isFinite(n) ? n : null; };
74 /** 160774 → "161k" (the same short form the bar writes). */
75 function short(n) {
76 const a = Math.abs(n);
77 if (a >= 1e6) return `${(n / 1e6).toFixed(a >= 1e7 ? 0 : 1).replace(/\.0$/, '')}M`;
78 if (a >= 1e4) return `${Math.round(n / 1e3)}k`;
79 if (a >= 1e3) return `${(n / 1e3).toFixed(1).replace(/\.0$/, '')}k`;
80 return String(Math.round(n));
81 }
82 const motion = () => { const m = doc.documentElement.getAttribute('data-ov-motion'); return m === 'off' ? 0 : m === 'reduced' ? .35 : 1; };
83 const seen = () => { try { return root.sessionStorage.getItem('ovp-toured') === '1'; } catch { return true; } };
84 const markSeen = () => { try { root.sessionStorage.setItem('ovp-toured', '1'); } catch { /* a private window: it tours again */ } };
85
86 let btn = null, minis = [], home = 0, at = 0, timers = [], last = {}, busy = false;
87 const later = (fn, ms) => { const t = setTimeout(fn, ms); timers.push(t); return t; };
88 const clear = () => { timers.forEach(clearTimeout); timers = []; };
89 const visible = () => btn && btn.isConnected && !doc.hidden && btn.getClientRects().length > 0;
90
91 function style() {
92 if (doc.getElementById('ovp-style')) return;
93 const st = doc.createElement('style'); st.id = 'ovp-style'; st.textContent = CSS; doc.head.appendChild(st);
94 }
95 /** Turn the pill to one balance: the one showing leaves upward, the next comes up from below. */
96 function show(i) {
97 if (!minis.length) return; i = ((i % minis.length) + minis.length) % minis.length;
98 if (i === at && minis[i].classList.contains('is-on')) return;
99 const out = minis[at];
100 minis.forEach((m) => m.classList.remove('is-out'));
101 if (out && out !== minis[i]) { out.classList.remove('is-on'); out.classList.add('is-out'); later(() => out.classList.remove('is-out'), 520); }
102 minis[i].classList.add('is-on'); at = i;
103 }
104 /** Now and then the pill turns to the next balance for a few seconds, then back to the largest. */
105 function idle() {
106 if (minis.length < 2) return;
107 let next = home;
108 const tick = () => {
109 if (!btn || !btn.isConnected) return;
110 if (!busy && visible()) { next = (next + 1) % minis.length; if (next === home) next = (next + 1) % minis.length; show(next); later(() => { if (!busy) show(home); }, 3600); }
111 later(tick, 42000);
112 };
113 later(tick, 42000);
114 }
115 /** The first page of a visit: each balance in turn, ending on the largest. */
116 function tour() {
117 if (minis.length < 2 || seen()) return idle();
118 markSeen();
119 let i = 0;
120 const step = () => { if (busy) return idle(); i += 1; if (i < minis.length) { show(i); later(step, 2300); } else { show(home); idle(); } };
121 later(step, 2600);
122 }
123 function mount(el) {
124 if (!el || !el.querySelectorAll) return;
125 style(); clear(); busy = false;
126 btn = el; minis = Array.from(el.querySelectorAll('.ovnav-chip-mini')); home = 0; at = 0; last = {};
127 el.classList.add('is-purse');
128 minis.forEach((m, i) => {
129 const id = m.getAttribute('data-chip-mini');
130 last[id] = num(m.getAttribute('data-v'));
131 if (ICONS[id]) {
132 m.setAttribute('data-purse', id);
133 const old = m.querySelector('.ovnav-ic, .ovp-ic'); const holder = doc.createElement('span'); holder.innerHTML = svg(19, ICONS[id]);
134 if (old) old.replaceWith(holder.firstChild); else m.insertBefore(holder.firstChild, m.firstChild);
135 }
136 m.classList.toggle('is-on', i === 0); m.classList.remove('is-out');
137 });
138 tour();
139 }
140 /** A few of the currency's pieces: into the pill from below on a gain, out of it and down on a spend. */
141 function pieces(id, rect, gain, k, at) {
142 if (!ICONS[id] || typeof doc.body.animate !== 'function' || k === 0) return;
143 // at: { x, y } where the pieces meet (the pill's icon by default), host: the layer they are drawn in, up: they fly above it.
144 const cx = at && at.x != null ? at.x : rect.left + 14, cy = at && at.y != null ? at.y : rect.top + rect.height / 2, n = k < 1 ? 3 : 7, host = (at && at.host) || doc.body, sign = at && at.up ? -1 : 1;
145 for (let i = 0; i < n; i += 1) {
146 const b = doc.createElement('span'); b.className = 'ovp-bit'; b.setAttribute('data-purse', id); b.innerHTML = svg(13, ICONS[id]);
147 host.appendChild(b);
148 const spread = ((i / (n - 1)) - .5) * 2, dx = spread * 34 * k + (Math.random() - .5) * 10, dy = sign * (38 + Math.random() * 26) * k, turn = (Math.random() - .5) * 260 * k;
149 const far = `translate(${cx - 6 + dx}px, ${cy - 6 + dy}px) rotate(${turn}deg) scale(.9)`, near = `translate(${cx - 6}px, ${cy - 6}px) rotate(0deg) scale(.55)`;
150 const mid = `translate(${cx - 6 + dx * .7}px, ${cy - 6 + dy * (gain ? .75 : .25) - (gain ? 0 : 9 * k)}px) rotate(${turn / 2}deg) scale(1)`;
151 const frames = gain ? [{ transform: far, opacity: 0 }, { transform: mid, opacity: 1, offset: .3 }, { transform: near, opacity: .9, offset: .92 }, { transform: near, opacity: 0 }]
152 : [{ transform: near, opacity: 0 }, { transform: mid, opacity: 1, offset: .28 }, { transform: far, opacity: 0 }];
153 const a = b.animate(frames, { duration: gain ? 760 : 900, delay: i * (gain ? 55 : 40), easing: gain ? 'cubic-bezier(.5,0,.3,1)' : 'cubic-bezier(.2,.6,.5,1)', fill: 'both' });
154 const done = () => b.remove(); a.onfinish = done; a.oncancel = done; setTimeout(done, 2200);
155 }
156 }
157 /** "+15" or "−200" under the pill: it rises into the pill on a gain and falls away on a spend. */
158 function delta(id, rect, d, k, at) {
159 if (typeof doc.body.animate !== 'function') return;
160 const host = (at && at.host) || doc.body, up = !!(at && at.up);
161 const t = doc.createElement('span'); t.className = 'ovp-delta'; t.setAttribute('data-purse', id);
162 t.textContent = `${d > 0 ? '+' : '−'}${Math.abs(d) >= 1e4 ? short(Math.abs(d)) : Math.abs(d).toLocaleString()}`;
163 host.appendChild(t);
164 const x = Math.max(6, Math.min(root.innerWidth - t.offsetWidth - 6, rect.left + rect.width / 2 - t.offsetWidth / 2)), y = up ? rect.top - t.offsetHeight - 6 : rect.bottom + 6, m = (up ? -16 : 16) * (k || .01);
165 const frames = d > 0 ? [{ transform: `translate(${x}px, ${y + m}px)`, opacity: 0 }, { transform: `translate(${x}px, ${y}px)`, opacity: 1, offset: .22 }, { transform: `translate(${x}px, ${y}px)`, opacity: 1, offset: .78 }, { transform: `translate(${x}px, ${y - m * .6}px) scale(.9)`, opacity: 0 }]
166 : [{ transform: `translate(${x}px, ${y - m * .5}px)`, opacity: 0 }, { transform: `translate(${x}px, ${y}px)`, opacity: 1, offset: .22 }, { transform: `translate(${x}px, ${y}px)`, opacity: 1, offset: .75 }, { transform: `translate(${x}px, ${y + m}px)`, opacity: 0 }];
167 const a = t.animate(frames, { duration: 1900, easing: 'ease-out', fill: 'both' });
168 const done = () => t.remove(); a.onfinish = done; a.oncancel = done; setTimeout(done, 2600);
169 }
170 function play(id, d) {
171 if (!btn || !visible() || !d || motion() === 0) return false;
172 const i = minis.findIndex((m) => m.getAttribute('data-chip-mini') === id);
173 const k = motion(), rect = btn.getBoundingClientRect();
174 busy = true; clear(); if (i >= 0) show(i);
175 btn.style.setProperty('--pf', i >= 0 ? root.getComputedStyle(minis[i]).getPropertyValue('--pc') || 'currentColor' : 'currentColor');
176 btn.classList.remove('is-gain', 'is-loss'); void btn.offsetWidth; btn.classList.add(d > 0 ? 'is-gain' : 'is-loss');
177 if (k === 1) pieces(id, rect, d > 0, k); delta(id, rect, d, k);
178 later(() => { btn.classList.remove('is-gain', 'is-loss'); }, 1500);
179 later(() => { busy = false; show(home); idle(); }, 3400);
180 return true;
181 }
182 /**
183 * The gain or spend effect at any element (4.190.0): the currency's pieces fly into it (a gain) or out of it (a
184 * spend) and the amount shows beside it. It works inside an open dialog, and goes upward near the bottom of the
185 * screen. burst(button, 'coins', -200) when someone spends; burst(card, 'vibes', 50) when they receive.
186 */
187 function burst(el, id, d) {
188 if (!el || !el.getBoundingClientRect || !d || !ICONS[id]) return;
189 style();
190 const rect = el.getBoundingClientRect(); if (!rect.width) return;
191 const icon = el.querySelector && el.querySelector('.ovp-ic'), ir = icon ? icon.getBoundingClientRect() : rect;
192 const at = { x: ir.left + ir.width / 2, y: ir.top + ir.height / 2, host: (el.closest && el.closest('dialog[open]')) || doc.body, up: rect.bottom > root.innerHeight - 110 };
193 const k = motion();
194 pieces(id, rect, d > 0, k, at); delta(id, rect, d, k, at);
195 }
196 /** Count a number from what it was to what it is: in the bar's short form, or with the format given. */
197 function count(el, from, to, fmt) {
198 if (!el || typeof root.requestAnimationFrame !== 'function') return;
199 if (motion() === 0) { el.textContent = (fmt || short)(to); return; }
200 const t0 = root.performance.now(), ms = 800;
201 const step = (t) => { const p = Math.min(1, (t - t0) / ms), e = 1 - Math.pow(1 - p, 3); if (!el.isConnected) return; el.textContent = (fmt || short)(p < 1 ? from + (to - from) * e : to); if (p < 1) root.requestAnimationFrame(step); };
202 root.requestAnimationFrame(step);
203 }
204 function set(id, value) {
205 const n = num(value), was = last[id];
206 last[id] = n;
207 if (!btn || n == null || was == null || n === was) return;
208 const m = minis.find((x) => x.getAttribute('data-chip-mini') === id); if (!m) return;
209 m.setAttribute('data-v', String(value));
210 play(id, n - was); count(m.querySelector('.ovnav-chip-mv'), was, n);
211 }
212 let recent = null, recentUser = null, running = null, runningUser = null;
213 const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
214 function recentChanges(user) {
215 const id = user || recentUser;
216 if (!id) return null;
217 if (recent && id === recentUser) return recent;
218 try { return JSON.parse(root.sessionStorage.getItem(`ovp-changes:${id}`) || 'null'); } catch { return null; }
219 }
220 async function catchUp(options = {}) {
221 const { frameBase = 'https://openvibe.network/api/frame', token, user, streamer, valid } = options;
222 if (!user || typeof root.fetch !== 'function') return;
223 if (recentUser !== user) recent = null;
224 recentUser = user;
225 if (running) return runningUser === user ? running : running.then(() => catchUp(options));
226 runningUser = user;
227 const base = String(frameBase).replace(/\/+$/, '');
228 const headers = token ? { Authorization: `Bearer ${token}` } : {};
229 const credentials = token ? 'omit' : 'include';
230 const channel = /^[1-9]\d{0,15}$/.test(String(streamer || '')) ? String(streamer) : '';
231 const url = `${base}/wallet/changes${channel ? `?streamer=${channel}` : ''}`;
232 running = (async () => {
233 let answer;
234 try {
235 const response = await root.fetch(url, { headers, credentials });
236 if (!response.ok) return;
237 answer = await response.json();
238 } catch { return; }
239 if (!answer || typeof answer !== 'object' || (valid && !valid())) return;
240 const order = [['opencoins', 'coins'], ['vibes', 'vibes'], ['channel_points', 'channel-points']];
241 const changed = order.filter(([name]) => answer[name] && Number.isSafeInteger(Number(answer[name].net)) && Number(answer[name].net) !== 0 && answer[name].cursor != null);
242 if (!changed.length) return;
243 recent = answer; recentUser = user;
244 try { root.sessionStorage.setItem(`ovp-changes:${user}`, JSON.stringify(answer)); } catch { /* closed storage */ }
245 for (const [name, id] of changed) {
246 const part = answer[name], cursor = String(part.cursor);
247 const guard = `ovp-read:${user}:${name}:${name === 'channel_points' ? channel : ''}`;
248 let previous = null; try { previous = root.sessionStorage.getItem(guard); } catch { /* closed storage */ }
249 if ((valid && !valid()) || previous === cursor || (motion() !== 0 && !visible())) continue;
250 if (motion() !== 0) play(id, Number(part.net));
251 try { root.sessionStorage.setItem(guard, cursor); } catch { /* closed storage */ }
252 try {
253 await root.fetch(`${base}/wallet/changes/read`, { method: 'PUT', credentials, headers: { ...headers, 'Content-Type': 'application/json' },
254 body: JSON.stringify({ currency: id === 'coins' ? 'opencoins' : id, cursor: part.cursor, ...(id === 'channel-points' ? { streamer: Number(channel) } : {}) }) });
255 } catch { /* the server's read state will be retried on the next visit */ }
256 if (changed.at(-1)[0] !== name) await pause(700);
257 }
258 })().finally(() => { running = null; runningUser = null; });
259 return running;
260 }
261 root.OpenVibePurse = { mount, set, play, burst, count, catchUp, recentChanges, icon: (id, size) => { style(); return ICONS[id] ? svg(size || 20, ICONS[id]) : ''; }, ids: Object.keys(ICONS) };
262 if (root.__ovPursePending) { const pending = root.__ovPursePending; delete root.__ovPursePending; catchUp(pending); }
263 const now = doc.getElementById('openvibe-chips-all'); if (now) mount(now);
264})(typeof window !== 'undefined' ? window : null);
265