Spaces:
Running
Running
Eric Putnam
Claude Opus 5 (1M context)
feat: deploy simpledocreview as a browser-only static space
3d968d9 Download sdr-api-shim.js from Hoctar77/SimpleDocReview: direct link, hf CLI and curl.
- Browser
- Download file 25.5 kB
-
https://huggingface.co/spaces/Hoctar77/SimpleDocReview/resolve/main/sdr-api-shim.js
- Command line
-
hf download hf://spaces/Hoctar77/SimpleDocReview/sdr-api-shim.js
-
curl -L -o sdr-api-shim.js https://huggingface.co/spaces/Hoctar77/SimpleDocReview/resolve/main/sdr-api-shim.js
25.5 kB
| /** | |
| * sdr-api-shim.js - wires sdr-api-core.js into the page in place of app.py. | |
| * | |
| * A Static Space serves files and nothing else, so the `http://127.0.0.1:4173` | |
| * server upstream's `static/app.js` talks to does not exist. This file answers | |
| * those requests in the browser: | |
| * | |
| * 1. an IndexedDB store standing in for data/reviews.json and uploads/, | |
| * 2. an XMLHttpRequest (and fetch) interceptor for every /api/ path, | |
| * 3. a click interceptor for the Download button, which upstream implements | |
| * as a navigation rather than a request, | |
| * 4. a one-line notice, because on a public URL "your documents stay in this | |
| * browser" is not what a visitor would assume. | |
| * | |
| * Nothing here reimplements review logic - that all lives in sdr-api-core.js. | |
| * This file is only the environment. | |
| * | |
| * Loaded by a <script> tag that scripts/build-static.ps1 inserts ahead of | |
| * upstream's static/app.js. Upstream's files are never edited. | |
| */ | |
| (function () { | |
| "use strict"; | |
| if (typeof window === "undefined" || window.__sdrApiShimInstalled) return; | |
| window.__sdrApiShimInstalled = true; | |
| var Core = window.SdrApiCore; | |
| if (!Core) { | |
| console.error("[SimpleDocReview] sdr-api-core.js did not load; the app cannot run."); | |
| return; | |
| } | |
| var DB_NAME = "simpledocreview"; | |
| var DB_VERSION = 1; | |
| var RECORD_STORE = "records"; | |
| var UPLOAD_STORE = "uploads"; | |
| var RECORD_KEY = "db"; | |
| // ------------------------------------------------------------------- store | |
| function openDatabase() { | |
| return new Promise(function (resolve, reject) { | |
| if (!window.indexedDB) { | |
| reject(new Error("IndexedDB is unavailable.")); | |
| return; | |
| } | |
| var request = window.indexedDB.open(DB_NAME, DB_VERSION); | |
| request.onupgradeneeded = function () { | |
| var database = request.result; | |
| if (!database.objectStoreNames.contains(RECORD_STORE)) { | |
| database.createObjectStore(RECORD_STORE); | |
| } | |
| if (!database.objectStoreNames.contains(UPLOAD_STORE)) { | |
| database.createObjectStore(UPLOAD_STORE); | |
| } | |
| }; | |
| request.onsuccess = function () { resolve(request.result); }; | |
| request.onerror = function () { reject(request.error || new Error("IndexedDB open failed.")); }; | |
| request.onblocked = function () { reject(new Error("IndexedDB is blocked by another tab.")); }; | |
| }); | |
| } | |
| function transact(storeName, mode, work) { | |
| return openDatabase().then(function (database) { | |
| return new Promise(function (resolve, reject) { | |
| var transaction = database.transaction(storeName, mode); | |
| var request = work(transaction.objectStore(storeName)); | |
| transaction.oncomplete = function () { | |
| database.close(); | |
| resolve(request ? request.result : undefined); | |
| }; | |
| transaction.onerror = function () { | |
| database.close(); | |
| reject(transaction.error || new Error("IndexedDB transaction failed.")); | |
| }; | |
| transaction.onabort = function () { | |
| database.close(); | |
| reject(transaction.error || new Error("IndexedDB transaction aborted.")); | |
| }; | |
| }); | |
| }); | |
| } | |
| var indexedDbStore = { | |
| load: function () { | |
| return transact(RECORD_STORE, "readonly", function (store) { | |
| return store.get(RECORD_KEY); | |
| }).then(function (value) { | |
| return value || null; | |
| }); | |
| }, | |
| save: function (data) { | |
| return transact(RECORD_STORE, "readwrite", function (store) { | |
| return store.put(data, RECORD_KEY); | |
| }); | |
| }, | |
| putUpload: function (name, bytes) { | |
| return transact(UPLOAD_STORE, "readwrite", function (store) { | |
| return store.put(bytes, name); | |
| }); | |
| }, | |
| getUpload: function (name) { | |
| if (!name) return Promise.resolve(null); | |
| return transact(UPLOAD_STORE, "readonly", function (store) { | |
| return store.get(name); | |
| }).then(function (value) { | |
| if (!value) return null; | |
| return value instanceof Uint8Array ? value : new Uint8Array(value); | |
| }); | |
| }, | |
| clear: function () { | |
| return transact(RECORD_STORE, "readwrite", function (store) { | |
| return store.clear(); | |
| }).then(function () { | |
| return transact(UPLOAD_STORE, "readwrite", function (store) { | |
| return store.clear(); | |
| }); | |
| }); | |
| } | |
| }; | |
| /** | |
| * Fallback for browsers that refuse storage. | |
| * | |
| * The Space renders inside an iframe on huggingface.co, and some browsers | |
| * partition or block storage for embedded third-party origins. Losing | |
| * persistence is better than a dead page, so the app still works for the | |
| * length of the visit and the notice says so. | |
| */ | |
| function createMemoryStore() { | |
| var records = null; | |
| var uploads = {}; | |
| return { | |
| volatile: true, | |
| load: function () { return Promise.resolve(records); }, | |
| save: function (data) { records = data; return Promise.resolve(); }, | |
| putUpload: function (name, bytes) { uploads[name] = bytes; return Promise.resolve(); }, | |
| getUpload: function (name) { return Promise.resolve(uploads[name] || null); }, | |
| clear: function () { records = null; uploads = {}; return Promise.resolve(); } | |
| }; | |
| } | |
| /** | |
| * Probe storage once, then keep using whichever backend answered. | |
| * | |
| * The probe result is cached rather than retried per request, so a page that | |
| * starts in memory stays consistent instead of writing half its records to | |
| * one backend and half to the other. | |
| */ | |
| var activeStore = null; | |
| var storeProbe = null; | |
| function resolveStore() { | |
| if (activeStore) return Promise.resolve(activeStore); | |
| if (!storeProbe) { | |
| storeProbe = indexedDbStore | |
| .load() | |
| .then(function () { | |
| activeStore = indexedDbStore; | |
| return activeStore; | |
| }) | |
| .catch(function (error) { | |
| console.warn( | |
| "[SimpleDocReview] IndexedDB unavailable (" + error.message + "); " + | |
| "documents will be kept in memory for this visit only." | |
| ); | |
| activeStore = createMemoryStore(); | |
| return activeStore; | |
| }); | |
| } | |
| return storeProbe; | |
| } | |
| var store = { | |
| load: function () { return resolveStore().then(function (s) { return s.load(); }); }, | |
| save: function (data) { return resolveStore().then(function (s) { return s.save(data); }); }, | |
| putUpload: function (n, b) { return resolveStore().then(function (s) { return s.putUpload(n, b); }); }, | |
| getUpload: function (n) { return resolveStore().then(function (s) { return s.getUpload(n); }); }, | |
| clear: function () { return resolveStore().then(function (s) { return s.clear(); }); } | |
| }; | |
| var api = Core.createApi({ store: store }); | |
| // ------------------------------------------------------------ request body | |
| function apiError(message, status) { | |
| var error = new Error(message); | |
| error.apiStatus = status || 400; | |
| return error; | |
| } | |
| /** | |
| * Turn an XHR/fetch body into what the core API expects. | |
| * | |
| * Upload is the only interesting case: upstream posts multipart/form-data, | |
| * which app.py parses back apart. Here the FormData object is already | |
| * structured, so the parse step is just reading the parts out - the file as | |
| * bytes, everything else as a text field. | |
| */ | |
| function normalizeBody(body) { | |
| if (body === null || body === undefined) return Promise.resolve(null); | |
| if (typeof body === "string") return Promise.resolve(body); | |
| if (typeof FormData !== "undefined" && body instanceof FormData) { | |
| var file = body.get("file"); | |
| if (!file || typeof file === "string") { | |
| return Promise.reject(apiError("No file part was found in the upload.", 400)); | |
| } | |
| var fields = {}; | |
| body.forEach(function (value, key) { | |
| if (typeof value === "string") fields[key] = value.trim(); | |
| }); | |
| return file.arrayBuffer().then(function (buffer) { | |
| return { | |
| filename: file.name, | |
| bytes: new Uint8Array(buffer), | |
| fields: fields | |
| }; | |
| }); | |
| } | |
| if (typeof Blob !== "undefined" && body instanceof Blob) return body.text(); | |
| if (body instanceof ArrayBuffer) return Promise.resolve(new TextDecoder().decode(body)); | |
| if (body && body.buffer instanceof ArrayBuffer) { | |
| return Promise.resolve(new TextDecoder().decode(body)); | |
| } | |
| return Promise.resolve(String(body)); | |
| } | |
| function toPathname(url) { | |
| try { | |
| return new URL(String(url), window.location.href).pathname; | |
| } catch (error) { | |
| return String(url); | |
| } | |
| } | |
| function isApiUrl(url) { | |
| return api.isApiPath(toPathname(url)); | |
| } | |
| // ----------------------------------------------------- selected document id | |
| /** | |
| * The id of the document currently on screen. | |
| * | |
| * Upstream's Download button navigates to /api/documents/<id>/download rather | |
| * than requesting it, and a page cannot cancel an assignment to | |
| * window.location.href. The click is intercepted instead (see below), which | |
| * means the id has to come from somewhere other than the event. | |
| * | |
| * Every render of the document panel is driven by a response for that | |
| * document - selectDocument() and refreshSelected() both fetch it by id, and | |
| * the reviewer actions post to it - so the last /api/documents/<id> path to | |
| * pass through here names what the panel is showing. | |
| */ | |
| var lastDocumentId = null; | |
| function rememberDocumentId(pathname) { | |
| var match = /^\/api\/documents\/([^/]+)/.exec(pathname); | |
| if (match && match[1] !== "download") lastDocumentId = decodeURIComponent(match[1]); | |
| } | |
| function dispatchToCore(method, url, body) { | |
| var pathname = toPathname(url); | |
| rememberDocumentId(pathname); | |
| return normalizeBody(body).then(function (payload) { | |
| return api.handle(method, pathname, payload); | |
| }); | |
| } | |
| // ------------------------------------------------- XMLHttpRequest override | |
| var RealXMLHttpRequest = window.XMLHttpRequest; | |
| var SIMULATED_PROPERTIES = { | |
| readyState: true, | |
| status: true, | |
| statusText: true, | |
| response: true, | |
| responseText: true, | |
| responseURL: true | |
| }; | |
| var HANDLER_PROPERTIES = { | |
| onreadystatechange: true, | |
| onload: true, | |
| onloadstart: true, | |
| onloadend: true, | |
| onerror: true, | |
| onabort: true, | |
| ontimeout: true, | |
| onprogress: true | |
| }; | |
| /** | |
| * An XMLHttpRequest that answers /api/ locally and forwards everything else. | |
| * | |
| * A Proxy over a real XMLHttpRequest rather than a hand-written stand-in: any | |
| * member this shim does not simulate still reaches the genuine object, so a | |
| * future upstream change that uses more of the XHR surface keeps working | |
| * instead of hitting a gap in a fake. | |
| */ | |
| function createInterceptingXhr() { | |
| var real = new RealXMLHttpRequest(); | |
| var state = { | |
| intercepted: false, | |
| method: "GET", | |
| url: "", | |
| overrides: {}, | |
| handlers: {}, | |
| listeners: {}, | |
| sent: false | |
| }; | |
| var proxy; | |
| function dispatch(type) { | |
| var event = { | |
| type: type, | |
| target: proxy, | |
| currentTarget: proxy, | |
| lengthComputable: false, | |
| loaded: 0, | |
| total: 0 | |
| }; | |
| var handler = state.handlers["on" + type]; | |
| if (typeof handler === "function") handler.call(proxy, event); | |
| (state.listeners[type] || []).forEach(function (listener) { | |
| if (typeof listener === "function") listener.call(proxy, event); | |
| else if (listener && typeof listener.handleEvent === "function") listener.handleEvent(event); | |
| }); | |
| } | |
| function settle(response) { | |
| state.overrides.readyState = 4; | |
| state.overrides.status = response.status; | |
| state.overrides.statusText = response.status === 200 ? "OK" : ""; | |
| state.overrides.responseURL = new URL(state.url, window.location.href).href; | |
| // A download reply carries bytes, not text. Upstream never reads one | |
| // through XHR (it navigates instead), so the body is left empty rather | |
| // than lossily decoded. | |
| state.overrides.responseText = response.body || ""; | |
| state.overrides.response = response.body || ""; | |
| state.overrides.__headers = response.headers || {}; | |
| state.overrides.__bytes = response.bytes || null; | |
| dispatch("readystatechange"); | |
| dispatch("load"); | |
| dispatch("loadend"); | |
| } | |
| function failed(error) { | |
| if (error && error.apiStatus) { | |
| settle({ | |
| status: error.apiStatus, | |
| headers: { "Content-Type": "application/json; charset=utf-8" }, | |
| body: JSON.stringify({ error: error.message }) | |
| }); | |
| return; | |
| } | |
| console.error("[SimpleDocReview] local API failed:", error); | |
| state.overrides.readyState = 4; | |
| state.overrides.status = 0; | |
| state.overrides.responseText = ""; | |
| state.overrides.response = ""; | |
| dispatch("readystatechange"); | |
| dispatch("error"); | |
| dispatch("loadend"); | |
| } | |
| var members = { | |
| open: function (method, url) { | |
| state.method = String(method || "GET"); | |
| state.url = String(url); | |
| state.intercepted = isApiUrl(state.url); | |
| state.overrides = {}; | |
| state.sent = false; | |
| if (state.intercepted) { | |
| state.overrides.readyState = 1; | |
| dispatch("readystatechange"); | |
| return undefined; | |
| } | |
| return real.open.apply(real, arguments); | |
| }, | |
| send: function (body) { | |
| if (!state.intercepted) return real.send.apply(real, arguments); | |
| if (state.sent) throw new Error("InvalidStateError: send() already called."); | |
| state.sent = true; | |
| dispatch("loadstart"); | |
| dispatchToCore(state.method, state.url, body).then(settle, failed); | |
| return undefined; | |
| }, | |
| setRequestHeader: function () { | |
| if (state.intercepted) return undefined; | |
| return real.setRequestHeader.apply(real, arguments); | |
| }, | |
| getResponseHeader: function (name) { | |
| if (!state.intercepted) return real.getResponseHeader.apply(real, arguments); | |
| var headers = state.overrides.__headers || {}; | |
| var wanted = String(name).toLowerCase(); | |
| var found = Object.keys(headers).filter(function (key) { | |
| return key.toLowerCase() === wanted; | |
| })[0]; | |
| return found ? headers[found] : null; | |
| }, | |
| getAllResponseHeaders: function () { | |
| if (!state.intercepted) return real.getAllResponseHeaders.apply(real, arguments); | |
| var headers = state.overrides.__headers || {}; | |
| return Object.keys(headers) | |
| .map(function (key) { return key.toLowerCase() + ": " + headers[key] + "\r\n"; }) | |
| .join(""); | |
| }, | |
| abort: function () { | |
| if (!state.intercepted) return real.abort.apply(real, arguments); | |
| // The work is already running locally and has nothing to cancel; report | |
| // the aborted state so callers waiting on it are not left hanging. | |
| state.overrides.readyState = 4; | |
| state.overrides.status = 0; | |
| dispatch("readystatechange"); | |
| dispatch("abort"); | |
| dispatch("loadend"); | |
| return undefined; | |
| }, | |
| addEventListener: function (type, listener) { | |
| if (!state.listeners[type]) state.listeners[type] = []; | |
| state.listeners[type].push(listener); | |
| return real.addEventListener.apply(real, arguments); | |
| }, | |
| removeEventListener: function (type, listener) { | |
| state.listeners[type] = (state.listeners[type] || []).filter(function (item) { | |
| return item !== listener; | |
| }); | |
| return real.removeEventListener.apply(real, arguments); | |
| } | |
| }; | |
| proxy = new Proxy(real, { | |
| get: function (target, property) { | |
| if (Object.prototype.hasOwnProperty.call(members, property)) return members[property]; | |
| if (Object.prototype.hasOwnProperty.call(HANDLER_PROPERTIES, property)) { | |
| return state.handlers[property] === undefined ? null : state.handlers[property]; | |
| } | |
| if (state.intercepted && Object.prototype.hasOwnProperty.call(SIMULATED_PROPERTIES, property)) { | |
| var override = state.overrides[property]; | |
| if (override !== undefined) return override; | |
| if (property === "readyState" || property === "status") return 0; | |
| return ""; | |
| } | |
| var value = target[property]; | |
| return typeof value === "function" ? value.bind(target) : value; | |
| }, | |
| set: function (target, property, value) { | |
| if (Object.prototype.hasOwnProperty.call(HANDLER_PROPERTIES, property)) { | |
| state.handlers[property] = value; | |
| return true; | |
| } | |
| target[property] = value; | |
| return true; | |
| } | |
| }); | |
| return proxy; | |
| } | |
| function ShimXMLHttpRequest() { | |
| return createInterceptingXhr(); | |
| } | |
| ShimXMLHttpRequest.prototype = RealXMLHttpRequest.prototype; | |
| ["UNSENT", "OPENED", "HEADERS_RECEIVED", "LOADING", "DONE"].forEach(function (name) { | |
| ShimXMLHttpRequest[name] = RealXMLHttpRequest[name]; | |
| }); | |
| window.XMLHttpRequest = ShimXMLHttpRequest; | |
| // ------------------------------------------------------------ fetch override | |
| // Upstream uses XHR throughout. This covers /api/ requests made with fetch so | |
| // that a future upstream rewrite does not silently start 404ing. | |
| if (typeof window.fetch === "function") { | |
| var realFetch = window.fetch.bind(window); | |
| window.fetch = function (input, init) { | |
| var url = typeof input === "string" | |
| ? input | |
| : input instanceof URL ? input.toString() : input && input.url; | |
| if (!url || !isApiUrl(url)) return realFetch(input, init); | |
| var method = ((init && init.method) || (input && input.method) || "GET").toUpperCase(); | |
| var body = init ? init.body : null; | |
| return dispatchToCore(method, url, body).then(function (response) { | |
| var payload = response.bytes ? response.bytes : response.body; | |
| return new Response(payload, { status: response.status, headers: response.headers }); | |
| }, function (error) { | |
| if (error && error.apiStatus) { | |
| return new Response(JSON.stringify({ error: error.message }), { | |
| status: error.apiStatus, | |
| headers: { "Content-Type": "application/json; charset=utf-8" } | |
| }); | |
| } | |
| throw error; | |
| }); | |
| }; | |
| } | |
| // --------------------------------------------------------------- downloads | |
| /** | |
| * Serve the Download button from IndexedDB. | |
| * | |
| * upstream: els.downloadButton.onclick = () => { | |
| * window.location.href = `/api/documents/${document.id}/download`; | |
| * } | |
| * | |
| * A navigation cannot be intercepted, so the click is caught in the capture | |
| * phase at the document level - before the target phase, which is what stops | |
| * upstream's own handler from running - and the file is handed over as a blob | |
| * URL instead. | |
| */ | |
| document.addEventListener( | |
| "click", | |
| function (event) { | |
| var target = event.target; | |
| if (!target || typeof target.closest !== "function") return; | |
| if (!target.closest("#downloadButton")) return; | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation(); | |
| if (!lastDocumentId) { | |
| console.warn("[SimpleDocReview] No document is selected, so there is nothing to download."); | |
| return; | |
| } | |
| api.handle("GET", "/api/documents/" + encodeURIComponent(lastDocumentId) + "/download", null) | |
| .then(function (response) { | |
| if (response.status !== 200 || !response.bytes) { | |
| var message = "The original upload could not be found."; | |
| try { | |
| message = JSON.parse(response.body).error || message; | |
| } catch (error) { /* keep the default */ } | |
| console.warn("[SimpleDocReview] " + message); | |
| return; | |
| } | |
| var blob = new Blob([response.bytes], { | |
| type: response.headers["Content-Type"] || "application/octet-stream" | |
| }); | |
| var url = URL.createObjectURL(blob); | |
| var link = document.createElement("a"); | |
| link.href = url; | |
| link.download = response.filename || "document"; | |
| link.rel = "noopener"; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| // Revoke on a later turn: Firefox cancels an in-flight download if the | |
| // object URL disappears in the same task as the click. | |
| setTimeout(function () { URL.revokeObjectURL(url); }, 30000); | |
| }) | |
| .catch(function (error) { | |
| console.error("[SimpleDocReview] Download failed:", error); | |
| }); | |
| }, | |
| true | |
| ); | |
| // ------------------------------------------------------------------ notice | |
| var UPSTREAM_URL = "https://github.com/eputnam77/SimpleDocReview"; | |
| /** | |
| * Say what this is. | |
| * | |
| * Upstream's README can leave storage unexplained, because you started the | |
| * server yourself and the files are on your disk. A public URL cannot: a | |
| * visitor's default assumption is a shared service that keeps their records, | |
| * and this deployment keeps nothing. Somebody who ran a real approval round | |
| * here and then cleared their browser would lose it with no warning. | |
| * | |
| * So the page says so twice: a badge that never goes away, and one notice | |
| * explaining where the data lives. Both are injected rather than edited into | |
| * upstream's markup, and both are styled inline so they do not depend on | |
| * upstream's stylesheet. | |
| */ | |
| function addDemoBadge() { | |
| var heading = document.querySelector(".brand h1"); | |
| if (!heading || heading.querySelector(".sdr-demo-badge")) return; | |
| var badge = document.createElement("span"); | |
| badge.className = "sdr-demo-badge"; | |
| badge.textContent = "Demo"; | |
| badge.title = "Runs entirely in your browser. Records are not stored on a server."; | |
| // Upstream's palette tokens, so the badge retints with the app rather than | |
| // sitting on it in a colour of its own. | |
| badge.style.cssText = [ | |
| "margin-left:8px", "vertical-align:middle", "white-space:nowrap", | |
| "padding:2px 6px", "border-radius:5px", | |
| "border:1px solid var(--line, #d9e1e4)", | |
| "color:var(--muted, #64737c)", | |
| "font-size:.6em", "font-weight:700", | |
| "letter-spacing:.08em", "text-transform:uppercase" | |
| ].join(";"); | |
| heading.appendChild(badge); | |
| var dot = document.querySelector(".brand .live-dot"); | |
| if (dot) dot.title = "Demo build - no server"; | |
| } | |
| function showStorageNotice() { | |
| if (window.sessionStorage && window.sessionStorage.getItem("sdrNoticeDismissed")) return; | |
| // Borrowed from upstream's own .toast rule so the notice reads as part of | |
| // the app: same ink, same radius, same shadow, same right margin. It sits | |
| // one toast-height higher so an actual toast still has its usual corner. | |
| var bar = document.createElement("aside"); | |
| bar.setAttribute("role", "note"); | |
| bar.style.cssText = [ | |
| "position:fixed", "right:22px", "bottom:84px", "z-index:60", | |
| "max-width:min(460px, calc(100vw - 44px))", | |
| "display:flex", "gap:12px", "align-items:flex-start", | |
| "padding:12px 14px", "border-radius:8px", | |
| "background:#172126", "color:#fff", | |
| "box-shadow:0 18px 45px rgba(23, 33, 38, 0.28)", | |
| "font-size:13px", "line-height:1.5" | |
| ].join(";"); | |
| var text = document.createElement("p"); | |
| text.style.cssText = "margin:0"; | |
| text.innerHTML = | |
| "<strong>Demo, not a review system.</strong> " + | |
| (activeStore && activeStore.volatile | |
| ? "This browser blocks storage for embedded pages, so anything you add lasts only until you close the tab. " | |
| : "Documents and review records live only in this browser, and clearing site data deletes them. ") + | |
| 'Nothing is sent to a server. For real reviews, <a href="' + UPSTREAM_URL + | |
| '" target="_blank" rel="noopener" style="color:inherit;text-decoration:underline">run it locally</a>.'; | |
| var dismiss = document.createElement("button"); | |
| dismiss.type = "button"; | |
| dismiss.textContent = "Got it"; | |
| dismiss.style.cssText = [ | |
| "flex:0 0 auto", "cursor:pointer", "border-radius:6px", "border:0", | |
| "background:rgba(255,255,255,0.14)", "color:inherit", | |
| "padding:5px 10px", "font-size:inherit", "font-weight:600" | |
| ].join(";"); | |
| dismiss.addEventListener("click", function () { | |
| bar.remove(); | |
| if (window.sessionStorage) window.sessionStorage.setItem("sdrNoticeDismissed", "1"); | |
| }); | |
| bar.appendChild(text); | |
| bar.appendChild(dismiss); | |
| document.body.appendChild(bar); | |
| } | |
| // Wait for the store probe so the notice can tell the truth about persistence. | |
| function whenReady(callback) { | |
| if (document.readyState === "loading") { | |
| document.addEventListener("DOMContentLoaded", callback, { once: true }); | |
| } else { | |
| callback(); | |
| } | |
| } | |
| whenReady(function () { | |
| addDemoBadge(); | |
| resolveStore().then(showStorageNotice, showStorageNotice); | |
| }); | |
| // ------------------------------------------------------------------ console | |
| /** | |
| * Debug surface. `__sdrStaticApi.reset()` clears the workspace, which is the | |
| * only way to start over other than clearing site data in the browser. | |
| */ | |
| window.__sdrStaticApi = { | |
| api: api, | |
| store: store, | |
| reset: function () { | |
| return store.clear().then(function () { | |
| window.location.reload(); | |
| }); | |
| } | |
| }; | |
| })(); | |