eddie-jekyll 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,234 @@
1
+ // SPDX-License-Identifier: GPL-3.0-only
2
+ // Generated by widget/build.sh from widget/src/eddie-boot.js and widget/src/lib/*.js; edit those instead.
3
+ (function () {
4
+ "use strict";
5
+ const EDDIE_ASSET_VERSION = "c0fdc503e01a";
6
+ const EddieLib = {};
7
+ // SPDX-License-Identifier: GPL-3.0-only
8
+
9
+ // Boot loader decisions (widget/src/eddie-boot.js). Pure and tiny: the boot
10
+ // script is on every page view, so it carries only what it needs to draw
11
+ // the trigger button and decide when to fetch the full widget.
12
+
13
+ (function (factory) {
14
+ const api = factory();
15
+ if (typeof module === "object" && module && module.exports) {
16
+ module.exports = api;
17
+ } else if (typeof EddieLib === "object" && EddieLib) {
18
+ Object.assign(EddieLib, api);
19
+ } else {
20
+ globalThis.EddieLib = Object.assign(globalThis.EddieLib || {}, api);
21
+ }
22
+ })(function () {
23
+ "use strict";
24
+
25
+ const POSITIONS = ["top-left", "top-right", "bottom-left", "bottom-right"];
26
+ const SEARCH_CONSENT_KEY = "eddie.search.consent";
27
+ // Set on the first modal open on this browser: the boot loader treats the
28
+ // visitor as returning from then on (see decideBoot / decideWarm).
29
+ const SEARCH_USED_KEY = "eddie.search.used";
30
+
31
+ /** Trigger placement and theme from the `data-*` attributes (same rules as config.js). */
32
+ function bootLayout(get) {
33
+ const pos = String(get("data-position") || "").trim().toLowerCase();
34
+ const theme = String(get("data-theme") || "").trim().toLowerCase();
35
+ const num = (raw) => {
36
+ const v = Number(raw);
37
+ return raw != null && raw !== "" && Number.isFinite(v) ? Math.trunc(v) : 0;
38
+ };
39
+ const warm = String(get("data-warm") || "").trim().toLowerCase();
40
+ return {
41
+ position: POSITIONS.includes(pos) ? pos : "bottom-right",
42
+ theme: ["auto", "light", "dark"].includes(theme) ? theme : "auto",
43
+ offsetX: num(get("data-offset-x")),
44
+ offsetY: num(get("data-offset-y")),
45
+ warm: ["auto", "off", "always"].includes(warm) ? warm : "auto",
46
+ };
47
+ }
48
+
49
+ /**
50
+ * When the boot loader fetches the full widget.
51
+ * warm "auto" | "off" | "always" (data-warm)
52
+ * saveData navigator.connection.saveData
53
+ * reducedData prefers-reduced-data: reduce
54
+ * used the visitor opened the search before on this browser
55
+ * consented a dense lane was accepted before on this browser
56
+ * Returns { action: "idle" | "interaction", reason }: "idle" loads the
57
+ * widget after the page has loaded and the browser is idle (the widget then
58
+ * runs its own warm-up), "interaction" waits for the trigger, the shortcut
59
+ * or a programmatic open.
60
+ */
61
+ function decideBoot(opts) {
62
+ const o = opts || {};
63
+ if (o.warm === "off") return { action: "interaction", reason: "warm is off" };
64
+ if (o.saveData) return { action: "interaction", reason: "data saver" };
65
+ if (o.reducedData) return { action: "interaction", reason: "prefers-reduced-data" };
66
+ if (o.warm === "always") return { action: "idle", reason: "warm is always" };
67
+ if (o.consented) return { action: "idle", reason: "model consented before" };
68
+ if (o.used) return { action: "idle", reason: "search used before" };
69
+ return { action: "interaction", reason: "first visit" };
70
+ }
71
+
72
+ /** `v` query parameter of a URL string, or null. */
73
+ function bootVersionOf(href) {
74
+ const m = /[?&]v=([^&#]+)/.exec(String(href || ""));
75
+ return m ? decodeURIComponent(m[1]) : null;
76
+ }
77
+
78
+ /** Runtime-asset `?v=`, stamped by widget/build.sh; see lib/urls.js. */
79
+ const ASSET_VERSION =
80
+ typeof EDDIE_ASSET_VERSION === "string" && EDDIE_ASSET_VERSION ? EDDIE_ASSET_VERSION : null;
81
+
82
+ /**
83
+ * URL of the full widget next to the boot script, versioned by the asset
84
+ * hash so a content rebuild does not re-download it. An explicit `?v=` on
85
+ * the boot script's own URL wins (a site pinning a build); the index's
86
+ * `?v=` is the last resort, for a bundle without the stamp.
87
+ */
88
+ function widgetScriptUrl(scriptHref, indexUrl) {
89
+ const base = scriptHref.split(/[?#]/)[0];
90
+ const dir = base.substring(0, base.lastIndexOf("/") + 1);
91
+ const v = bootVersionOf(scriptHref) || ASSET_VERSION || bootVersionOf(indexUrl);
92
+ return dir + "eddie-widget.js" + (v ? "?v=" + encodeURIComponent(v) : "");
93
+ }
94
+
95
+ return { SEARCH_CONSENT_KEY, SEARCH_USED_KEY, bootLayout, decideBoot, bootVersionOf, widgetScriptUrl };
96
+ });
97
+
98
+ // SPDX-License-Identifier: GPL-3.0-only
99
+
100
+ // Eddie boot loader: the script a page carries by default. It draws the
101
+ // trigger button (same data-* attributes, placement and theme as the full
102
+ // widget) and installs the Ctrl/Cmd+K shortcut; the full widget
103
+ // (eddie-widget.js, next to this file, same ?v=) is fetched on the first
104
+ // interaction with the trigger, on the shortcut, on `window.eddie.open()`,
105
+ // or after load when lib/boot.js decides the visitor is returning (the full
106
+ // widget then runs its own warm-up). A first-time visitor pays for this
107
+ // file only.
108
+ //
109
+ // widget/build.sh concatenates widget/src/lib/boot.js ahead of this file
110
+ // inside one IIFE (`EddieLib`).
111
+
112
+ "use strict";
113
+
114
+ (function () {
115
+ const scriptEl = document.currentScript;
116
+ if (!scriptEl || document.getElementById("eddie-host") || document.getElementById("eddie-boot")) return;
117
+ const lib = EddieLib;
118
+ const get = (name) => scriptEl.getAttribute(name);
119
+ const layout = lib.bootLayout(get);
120
+ const scriptHref = new URL(scriptEl.src, location.href).href;
121
+ const widgetUrl = lib.widgetScriptUrl(scriptHref, get("data-index-url") || "");
122
+
123
+ const host = document.createElement("div");
124
+ host.id = "eddie-boot";
125
+ host.dataset.theme = layout.theme;
126
+ const shadow = host.attachShadow({ mode: "closed" });
127
+ const style = document.createElement("style");
128
+ style.textContent =
129
+ ":host{all:initial;position:fixed;z-index:999999;--b:#fff;--t:#1a1a1a;--l:#e0e0e0;--a:#2563eb}" +
130
+ ":host([data-theme=dark]){--b:#1a1a1a;--t:#e8e8e8;--l:#333;--a:#60a5fa}" +
131
+ "@media(prefers-color-scheme:dark){:host([data-theme=auto]){--b:#1a1a1a;--t:#e8e8e8;--l:#333;--a:#60a5fa}}" +
132
+ "button{position:fixed;width:48px;height:48px;border-radius:50%;border:1px solid var(--l);background:var(--b);color:var(--t);cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;box-shadow:0 2px 12px rgba(0,0,0,.1);transition:transform .15s ease,box-shadow .15s ease}" +
133
+ "button:hover{transform:scale(1.06);box-shadow:0 4px 16px rgba(0,0,0,.15)}button:active{transform:scale(.96)}" +
134
+ "button:focus-visible{outline:2px solid var(--a);outline-offset:2px}button[aria-busy=true]{opacity:.6}" +
135
+ "svg{width:20px;height:20px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}" +
136
+ "@media(prefers-reduced-motion:reduce){button{transition:none}}";
137
+ shadow.appendChild(style);
138
+
139
+ const trigger = document.createElement("button");
140
+ trigger.type = "button";
141
+ trigger.setAttribute("aria-label", "Search");
142
+ const v = `${24 + layout.offsetY}px`;
143
+ const h = `${24 + layout.offsetX}px`;
144
+ trigger.style[layout.position.startsWith("top") ? "top" : "bottom"] = v;
145
+ trigger.style[layout.position.endsWith("left") ? "left" : "right"] = h;
146
+ trigger.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>';
147
+ shadow.appendChild(trigger);
148
+
149
+ let loading = null; // the injected <script>, once requested
150
+ const handoff = {
151
+ open: false, // the visitor asked to open before the widget arrived
152
+ load,
153
+ dispose() {
154
+ if (host.parentNode) host.parentNode.removeChild(host);
155
+ document.removeEventListener("keydown", onKey);
156
+ if (window.__eddieBoot === handoff) delete window.__eddieBoot;
157
+ },
158
+ };
159
+ window.__eddieBoot = handoff;
160
+
161
+ /** Fetch the full widget once; it reads this script's data-* attributes from its own tag. */
162
+ function load(reason) {
163
+ if (loading) return;
164
+ const s = document.createElement("script");
165
+ for (const attr of Array.from(scriptEl.attributes)) {
166
+ if (attr.name.startsWith("data-")) s.setAttribute(attr.name, attr.value);
167
+ }
168
+ s.setAttribute("data-boot", reason || "");
169
+ s.src = widgetUrl;
170
+ s.async = true;
171
+ s.onerror = () => {
172
+ loading = null; // the next interaction retries
173
+ trigger.removeAttribute("aria-busy");
174
+ };
175
+ loading = s;
176
+ document.head.appendChild(s);
177
+ }
178
+
179
+ function open() {
180
+ handoff.open = true;
181
+ trigger.setAttribute("aria-busy", "true");
182
+ load("open");
183
+ }
184
+
185
+ function isEditable(node) {
186
+ if (!node || node.nodeType !== 1) return false;
187
+ if (node.isContentEditable) return true;
188
+ const tag = node.tagName;
189
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
190
+ }
191
+
192
+ function onKey(e) {
193
+ if (!(e.ctrlKey || e.metaKey) || e.altKey || typeof e.key !== "string" || e.key.toLowerCase() !== "k") return;
194
+ if (isEditable(e.composedPath ? e.composedPath()[0] : e.target)) return;
195
+ e.preventDefault();
196
+ open();
197
+ }
198
+
199
+ trigger.addEventListener("pointerover", () => load("hover"), { once: true });
200
+ trigger.addEventListener("focusin", () => load("focus"), { once: true });
201
+ trigger.addEventListener("click", open);
202
+ document.addEventListener("keydown", onKey);
203
+ // `window.eddie.open()` works before and after the full widget arrives.
204
+ window.eddie = Object.assign(window.eddie || {}, { open });
205
+ document.body.appendChild(host);
206
+
207
+ let saveData = false;
208
+ let reducedData = false;
209
+ try {
210
+ saveData = !!(navigator.connection && navigator.connection.saveData);
211
+ reducedData = !!(window.matchMedia && window.matchMedia("(prefers-reduced-data: reduce)").matches);
212
+ } catch (_) {
213
+ // no connection info: treat as a normal connection
214
+ }
215
+ let used = false;
216
+ let consented = false;
217
+ try {
218
+ used = !!localStorage.getItem(lib.SEARCH_USED_KEY);
219
+ consented = !!localStorage.getItem(lib.SEARCH_CONSENT_KEY);
220
+ } catch (_) {
221
+ // storage unavailable: first-visit rules
222
+ }
223
+ const decision = lib.decideBoot({ warm: layout.warm, saveData, reducedData, used, consented });
224
+ host.dataset.boot = decision.action;
225
+ if (decision.action === "idle") {
226
+ const go = () => {
227
+ if (typeof requestIdleCallback === "function") requestIdleCallback(() => load("idle"), { timeout: 3000 });
228
+ else setTimeout(() => load("idle"), 500);
229
+ };
230
+ if (document.readyState === "complete") go();
231
+ else window.addEventListener("load", go, { once: true });
232
+ }
233
+ })();
234
+ })();