diff --git a/Sources/ContentScopeScripts/dist/pages/duckplayer/index.html b/Sources/ContentScopeScripts/dist/pages/duckplayer/index.html
index 6d7ec2805..89e8174a8 100644
--- a/Sources/ContentScopeScripts/dist/pages/duckplayer/index.html
+++ b/Sources/ContentScopeScripts/dist/pages/duckplayer/index.html
@@ -72,7 +72,7 @@
padding: 16px;
}
-/* pages/duckplayer/app/components/Fallback.module.css */
+/* shared/components/Fallback/Fallback.module.css */
.Fallback_fallback {
height: 100%;
width: 100%;
@@ -2119,7 +2119,7 @@
} catch (e3) {
console.error("could not access handlers for %s, falling back to mock interface", opts.injectName);
}
- const fallback = new TestTransportConfig({
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
/**
* @param {import('@duckduckgo/messaging').NotificationMessage} msg
*/
@@ -3242,12 +3242,12 @@
return q2(UserValuesContext).setEnabled;
}
- // pages/duckplayer/app/components/Fallback.module.css
+ // shared/components/Fallback/Fallback.module.css
var Fallback_default = {
fallback: "Fallback_fallback"
};
- // pages/duckplayer/app/components/Fallback.jsx
+ // shared/components/Fallback/Fallback.jsx
function Fallback({ showDetails }) {
return /* @__PURE__ */ y("div", { class: Fallback_default.fallback }, /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", null, "Something went wrong!"), showDetails && /* @__PURE__ */ y("p", null, "Please check logs for a message called ", /* @__PURE__ */ y("code", null, "reportPageException"))));
}
diff --git a/Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.css b/Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.css
index de2906b8f..f0299a0ba 100644
--- a/Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.css
+++ b/Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.css
@@ -48,7 +48,7 @@ body[data-display=app] {
padding: 16px;
}
-/* pages/duckplayer/app/components/Fallback.module.css */
+/* shared/components/Fallback/Fallback.module.css */
.Fallback_fallback {
height: 100%;
width: 100%;
diff --git a/Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.js b/Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.js
index 9fc0f424b..7a37e4e73 100644
--- a/Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.js
+++ b/Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.js
@@ -1135,7 +1135,7 @@
} catch (e3) {
console.error("could not access handlers for %s, falling back to mock interface", opts.injectName);
}
- const fallback = new TestTransportConfig({
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
/**
* @param {import('@duckduckgo/messaging').NotificationMessage} msg
*/
@@ -2258,12 +2258,12 @@
return q2(UserValuesContext).setEnabled;
}
- // pages/duckplayer/app/components/Fallback.module.css
+ // shared/components/Fallback/Fallback.module.css
var Fallback_default = {
fallback: "Fallback_fallback"
};
- // pages/duckplayer/app/components/Fallback.jsx
+ // shared/components/Fallback/Fallback.jsx
function Fallback({ showDetails }) {
return /* @__PURE__ */ y("div", { class: Fallback_default.fallback }, /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", null, "Something went wrong!"), showDetails && /* @__PURE__ */ y("p", null, "Please check logs for a message called ", /* @__PURE__ */ y("code", null, "reportPageException"))));
}
diff --git a/Sources/ContentScopeScripts/dist/pages/onboarding/js/index.js b/Sources/ContentScopeScripts/dist/pages/onboarding/js/index.js
index d77743314..998401264 100644
--- a/Sources/ContentScopeScripts/dist/pages/onboarding/js/index.js
+++ b/Sources/ContentScopeScripts/dist/pages/onboarding/js/index.js
@@ -8440,7 +8440,7 @@
} catch (e3) {
console.error("could not access handlers for %s, falling back to mock interface", opts.injectName);
}
- const fallback = new TestTransportConfig({
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
/**
* @param {import('@duckduckgo/messaging').NotificationMessage} msg
*/
diff --git a/Sources/ContentScopeScripts/dist/pages/release-notes/js/index.js b/Sources/ContentScopeScripts/dist/pages/release-notes/js/index.js
index 29a845dc1..51ee5b01b 100644
--- a/Sources/ContentScopeScripts/dist/pages/release-notes/js/index.js
+++ b/Sources/ContentScopeScripts/dist/pages/release-notes/js/index.js
@@ -2051,7 +2051,7 @@
} catch (e3) {
console.error("could not access handlers for %s, falling back to mock interface", opts.injectName);
}
- const fallback = new TestTransportConfig({
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
/**
* @param {import('@duckduckgo/messaging').NotificationMessage} msg
*/
diff --git a/Sources/ContentScopeScripts/dist/pages/special-error/index.html b/Sources/ContentScopeScripts/dist/pages/special-error/index.html
index 19d2017fa..3777132f9 100644
--- a/Sources/ContentScopeScripts/dist/pages/special-error/index.html
+++ b/Sources/ContentScopeScripts/dist/pages/special-error/index.html
@@ -1917,7 +1917,7 @@
} catch (e3) {
console.error("could not access handlers for %s, falling back to mock interface", opts.injectName);
}
- const fallback = new TestTransportConfig({
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
/**
* @param {import('@duckduckgo/messaging').NotificationMessage} msg
*/
diff --git a/Sources/ContentScopeScripts/dist/pages/special-error/js/index.js b/Sources/ContentScopeScripts/dist/pages/special-error/js/index.js
index cb6005c4b..76e9c9512 100644
--- a/Sources/ContentScopeScripts/dist/pages/special-error/js/index.js
+++ b/Sources/ContentScopeScripts/dist/pages/special-error/js/index.js
@@ -1135,7 +1135,7 @@
} catch (e3) {
console.error("could not access handlers for %s, falling back to mock interface", opts.injectName);
}
- const fallback = new TestTransportConfig({
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
/**
* @param {import('@duckduckgo/messaging').NotificationMessage} msg
*/
diff --git a/build/android/pages/duckplayer/js/index.css b/build/android/pages/duckplayer/js/index.css
index de2906b8f..f0299a0ba 100644
--- a/build/android/pages/duckplayer/js/index.css
+++ b/build/android/pages/duckplayer/js/index.css
@@ -48,7 +48,7 @@ body[data-display=app] {
padding: 16px;
}
-/* pages/duckplayer/app/components/Fallback.module.css */
+/* shared/components/Fallback/Fallback.module.css */
.Fallback_fallback {
height: 100%;
width: 100%;
diff --git a/build/android/pages/duckplayer/js/index.js b/build/android/pages/duckplayer/js/index.js
index 59dac3b27..7345cb0b5 100644
--- a/build/android/pages/duckplayer/js/index.js
+++ b/build/android/pages/duckplayer/js/index.js
@@ -1135,7 +1135,7 @@
} catch (e3) {
console.error("could not access handlers for %s, falling back to mock interface", opts.injectName);
}
- const fallback = new TestTransportConfig({
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
/**
* @param {import('@duckduckgo/messaging').NotificationMessage} msg
*/
@@ -2258,12 +2258,12 @@
return q2(UserValuesContext).setEnabled;
}
- // pages/duckplayer/app/components/Fallback.module.css
+ // shared/components/Fallback/Fallback.module.css
var Fallback_default = {
fallback: "Fallback_fallback"
};
- // pages/duckplayer/app/components/Fallback.jsx
+ // shared/components/Fallback/Fallback.jsx
function Fallback({ showDetails }) {
return /* @__PURE__ */ y("div", { class: Fallback_default.fallback }, /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", null, "Something went wrong!"), showDetails && /* @__PURE__ */ y("p", null, "Please check logs for a message called ", /* @__PURE__ */ y("code", null, "reportPageException"))));
}
diff --git a/build/integration/pages/duckplayer/js/index.css b/build/integration/pages/duckplayer/js/index.css
index de2906b8f..f0299a0ba 100644
--- a/build/integration/pages/duckplayer/js/index.css
+++ b/build/integration/pages/duckplayer/js/index.css
@@ -48,7 +48,7 @@ body[data-display=app] {
padding: 16px;
}
-/* pages/duckplayer/app/components/Fallback.module.css */
+/* shared/components/Fallback/Fallback.module.css */
.Fallback_fallback {
height: 100%;
width: 100%;
diff --git a/build/integration/pages/duckplayer/js/index.js b/build/integration/pages/duckplayer/js/index.js
index 1e9759385..8d1fc6632 100644
--- a/build/integration/pages/duckplayer/js/index.js
+++ b/build/integration/pages/duckplayer/js/index.js
@@ -1135,7 +1135,7 @@
} catch (e3) {
console.error("could not access handlers for %s, falling back to mock interface", opts.injectName);
}
- const fallback = new TestTransportConfig({
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
/**
* @param {import('@duckduckgo/messaging').NotificationMessage} msg
*/
@@ -2258,12 +2258,12 @@
return q2(UserValuesContext).setEnabled;
}
- // pages/duckplayer/app/components/Fallback.module.css
+ // shared/components/Fallback/Fallback.module.css
var Fallback_default = {
fallback: "Fallback_fallback"
};
- // pages/duckplayer/app/components/Fallback.jsx
+ // shared/components/Fallback/Fallback.jsx
function Fallback({ showDetails }) {
return /* @__PURE__ */ y("div", { class: Fallback_default.fallback }, /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", null, "Something went wrong!"), showDetails && /* @__PURE__ */ y("p", null, "Please check logs for a message called ", /* @__PURE__ */ y("code", null, "reportPageException"))));
}
diff --git a/build/integration/pages/example/js/index.js b/build/integration/pages/example/js/index.js
index 4b01e58cc..cc27e8d00 100644
--- a/build/integration/pages/example/js/index.js
+++ b/build/integration/pages/example/js/index.js
@@ -1058,7 +1058,7 @@
} catch (e3) {
console.error("could not access handlers for %s, falling back to mock interface", opts.injectName);
}
- const fallback = new TestTransportConfig({
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
/**
* @param {import('@duckduckgo/messaging').NotificationMessage} msg
*/
diff --git a/build/integration/pages/new-tab/index.html b/build/integration/pages/new-tab/index.html
new file mode 100644
index 000000000..6d0102db4
--- /dev/null
+++ b/build/integration/pages/new-tab/index.html
@@ -0,0 +1,14 @@
+
+
+
+ New Tab Page
+
+
+
+
+
+
+
+
+
+
diff --git a/build/integration/pages/new-tab/js/index.css b/build/integration/pages/new-tab/js/index.css
new file mode 100644
index 000000000..55225c2b1
--- /dev/null
+++ b/build/integration/pages/new-tab/js/index.css
@@ -0,0 +1,58 @@
+/* pages/new-tab/app/styles/base.css */
+*,
+*:after,
+*:before {
+ box-sizing: border-box;
+}
+html[data-reduced-motion=true] * {
+ animation: none !important;
+ transition: none !important;
+}
+body {
+ font-family: system-ui;
+ margin: 0;
+ height: 100vh;
+ width: 100%;
+ overflow-x: hidden;
+ user-select: none;
+ -webkit-user-select: none;
+ cursor: default;
+ color: var(--theme-txt-color);
+ background: var(--theme-page-bg);
+}
+body > main {
+ width: 100%;
+}
+h1,
+h2,
+h3,
+h4 {
+ margin: 0;
+}
+button {
+ font-family: system-ui, sans-serif;
+}
+ul {
+ margin: 0;
+ padding: 0;
+}
+li {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+/* pages/new-tab/app/components/App.module.css */
+.App_layout {
+ padding-top: var(--sp-16);
+ padding-bottom: var(--sp-16);
+ max-width: 504px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* shared/components/Fallback/Fallback.module.css */
+.Fallback_fallback {
+ height: 100%;
+ width: 100%;
+}
diff --git a/build/integration/pages/new-tab/js/index.js b/build/integration/pages/new-tab/js/index.js
new file mode 100644
index 000000000..6d4317cf0
--- /dev/null
+++ b/build/integration/pages/new-tab/js/index.js
@@ -0,0 +1,2173 @@
+"use strict";
+(() => {
+ // ../../node_modules/preact/dist/preact.module.js
+ var n;
+ var l;
+ var u;
+ var t;
+ var i;
+ var o;
+ var r;
+ var f;
+ var e;
+ var c = {};
+ var s = [];
+ var a = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;
+ var h = Array.isArray;
+ function v(n2, l3) {
+ for (var u3 in l3)
+ n2[u3] = l3[u3];
+ return n2;
+ }
+ function p(n2) {
+ var l3 = n2.parentNode;
+ l3 && l3.removeChild(n2);
+ }
+ function y(l3, u3, t3) {
+ var i3, o3, r3, f3 = {};
+ for (r3 in u3)
+ "key" == r3 ? i3 = u3[r3] : "ref" == r3 ? o3 = u3[r3] : f3[r3] = u3[r3];
+ if (arguments.length > 2 && (f3.children = arguments.length > 3 ? n.call(arguments, 2) : t3), "function" == typeof l3 && null != l3.defaultProps)
+ for (r3 in l3.defaultProps)
+ void 0 === f3[r3] && (f3[r3] = l3.defaultProps[r3]);
+ return d(l3, f3, i3, o3, null);
+ }
+ function d(n2, t3, i3, o3, r3) {
+ var f3 = { type: n2, props: t3, key: i3, ref: o3, __k: null, __: null, __b: 0, __e: null, __d: void 0, __c: null, constructor: void 0, __v: null == r3 ? ++u : r3, __i: -1, __u: 0 };
+ return null == r3 && null != l.vnode && l.vnode(f3), f3;
+ }
+ function g(n2) {
+ return n2.children;
+ }
+ function b(n2, l3) {
+ this.props = n2, this.context = l3;
+ }
+ function m(n2, l3) {
+ if (null == l3)
+ return n2.__ ? m(n2.__, n2.__i + 1) : null;
+ for (var u3; l3 < n2.__k.length; l3++)
+ if (null != (u3 = n2.__k[l3]) && null != u3.__e)
+ return u3.__e;
+ return "function" == typeof n2.type ? m(n2) : null;
+ }
+ function k(n2) {
+ var l3, u3;
+ if (null != (n2 = n2.__) && null != n2.__c) {
+ for (n2.__e = n2.__c.base = null, l3 = 0; l3 < n2.__k.length; l3++)
+ if (null != (u3 = n2.__k[l3]) && null != u3.__e) {
+ n2.__e = n2.__c.base = u3.__e;
+ break;
+ }
+ return k(n2);
+ }
+ }
+ function w(n2) {
+ (!n2.__d && (n2.__d = true) && i.push(n2) && !x.__r++ || o !== l.debounceRendering) && ((o = l.debounceRendering) || r)(x);
+ }
+ function x() {
+ var n2, u3, t3, o3, r3, e3, c3, s3, a3;
+ for (i.sort(f); n2 = i.shift(); )
+ n2.__d && (u3 = i.length, o3 = void 0, e3 = (r3 = (t3 = n2).__v).__e, s3 = [], a3 = [], (c3 = t3.__P) && ((o3 = v({}, r3)).__v = r3.__v + 1, l.vnode && l.vnode(o3), L(c3, o3, r3, t3.__n, void 0 !== c3.ownerSVGElement, 32 & r3.__u ? [e3] : null, s3, null == e3 ? m(r3) : e3, !!(32 & r3.__u), a3), o3.__.__k[o3.__i] = o3, M(s3, o3, a3), o3.__e != e3 && k(o3)), i.length > u3 && i.sort(f));
+ x.__r = 0;
+ }
+ function C(n2, l3, u3, t3, i3, o3, r3, f3, e3, a3, h3) {
+ var v3, p3, y2, d3, _2, g3 = t3 && t3.__k || s, b3 = l3.length;
+ for (u3.__d = e3, P(u3, l3, g3), e3 = u3.__d, v3 = 0; v3 < b3; v3++)
+ null != (y2 = u3.__k[v3]) && "boolean" != typeof y2 && "function" != typeof y2 && (p3 = -1 === y2.__i ? c : g3[y2.__i] || c, y2.__i = v3, L(n2, y2, p3, i3, o3, r3, f3, e3, a3, h3), d3 = y2.__e, y2.ref && p3.ref != y2.ref && (p3.ref && z(p3.ref, null, y2), h3.push(y2.ref, y2.__c || d3, y2)), null == _2 && null != d3 && (_2 = d3), 65536 & y2.__u || p3.__k === y2.__k ? e3 = S(y2, e3, n2) : "function" == typeof y2.type && void 0 !== y2.__d ? e3 = y2.__d : d3 && (e3 = d3.nextSibling), y2.__d = void 0, y2.__u &= -196609);
+ u3.__d = e3, u3.__e = _2;
+ }
+ function P(n2, l3, u3) {
+ var t3, i3, o3, r3, f3, e3 = l3.length, c3 = u3.length, s3 = c3, a3 = 0;
+ for (n2.__k = [], t3 = 0; t3 < e3; t3++)
+ null != (i3 = n2.__k[t3] = null == (i3 = l3[t3]) || "boolean" == typeof i3 || "function" == typeof i3 ? null : "string" == typeof i3 || "number" == typeof i3 || "bigint" == typeof i3 || i3.constructor == String ? d(null, i3, null, null, i3) : h(i3) ? d(g, { children: i3 }, null, null, null) : void 0 === i3.constructor && i3.__b > 0 ? d(i3.type, i3.props, i3.key, i3.ref ? i3.ref : null, i3.__v) : i3) ? (i3.__ = n2, i3.__b = n2.__b + 1, f3 = H(i3, u3, r3 = t3 + a3, s3), i3.__i = f3, o3 = null, -1 !== f3 && (s3--, (o3 = u3[f3]) && (o3.__u |= 131072)), null == o3 || null === o3.__v ? (-1 == f3 && a3--, "function" != typeof i3.type && (i3.__u |= 65536)) : f3 !== r3 && (f3 === r3 + 1 ? a3++ : f3 > r3 ? s3 > e3 - r3 ? a3 += f3 - r3 : a3-- : a3 = f3 < r3 && f3 == r3 - 1 ? f3 - r3 : 0, f3 !== t3 + a3 && (i3.__u |= 65536))) : (o3 = u3[t3]) && null == o3.key && o3.__e && (o3.__e == n2.__d && (n2.__d = m(o3)), N(o3, o3, false), u3[t3] = null, s3--);
+ if (s3)
+ for (t3 = 0; t3 < c3; t3++)
+ null != (o3 = u3[t3]) && 0 == (131072 & o3.__u) && (o3.__e == n2.__d && (n2.__d = m(o3)), N(o3, o3));
+ }
+ function S(n2, l3, u3) {
+ var t3, i3;
+ if ("function" == typeof n2.type) {
+ for (t3 = n2.__k, i3 = 0; t3 && i3 < t3.length; i3++)
+ t3[i3] && (t3[i3].__ = n2, l3 = S(t3[i3], l3, u3));
+ return l3;
+ }
+ return n2.__e != l3 && (u3.insertBefore(n2.__e, l3 || null), l3 = n2.__e), l3 && l3.nextSibling;
+ }
+ function H(n2, l3, u3, t3) {
+ var i3 = n2.key, o3 = n2.type, r3 = u3 - 1, f3 = u3 + 1, e3 = l3[u3];
+ if (null === e3 || e3 && i3 == e3.key && o3 === e3.type)
+ return u3;
+ if (t3 > (null != e3 && 0 == (131072 & e3.__u) ? 1 : 0))
+ for (; r3 >= 0 || f3 < l3.length; ) {
+ if (r3 >= 0) {
+ if ((e3 = l3[r3]) && 0 == (131072 & e3.__u) && i3 == e3.key && o3 === e3.type)
+ return r3;
+ r3--;
+ }
+ if (f3 < l3.length) {
+ if ((e3 = l3[f3]) && 0 == (131072 & e3.__u) && i3 == e3.key && o3 === e3.type)
+ return f3;
+ f3++;
+ }
+ }
+ return -1;
+ }
+ function I(n2, l3, u3) {
+ "-" === l3[0] ? n2.setProperty(l3, null == u3 ? "" : u3) : n2[l3] = null == u3 ? "" : "number" != typeof u3 || a.test(l3) ? u3 : u3 + "px";
+ }
+ function T(n2, l3, u3, t3, i3) {
+ var o3;
+ n:
+ if ("style" === l3)
+ if ("string" == typeof u3)
+ n2.style.cssText = u3;
+ else {
+ if ("string" == typeof t3 && (n2.style.cssText = t3 = ""), t3)
+ for (l3 in t3)
+ u3 && l3 in u3 || I(n2.style, l3, "");
+ if (u3)
+ for (l3 in u3)
+ t3 && u3[l3] === t3[l3] || I(n2.style, l3, u3[l3]);
+ }
+ else if ("o" === l3[0] && "n" === l3[1])
+ o3 = l3 !== (l3 = l3.replace(/(PointerCapture)$|Capture$/, "$1")), l3 = l3.toLowerCase() in n2 ? l3.toLowerCase().slice(2) : l3.slice(2), n2.l || (n2.l = {}), n2.l[l3 + o3] = u3, u3 ? t3 ? u3.u = t3.u : (u3.u = Date.now(), n2.addEventListener(l3, o3 ? D : A, o3)) : n2.removeEventListener(l3, o3 ? D : A, o3);
+ else {
+ if (i3)
+ l3 = l3.replace(/xlink(H|:h)/, "h").replace(/sName$/, "s");
+ else if ("width" !== l3 && "height" !== l3 && "href" !== l3 && "list" !== l3 && "form" !== l3 && "tabIndex" !== l3 && "download" !== l3 && "rowSpan" !== l3 && "colSpan" !== l3 && "role" !== l3 && l3 in n2)
+ try {
+ n2[l3] = null == u3 ? "" : u3;
+ break n;
+ } catch (n3) {
+ }
+ "function" == typeof u3 || (null == u3 || false === u3 && "-" !== l3[4] ? n2.removeAttribute(l3) : n2.setAttribute(l3, u3));
+ }
+ }
+ function A(n2) {
+ var u3 = this.l[n2.type + false];
+ if (n2.t) {
+ if (n2.t <= u3.u)
+ return;
+ } else
+ n2.t = Date.now();
+ return u3(l.event ? l.event(n2) : n2);
+ }
+ function D(n2) {
+ return this.l[n2.type + true](l.event ? l.event(n2) : n2);
+ }
+ function L(n2, u3, t3, i3, o3, r3, f3, e3, c3, s3) {
+ var a3, p3, y2, d3, _2, m3, k3, w3, x2, P2, S2, $, H2, I2, T3, A2 = u3.type;
+ if (void 0 !== u3.constructor)
+ return null;
+ 128 & t3.__u && (c3 = !!(32 & t3.__u), r3 = [e3 = u3.__e = t3.__e]), (a3 = l.__b) && a3(u3);
+ n:
+ if ("function" == typeof A2)
+ try {
+ if (w3 = u3.props, x2 = (a3 = A2.contextType) && i3[a3.__c], P2 = a3 ? x2 ? x2.props.value : a3.__ : i3, t3.__c ? k3 = (p3 = u3.__c = t3.__c).__ = p3.__E : ("prototype" in A2 && A2.prototype.render ? u3.__c = p3 = new A2(w3, P2) : (u3.__c = p3 = new b(w3, P2), p3.constructor = A2, p3.render = O), x2 && x2.sub(p3), p3.props = w3, p3.state || (p3.state = {}), p3.context = P2, p3.__n = i3, y2 = p3.__d = true, p3.__h = [], p3._sb = []), null == p3.__s && (p3.__s = p3.state), null != A2.getDerivedStateFromProps && (p3.__s == p3.state && (p3.__s = v({}, p3.__s)), v(p3.__s, A2.getDerivedStateFromProps(w3, p3.__s))), d3 = p3.props, _2 = p3.state, p3.__v = u3, y2)
+ null == A2.getDerivedStateFromProps && null != p3.componentWillMount && p3.componentWillMount(), null != p3.componentDidMount && p3.__h.push(p3.componentDidMount);
+ else {
+ if (null == A2.getDerivedStateFromProps && w3 !== d3 && null != p3.componentWillReceiveProps && p3.componentWillReceiveProps(w3, P2), !p3.__e && (null != p3.shouldComponentUpdate && false === p3.shouldComponentUpdate(w3, p3.__s, P2) || u3.__v === t3.__v)) {
+ for (u3.__v !== t3.__v && (p3.props = w3, p3.state = p3.__s, p3.__d = false), u3.__e = t3.__e, u3.__k = t3.__k, u3.__k.forEach(function(n3) {
+ n3 && (n3.__ = u3);
+ }), S2 = 0; S2 < p3._sb.length; S2++)
+ p3.__h.push(p3._sb[S2]);
+ p3._sb = [], p3.__h.length && f3.push(p3);
+ break n;
+ }
+ null != p3.componentWillUpdate && p3.componentWillUpdate(w3, p3.__s, P2), null != p3.componentDidUpdate && p3.__h.push(function() {
+ p3.componentDidUpdate(d3, _2, m3);
+ });
+ }
+ if (p3.context = P2, p3.props = w3, p3.__P = n2, p3.__e = false, $ = l.__r, H2 = 0, "prototype" in A2 && A2.prototype.render) {
+ for (p3.state = p3.__s, p3.__d = false, $ && $(u3), a3 = p3.render(p3.props, p3.state, p3.context), I2 = 0; I2 < p3._sb.length; I2++)
+ p3.__h.push(p3._sb[I2]);
+ p3._sb = [];
+ } else
+ do {
+ p3.__d = false, $ && $(u3), a3 = p3.render(p3.props, p3.state, p3.context), p3.state = p3.__s;
+ } while (p3.__d && ++H2 < 25);
+ p3.state = p3.__s, null != p3.getChildContext && (i3 = v(v({}, i3), p3.getChildContext())), y2 || null == p3.getSnapshotBeforeUpdate || (m3 = p3.getSnapshotBeforeUpdate(d3, _2)), C(n2, h(T3 = null != a3 && a3.type === g && null == a3.key ? a3.props.children : a3) ? T3 : [T3], u3, t3, i3, o3, r3, f3, e3, c3, s3), p3.base = u3.__e, u3.__u &= -161, p3.__h.length && f3.push(p3), k3 && (p3.__E = p3.__ = null);
+ } catch (n3) {
+ u3.__v = null, c3 || null != r3 ? (u3.__e = e3, u3.__u |= c3 ? 160 : 32, r3[r3.indexOf(e3)] = null) : (u3.__e = t3.__e, u3.__k = t3.__k), l.__e(n3, u3, t3);
+ }
+ else
+ null == r3 && u3.__v === t3.__v ? (u3.__k = t3.__k, u3.__e = t3.__e) : u3.__e = j(t3.__e, u3, t3, i3, o3, r3, f3, c3, s3);
+ (a3 = l.diffed) && a3(u3);
+ }
+ function M(n2, u3, t3) {
+ u3.__d = void 0;
+ for (var i3 = 0; i3 < t3.length; i3++)
+ z(t3[i3], t3[++i3], t3[++i3]);
+ l.__c && l.__c(u3, n2), n2.some(function(u4) {
+ try {
+ n2 = u4.__h, u4.__h = [], n2.some(function(n3) {
+ n3.call(u4);
+ });
+ } catch (n3) {
+ l.__e(n3, u4.__v);
+ }
+ });
+ }
+ function j(l3, u3, t3, i3, o3, r3, f3, e3, s3) {
+ var a3, v3, y2, d3, _2, g3, b3, k3 = t3.props, w3 = u3.props, x2 = u3.type;
+ if ("svg" === x2 && (o3 = true), null != r3) {
+ for (a3 = 0; a3 < r3.length; a3++)
+ if ((_2 = r3[a3]) && "setAttribute" in _2 == !!x2 && (x2 ? _2.localName === x2 : 3 === _2.nodeType)) {
+ l3 = _2, r3[a3] = null;
+ break;
+ }
+ }
+ if (null == l3) {
+ if (null === x2)
+ return document.createTextNode(w3);
+ l3 = o3 ? document.createElementNS("http://www.w3.org/2000/svg", x2) : document.createElement(x2, w3.is && w3), r3 = null, e3 = false;
+ }
+ if (null === x2)
+ k3 === w3 || e3 && l3.data === w3 || (l3.data = w3);
+ else {
+ if (r3 = r3 && n.call(l3.childNodes), k3 = t3.props || c, !e3 && null != r3)
+ for (k3 = {}, a3 = 0; a3 < l3.attributes.length; a3++)
+ k3[(_2 = l3.attributes[a3]).name] = _2.value;
+ for (a3 in k3)
+ _2 = k3[a3], "children" == a3 || ("dangerouslySetInnerHTML" == a3 ? y2 = _2 : "key" === a3 || a3 in w3 || T(l3, a3, null, _2, o3));
+ for (a3 in w3)
+ _2 = w3[a3], "children" == a3 ? d3 = _2 : "dangerouslySetInnerHTML" == a3 ? v3 = _2 : "value" == a3 ? g3 = _2 : "checked" == a3 ? b3 = _2 : "key" === a3 || e3 && "function" != typeof _2 || k3[a3] === _2 || T(l3, a3, _2, k3[a3], o3);
+ if (v3)
+ e3 || y2 && (v3.__html === y2.__html || v3.__html === l3.innerHTML) || (l3.innerHTML = v3.__html), u3.__k = [];
+ else if (y2 && (l3.innerHTML = ""), C(l3, h(d3) ? d3 : [d3], u3, t3, i3, o3 && "foreignObject" !== x2, r3, f3, r3 ? r3[0] : t3.__k && m(t3, 0), e3, s3), null != r3)
+ for (a3 = r3.length; a3--; )
+ null != r3[a3] && p(r3[a3]);
+ e3 || (a3 = "value", void 0 !== g3 && (g3 !== l3[a3] || "progress" === x2 && !g3 || "option" === x2 && g3 !== k3[a3]) && T(l3, a3, g3, k3[a3], false), a3 = "checked", void 0 !== b3 && b3 !== l3[a3] && T(l3, a3, b3, k3[a3], false));
+ }
+ return l3;
+ }
+ function z(n2, u3, t3) {
+ try {
+ "function" == typeof n2 ? n2(u3) : n2.current = u3;
+ } catch (n3) {
+ l.__e(n3, t3);
+ }
+ }
+ function N(n2, u3, t3) {
+ var i3, o3;
+ if (l.unmount && l.unmount(n2), (i3 = n2.ref) && (i3.current && i3.current !== n2.__e || z(i3, null, u3)), null != (i3 = n2.__c)) {
+ if (i3.componentWillUnmount)
+ try {
+ i3.componentWillUnmount();
+ } catch (n3) {
+ l.__e(n3, u3);
+ }
+ i3.base = i3.__P = null, n2.__c = void 0;
+ }
+ if (i3 = n2.__k)
+ for (o3 = 0; o3 < i3.length; o3++)
+ i3[o3] && N(i3[o3], u3, t3 || "function" != typeof n2.type);
+ t3 || null == n2.__e || p(n2.__e), n2.__ = n2.__e = n2.__d = void 0;
+ }
+ function O(n2, l3, u3) {
+ return this.constructor(n2, u3);
+ }
+ function q(u3, t3, i3) {
+ var o3, r3, f3, e3;
+ l.__ && l.__(u3, t3), r3 = (o3 = "function" == typeof i3) ? null : i3 && i3.__k || t3.__k, f3 = [], e3 = [], L(t3, u3 = (!o3 && i3 || t3).__k = y(g, null, [u3]), r3 || c, c, void 0 !== t3.ownerSVGElement, !o3 && i3 ? [i3] : r3 ? null : t3.firstChild ? n.call(t3.childNodes) : null, f3, !o3 && i3 ? i3 : r3 ? r3.__e : t3.firstChild, o3, e3), M(f3, u3, e3);
+ }
+ function F(n2, l3) {
+ var u3 = { __c: l3 = "__cC" + e++, __: n2, Consumer: function(n3, l4) {
+ return n3.children(l4);
+ }, Provider: function(n3) {
+ var u4, t3;
+ return this.getChildContext || (u4 = [], (t3 = {})[l3] = this, this.getChildContext = function() {
+ return t3;
+ }, this.shouldComponentUpdate = function(n4) {
+ this.props.value !== n4.value && u4.some(function(n5) {
+ n5.__e = true, w(n5);
+ });
+ }, this.sub = function(n4) {
+ u4.push(n4);
+ var l4 = n4.componentWillUnmount;
+ n4.componentWillUnmount = function() {
+ u4.splice(u4.indexOf(n4), 1), l4 && l4.call(n4);
+ };
+ }), n3.children;
+ } };
+ return u3.Provider.__ = u3.Consumer.contextType = u3;
+ }
+ n = s.slice, l = { __e: function(n2, l3, u3, t3) {
+ for (var i3, o3, r3; l3 = l3.__; )
+ if ((i3 = l3.__c) && !i3.__)
+ try {
+ if ((o3 = i3.constructor) && null != o3.getDerivedStateFromError && (i3.setState(o3.getDerivedStateFromError(n2)), r3 = i3.__d), null != i3.componentDidCatch && (i3.componentDidCatch(n2, t3 || {}), r3 = i3.__d), r3)
+ return i3.__E = i3;
+ } catch (l4) {
+ n2 = l4;
+ }
+ throw n2;
+ } }, u = 0, t = function(n2) {
+ return null != n2 && null == n2.constructor;
+ }, b.prototype.setState = function(n2, l3) {
+ var u3;
+ u3 = null != this.__s && this.__s !== this.state ? this.__s : this.__s = v({}, this.state), "function" == typeof n2 && (n2 = n2(v({}, u3), this.props)), n2 && v(u3, n2), null != n2 && this.__v && (l3 && this._sb.push(l3), w(this));
+ }, b.prototype.forceUpdate = function(n2) {
+ this.__v && (this.__e = true, n2 && this.__h.push(n2), w(this));
+ }, b.prototype.render = g, i = [], r = "function" == typeof Promise ? Promise.prototype.then.bind(Promise.resolve()) : setTimeout, f = function(n2, l3) {
+ return n2.__v.__b - l3.__v.__b;
+ }, x.__r = 0, e = 0;
+
+ // ../../node_modules/preact/devtools/dist/devtools.module.js
+ "undefined" != typeof window && window.__PREACT_DEVTOOLS__ && window.__PREACT_DEVTOOLS__.attachPreact("10.19.3", l, { Fragment: g, Component: b });
+
+ // pages/new-tab/app/components/App.module.css
+ var App_default = {
+ layout: "App_layout"
+ };
+
+ // pages/new-tab/app/components/App.js
+ function App({ children }) {
+ return /* @__PURE__ */ y("div", { className: App_default.layout }, children);
+ }
+
+ // ../../node_modules/preact/hooks/dist/hooks.module.js
+ var t2;
+ var r2;
+ var u2;
+ var i2;
+ var o2 = 0;
+ var f2 = [];
+ var c2 = [];
+ var e2 = l.__b;
+ var a2 = l.__r;
+ var v2 = l.diffed;
+ var l2 = l.__c;
+ var m2 = l.unmount;
+ function d2(t3, u3) {
+ l.__h && l.__h(r2, t3, o2 || u3), o2 = 0;
+ var i3 = r2.__H || (r2.__H = { __: [], __h: [] });
+ return t3 >= i3.__.length && i3.__.push({ __V: c2 }), i3.__[t3];
+ }
+ function h2(n2) {
+ return o2 = 1, s2(B, n2);
+ }
+ function s2(n2, u3, i3) {
+ var o3 = d2(t2++, 2);
+ if (o3.t = n2, !o3.__c && (o3.__ = [i3 ? i3(u3) : B(void 0, u3), function(n3) {
+ var t3 = o3.__N ? o3.__N[0] : o3.__[0], r3 = o3.t(t3, n3);
+ t3 !== r3 && (o3.__N = [r3, o3.__[1]], o3.__c.setState({}));
+ }], o3.__c = r2, !r2.u)) {
+ var f3 = function(n3, t3, r3) {
+ if (!o3.__c.__H)
+ return true;
+ var u4 = o3.__c.__H.__.filter(function(n4) {
+ return n4.__c;
+ });
+ if (u4.every(function(n4) {
+ return !n4.__N;
+ }))
+ return !c3 || c3.call(this, n3, t3, r3);
+ var i4 = false;
+ return u4.forEach(function(n4) {
+ if (n4.__N) {
+ var t4 = n4.__[0];
+ n4.__ = n4.__N, n4.__N = void 0, t4 !== n4.__[0] && (i4 = true);
+ }
+ }), !(!i4 && o3.__c.props === n3) && (!c3 || c3.call(this, n3, t3, r3));
+ };
+ r2.u = true;
+ var c3 = r2.shouldComponentUpdate, e3 = r2.componentWillUpdate;
+ r2.componentWillUpdate = function(n3, t3, r3) {
+ if (this.__e) {
+ var u4 = c3;
+ c3 = void 0, f3(n3, t3, r3), c3 = u4;
+ }
+ e3 && e3.call(this, n3, t3, r3);
+ }, r2.shouldComponentUpdate = f3;
+ }
+ return o3.__N || o3.__;
+ }
+ function p2(u3, i3) {
+ var o3 = d2(t2++, 3);
+ !l.__s && z2(o3.__H, i3) && (o3.__ = u3, o3.i = i3, r2.__H.__h.push(o3));
+ }
+ function F2(n2, r3) {
+ var u3 = d2(t2++, 7);
+ return z2(u3.__H, r3) ? (u3.__V = n2(), u3.i = r3, u3.__h = n2, u3.__V) : u3.__;
+ }
+ function T2(n2, t3) {
+ return o2 = 8, F2(function() {
+ return n2;
+ }, t3);
+ }
+ function q2(n2) {
+ var u3 = r2.context[n2.__c], i3 = d2(t2++, 9);
+ return i3.c = n2, u3 ? (null == i3.__ && (i3.__ = true, u3.sub(r2)), u3.props.value) : n2.__;
+ }
+ function b2() {
+ for (var t3; t3 = f2.shift(); )
+ if (t3.__P && t3.__H)
+ try {
+ t3.__H.__h.forEach(k2), t3.__H.__h.forEach(w2), t3.__H.__h = [];
+ } catch (r3) {
+ t3.__H.__h = [], l.__e(r3, t3.__v);
+ }
+ }
+ l.__b = function(n2) {
+ r2 = null, e2 && e2(n2);
+ }, l.__r = function(n2) {
+ a2 && a2(n2), t2 = 0;
+ var i3 = (r2 = n2.__c).__H;
+ i3 && (u2 === r2 ? (i3.__h = [], r2.__h = [], i3.__.forEach(function(n3) {
+ n3.__N && (n3.__ = n3.__N), n3.__V = c2, n3.__N = n3.i = void 0;
+ })) : (i3.__h.forEach(k2), i3.__h.forEach(w2), i3.__h = [], t2 = 0)), u2 = r2;
+ }, l.diffed = function(t3) {
+ v2 && v2(t3);
+ var o3 = t3.__c;
+ o3 && o3.__H && (o3.__H.__h.length && (1 !== f2.push(o3) && i2 === l.requestAnimationFrame || ((i2 = l.requestAnimationFrame) || j2)(b2)), o3.__H.__.forEach(function(n2) {
+ n2.i && (n2.__H = n2.i), n2.__V !== c2 && (n2.__ = n2.__V), n2.i = void 0, n2.__V = c2;
+ })), u2 = r2 = null;
+ }, l.__c = function(t3, r3) {
+ r3.some(function(t4) {
+ try {
+ t4.__h.forEach(k2), t4.__h = t4.__h.filter(function(n2) {
+ return !n2.__ || w2(n2);
+ });
+ } catch (u3) {
+ r3.some(function(n2) {
+ n2.__h && (n2.__h = []);
+ }), r3 = [], l.__e(u3, t4.__v);
+ }
+ }), l2 && l2(t3, r3);
+ }, l.unmount = function(t3) {
+ m2 && m2(t3);
+ var r3, u3 = t3.__c;
+ u3 && u3.__H && (u3.__H.__.forEach(function(n2) {
+ try {
+ k2(n2);
+ } catch (n3) {
+ r3 = n3;
+ }
+ }), u3.__H = void 0, r3 && l.__e(r3, u3.__v));
+ };
+ var g2 = "function" == typeof requestAnimationFrame;
+ function j2(n2) {
+ var t3, r3 = function() {
+ clearTimeout(u3), g2 && cancelAnimationFrame(t3), setTimeout(n2);
+ }, u3 = setTimeout(r3, 100);
+ g2 && (t3 = requestAnimationFrame(r3));
+ }
+ function k2(n2) {
+ var t3 = r2, u3 = n2.__c;
+ "function" == typeof u3 && (n2.__c = void 0, u3()), r2 = t3;
+ }
+ function w2(n2) {
+ var t3 = r2;
+ n2.__c = n2.__(), r2 = t3;
+ }
+ function z2(n2, t3) {
+ return !n2 || n2.length !== t3.length || t3.some(function(t4, r3) {
+ return t4 !== n2[r3];
+ });
+ }
+ function B(n2, t3) {
+ return "function" == typeof t3 ? t3(n2) : t3;
+ }
+
+ // shared/components/EnvironmentProvider.js
+ var EnvironmentContext = F({
+ isReducedMotion: false,
+ isDarkMode: false,
+ debugState: false,
+ injectName: (
+ /** @type {import('../environment').Environment['injectName']} */
+ "windows"
+ ),
+ willThrow: false
+ });
+ var THEME_QUERY = "(prefers-color-scheme: dark)";
+ var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
+ function EnvironmentProvider({ children, debugState, willThrow = false, injectName = "windows" }) {
+ const [theme, setTheme] = h2(window.matchMedia(THEME_QUERY).matches ? "dark" : "light");
+ const [isReducedMotion, setReducedMotion] = h2(window.matchMedia(REDUCED_MOTION_QUERY).matches);
+ p2(() => {
+ const mediaQueryList = window.matchMedia(THEME_QUERY);
+ const listener = (e3) => setTheme(e3.matches ? "dark" : "light");
+ mediaQueryList.addEventListener("change", listener);
+ return () => mediaQueryList.removeEventListener("change", listener);
+ }, []);
+ p2(() => {
+ const mediaQueryList = window.matchMedia(REDUCED_MOTION_QUERY);
+ const listener = (e3) => setter(e3.matches);
+ mediaQueryList.addEventListener("change", listener);
+ setter(mediaQueryList.matches);
+ function setter(value) {
+ document.documentElement.dataset.reducedMotion = String(value);
+ setReducedMotion(value);
+ }
+ window.addEventListener("toggle-reduced-motion", () => {
+ setter(true);
+ });
+ return () => mediaQueryList.removeEventListener("change", listener);
+ }, []);
+ return /* @__PURE__ */ y(EnvironmentContext.Provider, { value: {
+ isReducedMotion,
+ debugState,
+ isDarkMode: theme === "dark",
+ injectName,
+ willThrow
+ } }, children);
+ }
+ function UpdateEnvironment({ search }) {
+ p2(() => {
+ const params = new URLSearchParams(search);
+ if (params.has("reduced-motion")) {
+ setTimeout(() => {
+ window.dispatchEvent(new CustomEvent("toggle-reduced-motion"));
+ }, 0);
+ }
+ }, [search]);
+ return null;
+ }
+
+ // shared/components/Fallback/Fallback.module.css
+ var Fallback_default = {
+ fallback: "Fallback_fallback"
+ };
+
+ // shared/components/Fallback/Fallback.jsx
+ function Fallback({ showDetails }) {
+ return /* @__PURE__ */ y("div", { class: Fallback_default.fallback }, /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", null, "Something went wrong!"), showDetails && /* @__PURE__ */ y("p", null, "Please check logs for a message called ", /* @__PURE__ */ y("code", null, "reportPageException"))));
+ }
+
+ // shared/components/ErrorBoundary.js
+ var ErrorBoundary = class extends b {
+ /**
+ * @param {{didCatch: (params: {error: Error; info: any}) => void}} props
+ */
+ constructor(props) {
+ super(props);
+ this.state = { hasError: false };
+ }
+ static getDerivedStateFromError() {
+ return { hasError: true };
+ }
+ componentDidCatch(error, info) {
+ console.error(error);
+ console.log(info);
+ this.props.didCatch({ error, info });
+ }
+ render() {
+ if (this.state.hasError) {
+ return this.props.fallback;
+ }
+ return this.props.children;
+ }
+ };
+
+ // pages/new-tab/app/settings.provider.js
+ var SettingsContext = F(
+ /** @type {{settings: import("./settings.js").Settings}} */
+ {}
+ );
+ function SettingsProvider({ settings, children }) {
+ return /* @__PURE__ */ y(SettingsContext.Provider, { value: { settings } }, children);
+ }
+
+ // shared/translations.js
+ function apply(subject, replacements, textLength = 1) {
+ if (typeof subject !== "string" || subject.length === 0)
+ return "";
+ let out = subject;
+ if (replacements) {
+ for (let [name, value] of Object.entries(replacements)) {
+ if (typeof value !== "string")
+ value = "";
+ out = out.replaceAll(`{${name}}`, value);
+ }
+ }
+ if (textLength !== 1 && textLength > 0 && textLength <= 2) {
+ const targetLen = Math.ceil(out.length * textLength);
+ const target = Math.ceil(textLength);
+ const combined = out.repeat(target);
+ return combined.slice(0, targetLen);
+ }
+ return out;
+ }
+
+ // shared/components/TranslationsProvider.js
+ var TranslationContext = F({
+ /** @type {LocalTranslationFn} */
+ t: () => {
+ throw new Error("must implement");
+ }
+ });
+ function TranslationProvider({ children, translationObject, fallback, textLength = 1 }) {
+ function t3(inputKey, replacements) {
+ const subject = translationObject?.[inputKey]?.title || fallback?.[inputKey]?.title;
+ return apply(subject, replacements, textLength);
+ }
+ return /* @__PURE__ */ y(TranslationContext.Provider, { value: { t: t3 } }, children);
+ }
+
+ // pages/new-tab/src/locales/en/newtab.json
+ var newtab_default = {
+ smartling: {
+ string_format: "icu",
+ translate_paths: [
+ {
+ path: "*/title",
+ key: "{*}/title",
+ instruction: "*/note"
+ }
+ ]
+ },
+ helloWorld: {
+ title: "Hello world!",
+ note: "here!"
+ }
+ };
+
+ // pages/new-tab/app/types.js
+ var MessagingContext = F(
+ /** @type {import("../src/js/index.js").NewTabPage} */
+ {}
+ );
+
+ // pages/new-tab/app/widget-list/widget-config.js
+ var WidgetConfigAPI = class {
+ /**
+ * @param {import("../../src/js/index.js").NewTabPage} ntp - The internal data feed, expected to have a `subscribe` method.
+ * @param {WidgetConfig} initialData - Initial widget configuration.
+ */
+ constructor(ntp, initialData) {
+ this.ntp = ntp;
+ this.manager = new WidgetConfigManager(ntp, initialData);
+ }
+ /**
+ * @param {(data: WidgetConfig) => void} cb
+ * @return {() => void} - call this returned method to dispose of the subscription
+ */
+ onUpdate(cb) {
+ const controller = new AbortController();
+ this.manager.eventTarget.addEventListener(this.manager.DATA_CHANGE_EVT, (evt) => {
+ cb(evt.detail);
+ }, { signal: controller.signal });
+ return () => controller.abort();
+ }
+ /**
+ * @param {string} id
+ */
+ show(id) {
+ const next = this.manager.inMemoryData.map((w3) => {
+ if (w3.id === id)
+ return { ...w3, visibility: (
+ /** @type {const} */
+ "visible"
+ ) };
+ return w3;
+ });
+ this.manager.update(next);
+ }
+ /**
+ * @param {string} id
+ */
+ hide(id) {
+ const next = this.manager.inMemoryData.map((w3) => {
+ if (w3.id === id)
+ return { ...w3, visibility: (
+ /** @type {const} */
+ "hidden"
+ ) };
+ return w3;
+ });
+ this.manager.update(next);
+ }
+ };
+ var WidgetConfigManager = class {
+ debounceTimer = null;
+ eventTarget = new EventTarget();
+ DATA_CHANGE_EVT = "dataChanged";
+ DEBOUNCE_TIME_MS = 200;
+ /**
+ * @param {import("../../src/js/index.js").NewTabPage} ntp - The internal data feed, expected to have a `subscribe` method.
+ * @param {WidgetConfig} initialData - Initial widget configuration.
+ */
+ constructor(ntp, initialData) {
+ this.ntp = ntp;
+ this.inMemoryData = initialData;
+ this.setupSubscriptionStream();
+ }
+ /**
+ * Sets up the subscription stream from the data feed, which behaves like an input source
+ * that updates in-memory data but does not trigger persistence.
+ */
+ setupSubscriptionStream() {
+ this.ntp.messaging.subscribe("onWidgetConfigUpdated", (newData) => {
+ this.updateInMemoryData(newData.widgetConfig, "subscription");
+ });
+ }
+ /**
+ * Manually trigger an update, for example, from a UI element.
+ * @param {WidgetConfig} newData - The new widget configuration to update.
+ */
+ update(newData) {
+ this.updateInMemoryData(newData, "manual");
+ }
+ /**
+ * Updates the in-memory data and triggers persistence if the source is a manual update.
+ * This method centralizes all state updates.
+ * @param {WidgetConfig} newData - The new widget configuration to update in memory.
+ * @param {'manual' | 'subscription'} source - The source of the update. Either 'subscription' or 'manual'.
+ */
+ updateInMemoryData(newData, source) {
+ this.log(`Updating in-memory data from '${source}:'`, newData);
+ this.inMemoryData = structuredClone(newData);
+ this.broadcastChange();
+ if (source === "manual") {
+ this.clearDebounceTimer();
+ this.debounceTimer = /** @type {any} */
+ setTimeout(() => {
+ this.persist();
+ }, this.DEBOUNCE_TIME_MS);
+ }
+ }
+ /**
+ * Clears the debounce timer if it exists, simulating the switchMap behavior.
+ */
+ clearDebounceTimer() {
+ if (this.debounceTimer) {
+ this.log("Clearing previous debounce timer.");
+ clearTimeout(this.debounceTimer);
+ this.debounceTimer = null;
+ }
+ }
+ /**
+ * Broadcasts the current state to external subscribers using EventTarget.
+ */
+ broadcastChange() {
+ this.log("Broadcasting change to external listeners:", this.inMemoryData);
+ this.eventTarget.dispatchEvent(new CustomEvent(this.DATA_CHANGE_EVT, { detail: this.inMemoryData }));
+ }
+ /**
+ * Persists the current in-memory widget configuration state to the internal data feed.
+ */
+ persist() {
+ this.log("will persist data to backend:", this.inMemoryData);
+ this.ntp.messaging.notify("setWidgetConfig", { widgetConfig: this.inMemoryData });
+ }
+ /**
+ * Logs messages to the console for tracing internal state updates.
+ * @param {string} message - The message to log.
+ * @param {any} data - The associated data for the message.
+ */
+ log(message, data = null) {
+ console.log(`[WidgetConfigManager] ${message}`, data);
+ }
+ };
+
+ // pages/new-tab/app/widget-list/widget-config.provider.js
+ var WidgetConfigContext = F({
+ /** @type {WidgetList} */
+ widgets: [],
+ /** @type {WidgetConfig} */
+ widgetConfig: [],
+ /** @type {(name:string) => void} */
+ hide: () => {
+ },
+ /** @type {(name:string) => void} */
+ show: () => {
+ }
+ });
+ var WidgetConfigDispatchContext = F({
+ dispatch: null
+ });
+ function WidgetConfigProvider(props) {
+ const [data, setData] = h2(props.widgetConfig);
+ p2(() => {
+ const unsub = props.api.onUpdate((widgetConfig) => {
+ setData(widgetConfig);
+ });
+ return () => unsub();
+ }, [props.api]);
+ function hide(name) {
+ console.log("will hide", name);
+ props.api.hide(name);
+ }
+ function show(name) {
+ console.log("will show", name);
+ props.api.show(name);
+ }
+ return /* @__PURE__ */ y(WidgetConfigContext.Provider, { value: {
+ // this field is static for the lifespan of the page
+ widgets: props.widgets,
+ // this will be updated via subscriptions
+ widgetConfig: data,
+ hide,
+ show
+ } }, props.children);
+ }
+ var WidgetVisibilityContext = F({
+ visibility: (
+ /** @type {WidgetConfigItem['visibility']} */
+ "visible"
+ ),
+ id: (
+ /** @type {WidgetConfigItem['id']} */
+ ""
+ ),
+ toggle: () => {
+ }
+ });
+ function useVisibility() {
+ return q2(WidgetVisibilityContext);
+ }
+ function WidgetVisibilityProvider(props) {
+ const { widgetConfig, show, hide } = q2(WidgetConfigContext);
+ const toggle = T2(() => {
+ const matching = widgetConfig.find((x2) => x2.id === props.id);
+ if (matching?.visibility === "visible") {
+ hide(props.id);
+ } else {
+ show(props.id);
+ }
+ }, [props.id, widgetConfig]);
+ return /* @__PURE__ */ y(WidgetVisibilityContext.Provider, { value: {
+ visibility: props.visibility,
+ id: props.id,
+ toggle
+ } }, props.children);
+ }
+
+ // pages/new-tab/app/widget-list/WidgetList.js
+ var widgetMap = {
+ favorites: () => /* @__PURE__ */ y(Favorites, null),
+ privacyStats: () => /* @__PURE__ */ y(PrivacyStats, null)
+ };
+ function WidgetList() {
+ const { widgets, widgetConfig } = q2(WidgetConfigContext);
+ return /* @__PURE__ */ y("div", null, widgets.map((widget) => {
+ const matchingConfig = widgetConfig.find((item) => item.id === widget.id);
+ if (!matchingConfig) {
+ console.warn("missing config for widget: ", widget);
+ return null;
+ }
+ return /* @__PURE__ */ y(g, { key: widget.id }, /* @__PURE__ */ y(
+ WidgetVisibilityProvider,
+ {
+ visibility: matchingConfig.visibility,
+ id: matchingConfig.id
+ },
+ widgetMap[widget.id]?.()
+ ));
+ }));
+ }
+ function Favorites() {
+ const { visibility, id, toggle } = useVisibility();
+ return /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", { style: { opacity: visibility === "visible" ? "1" : "0.2" } }, "Favorites Component"), /* @__PURE__ */ y("code", null, /* @__PURE__ */ y("b", null, id, " visibility: ", visibility)), " ", /* @__PURE__ */ y("button", { type: "button", onClick: toggle }, "Toggle Favorites"));
+ }
+ function PrivacyStats() {
+ const { visibility, id, toggle } = useVisibility();
+ return /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", { style: { opacity: visibility === "visible" ? "1" : "0.2" } }, "Privacy Stats Component"), /* @__PURE__ */ y("code", null, /* @__PURE__ */ y("b", null, id, " visibility: ", visibility)), " ", /* @__PURE__ */ y("button", { type: "button", onClick: toggle }, "Toggle Privacy Stats"));
+ }
+
+ // pages/new-tab/app/settings.js
+ var Settings = class _Settings {
+ /**
+ * @param {object} params
+ * @param {{name: ImportMeta['platform']}} [params.platform]
+ */
+ constructor({
+ platform = { name: "windows" }
+ }) {
+ this.platform = platform;
+ }
+ withPlatformName(name) {
+ const valid = ["windows", "macos", "ios", "android"];
+ if (valid.includes(
+ /** @type {any} */
+ name
+ )) {
+ return new _Settings({
+ ...this,
+ platform: { name }
+ });
+ }
+ return this;
+ }
+ };
+
+ // pages/new-tab/app/index.js
+ async function init(messaging2, baseEnvironment2) {
+ const init2 = await messaging2.init();
+ if (!Array.isArray(init2.widgets)) {
+ throw new Error("missing critical initialSetup.widgets array");
+ }
+ if (!Array.isArray(init2.widgetConfig)) {
+ throw new Error("missing critical initialSetup.widgetConfig array");
+ }
+ const widgetConfigAPI = new WidgetConfigAPI(messaging2, init2.widgetConfig);
+ const environment = baseEnvironment2.withEnv(init2.env).withLocale(init2.locale).withLocale(baseEnvironment2.urlParams.get("locale")).withTextLength(baseEnvironment2.urlParams.get("textLength")).withDisplay(baseEnvironment2.urlParams.get("display"));
+ console.log("environment:", environment);
+ console.log("locale:", environment.locale);
+ const strings = environment.locale === "en" ? newtab_default : await fetch(`./locales/${environment.locale}/new-tab.json`).then((x2) => x2.json()).catch((e3) => {
+ console.error("Could not load locale", environment.locale, e3);
+ return newtab_default;
+ });
+ const settings = new Settings({}).withPlatformName(baseEnvironment2.injectName).withPlatformName(init2.platform?.name).withPlatformName(baseEnvironment2.urlParams.get("platform"));
+ const didCatch = (error) => {
+ const message = error?.message || error?.error || "unknown";
+ messaging2.reportPageException({ message });
+ };
+ const root = document.querySelector("#app");
+ if (!root)
+ throw new Error("could not render, root element missing");
+ q(
+ /* @__PURE__ */ y(
+ EnvironmentProvider,
+ {
+ debugState: environment.debugState,
+ injectName: environment.injectName,
+ willThrow: environment.willThrow
+ },
+ /* @__PURE__ */ y(ErrorBoundary, { didCatch, fallback: /* @__PURE__ */ y(Fallback, { showDetails: environment.env === "development" }) }, /* @__PURE__ */ y(UpdateEnvironment, { search: window.location.search }), /* @__PURE__ */ y(MessagingContext.Provider, { value: messaging2 }, /* @__PURE__ */ y(SettingsProvider, { settings }, /* @__PURE__ */ y(TranslationProvider, { translationObject: strings, fallback: strings, textLength: environment.textLength }, /* @__PURE__ */ y(WidgetConfigProvider, { api: widgetConfigAPI, widgetConfig: init2.widgetConfig, widgets: init2.widgets }, /* @__PURE__ */ y(App, null, /* @__PURE__ */ y(WidgetList, null)))))))
+ ),
+ root
+ );
+ }
+
+ // ../messaging/lib/windows.js
+ var WindowsMessagingTransport = class {
+ /**
+ * @param {WindowsMessagingConfig} config
+ * @param {import('../index.js').MessagingContext} messagingContext
+ * @internal
+ */
+ constructor(config, messagingContext) {
+ this.messagingContext = messagingContext;
+ this.config = config;
+ this.globals = {
+ window,
+ JSONparse: window.JSON.parse,
+ JSONstringify: window.JSON.stringify,
+ Promise: window.Promise,
+ Error: window.Error,
+ String: window.String
+ };
+ for (const [methodName, fn] of Object.entries(this.config.methods)) {
+ if (typeof fn !== "function") {
+ throw new Error("cannot create WindowsMessagingTransport, missing the method: " + methodName);
+ }
+ }
+ }
+ /**
+ * @param {import('../index.js').NotificationMessage} msg
+ */
+ notify(msg) {
+ const data = this.globals.JSONparse(this.globals.JSONstringify(msg.params || {}));
+ const notification = WindowsNotification.fromNotification(msg, data);
+ this.config.methods.postMessage(notification);
+ }
+ /**
+ * @param {import('../index.js').RequestMessage} msg
+ * @param {{signal?: AbortSignal}} opts
+ * @return {Promise}
+ */
+ request(msg, opts = {}) {
+ const data = this.globals.JSONparse(this.globals.JSONstringify(msg.params || {}));
+ const outgoing = WindowsRequestMessage.fromRequest(msg, data);
+ this.config.methods.postMessage(outgoing);
+ const comparator = (eventData) => {
+ return eventData.featureName === msg.featureName && eventData.context === msg.context && eventData.id === msg.id;
+ };
+ function isMessageResponse(data2) {
+ if ("result" in data2)
+ return true;
+ if ("error" in data2)
+ return true;
+ return false;
+ }
+ return new this.globals.Promise((resolve, reject) => {
+ try {
+ this._subscribe(comparator, opts, (value, unsubscribe) => {
+ unsubscribe();
+ if (!isMessageResponse(value)) {
+ console.warn("unknown response type", value);
+ return reject(new this.globals.Error("unknown response"));
+ }
+ if (value.result) {
+ return resolve(value.result);
+ }
+ const message = this.globals.String(value.error?.message || "unknown error");
+ reject(new this.globals.Error(message));
+ });
+ } catch (e3) {
+ reject(e3);
+ }
+ });
+ }
+ /**
+ * @param {import('../index.js').Subscription} msg
+ * @param {(value: unknown | undefined) => void} callback
+ */
+ subscribe(msg, callback) {
+ const comparator = (eventData) => {
+ return eventData.featureName === msg.featureName && eventData.context === msg.context && eventData.subscriptionName === msg.subscriptionName;
+ };
+ const cb = (eventData) => {
+ return callback(eventData.params);
+ };
+ return this._subscribe(comparator, {}, cb);
+ }
+ /**
+ * @typedef {import('../index.js').MessageResponse | import('../index.js').SubscriptionEvent} Incoming
+ */
+ /**
+ * @param {(eventData: any) => boolean} comparator
+ * @param {{signal?: AbortSignal}} options
+ * @param {(value: Incoming, unsubscribe: (()=>void)) => void} callback
+ * @internal
+ */
+ _subscribe(comparator, options, callback) {
+ if (options?.signal?.aborted) {
+ throw new DOMException("Aborted", "AbortError");
+ }
+ let teardown;
+ const idHandler = (event) => {
+ if (this.messagingContext.env === "production") {
+ if (event.origin !== null && event.origin !== void 0) {
+ console.warn("ignoring because evt.origin is not `null` or `undefined`");
+ return;
+ }
+ }
+ if (!event.data) {
+ console.warn("data absent from message");
+ return;
+ }
+ if (comparator(event.data)) {
+ if (!teardown)
+ throw new Error("unreachable");
+ callback(event.data, teardown);
+ }
+ };
+ const abortHandler = () => {
+ teardown?.();
+ throw new DOMException("Aborted", "AbortError");
+ };
+ this.config.methods.addEventListener("message", idHandler);
+ options?.signal?.addEventListener("abort", abortHandler);
+ teardown = () => {
+ this.config.methods.removeEventListener("message", idHandler);
+ options?.signal?.removeEventListener("abort", abortHandler);
+ };
+ return () => {
+ teardown?.();
+ };
+ }
+ };
+ var WindowsMessagingConfig = class {
+ /**
+ * @param {object} params
+ * @param {WindowsInteropMethods} params.methods
+ * @internal
+ */
+ constructor(params) {
+ this.methods = params.methods;
+ this.platform = "windows";
+ }
+ };
+ var WindowsNotification = class {
+ /**
+ * @param {object} params
+ * @param {string} params.Feature
+ * @param {string} params.SubFeatureName
+ * @param {string} params.Name
+ * @param {Record} [params.Data]
+ * @internal
+ */
+ constructor(params) {
+ this.Feature = params.Feature;
+ this.SubFeatureName = params.SubFeatureName;
+ this.Name = params.Name;
+ this.Data = params.Data;
+ }
+ /**
+ * Helper to convert a {@link NotificationMessage} to a format that Windows can support
+ * @param {NotificationMessage} notification
+ * @returns {WindowsNotification}
+ */
+ static fromNotification(notification, data) {
+ const output = {
+ Data: data,
+ Feature: notification.context,
+ SubFeatureName: notification.featureName,
+ Name: notification.method
+ };
+ return output;
+ }
+ };
+ var WindowsRequestMessage = class {
+ /**
+ * @param {object} params
+ * @param {string} params.Feature
+ * @param {string} params.SubFeatureName
+ * @param {string} params.Name
+ * @param {Record} [params.Data]
+ * @param {string} [params.Id]
+ * @internal
+ */
+ constructor(params) {
+ this.Feature = params.Feature;
+ this.SubFeatureName = params.SubFeatureName;
+ this.Name = params.Name;
+ this.Data = params.Data;
+ this.Id = params.Id;
+ }
+ /**
+ * Helper to convert a {@link RequestMessage} to a format that Windows can support
+ * @param {RequestMessage} msg
+ * @param {Record} data
+ * @returns {WindowsRequestMessage}
+ */
+ static fromRequest(msg, data) {
+ const output = {
+ Data: data,
+ Feature: msg.context,
+ SubFeatureName: msg.featureName,
+ Name: msg.method,
+ Id: msg.id
+ };
+ return output;
+ }
+ };
+
+ // ../messaging/schema.js
+ var RequestMessage = class {
+ /**
+ * @param {object} params
+ * @param {string} params.context
+ * @param {string} params.featureName
+ * @param {string} params.method
+ * @param {string} params.id
+ * @param {Record} [params.params]
+ * @internal
+ */
+ constructor(params) {
+ this.context = params.context;
+ this.featureName = params.featureName;
+ this.method = params.method;
+ this.id = params.id;
+ this.params = params.params;
+ }
+ };
+ var NotificationMessage = class {
+ /**
+ * @param {object} params
+ * @param {string} params.context
+ * @param {string} params.featureName
+ * @param {string} params.method
+ * @param {Record} [params.params]
+ * @internal
+ */
+ constructor(params) {
+ this.context = params.context;
+ this.featureName = params.featureName;
+ this.method = params.method;
+ this.params = params.params;
+ }
+ };
+ var Subscription = class {
+ /**
+ * @param {object} params
+ * @param {string} params.context
+ * @param {string} params.featureName
+ * @param {string} params.subscriptionName
+ * @internal
+ */
+ constructor(params) {
+ this.context = params.context;
+ this.featureName = params.featureName;
+ this.subscriptionName = params.subscriptionName;
+ }
+ };
+ function isResponseFor(request, data) {
+ if ("result" in data) {
+ return data.featureName === request.featureName && data.context === request.context && data.id === request.id;
+ }
+ if ("error" in data) {
+ if ("message" in data.error) {
+ return true;
+ }
+ }
+ return false;
+ }
+ function isSubscriptionEventFor(sub, data) {
+ if ("subscriptionName" in data) {
+ return data.featureName === sub.featureName && data.context === sub.context && data.subscriptionName === sub.subscriptionName;
+ }
+ return false;
+ }
+
+ // ../messaging/lib/webkit.js
+ var WebkitMessagingTransport = class {
+ /**
+ * @param {WebkitMessagingConfig} config
+ * @param {import('../index.js').MessagingContext} messagingContext
+ */
+ constructor(config, messagingContext) {
+ this.messagingContext = messagingContext;
+ this.config = config;
+ this.globals = captureGlobals();
+ if (!this.config.hasModernWebkitAPI) {
+ this.captureWebkitHandlers(this.config.webkitMessageHandlerNames);
+ }
+ }
+ /**
+ * Sends message to the webkit layer (fire and forget)
+ * @param {String} handler
+ * @param {*} data
+ * @internal
+ */
+ wkSend(handler, data = {}) {
+ if (!(handler in this.globals.window.webkit.messageHandlers)) {
+ throw new MissingHandler(`Missing webkit handler: '${handler}'`, handler);
+ }
+ if (!this.config.hasModernWebkitAPI) {
+ const outgoing = {
+ ...data,
+ messageHandling: {
+ ...data.messageHandling,
+ secret: this.config.secret
+ }
+ };
+ if (!(handler in this.globals.capturedWebkitHandlers)) {
+ throw new MissingHandler(`cannot continue, method ${handler} not captured on macos < 11`, handler);
+ } else {
+ return this.globals.capturedWebkitHandlers[handler](outgoing);
+ }
+ }
+ return this.globals.window.webkit.messageHandlers[handler].postMessage?.(data);
+ }
+ /**
+ * Sends message to the webkit layer and waits for the specified response
+ * @param {String} handler
+ * @param {import('../index.js').RequestMessage} data
+ * @returns {Promise<*>}
+ * @internal
+ */
+ async wkSendAndWait(handler, data) {
+ if (this.config.hasModernWebkitAPI) {
+ const response = await this.wkSend(handler, data);
+ return this.globals.JSONparse(response || "{}");
+ }
+ try {
+ const randMethodName = this.createRandMethodName();
+ const key = await this.createRandKey();
+ const iv = this.createRandIv();
+ const {
+ ciphertext,
+ tag
+ } = await new this.globals.Promise((resolve) => {
+ this.generateRandomMethod(randMethodName, resolve);
+ data.messageHandling = new SecureMessagingParams({
+ methodName: randMethodName,
+ secret: this.config.secret,
+ key: this.globals.Arrayfrom(key),
+ iv: this.globals.Arrayfrom(iv)
+ });
+ this.wkSend(handler, data);
+ });
+ const cipher = new this.globals.Uint8Array([...ciphertext, ...tag]);
+ const decrypted = await this.decrypt(cipher, key, iv);
+ return this.globals.JSONparse(decrypted || "{}");
+ } catch (e3) {
+ if (e3 instanceof MissingHandler) {
+ throw e3;
+ } else {
+ console.error("decryption failed", e3);
+ console.error(e3);
+ return { error: e3 };
+ }
+ }
+ }
+ /**
+ * @param {import('../index.js').NotificationMessage} msg
+ */
+ notify(msg) {
+ this.wkSend(msg.context, msg);
+ }
+ /**
+ * @param {import('../index.js').RequestMessage} msg
+ */
+ async request(msg) {
+ const data = await this.wkSendAndWait(msg.context, msg);
+ if (isResponseFor(msg, data)) {
+ if (data.result) {
+ return data.result || {};
+ }
+ if (data.error) {
+ throw new Error(data.error.message);
+ }
+ }
+ throw new Error("an unknown error occurred");
+ }
+ /**
+ * Generate a random method name and adds it to the global scope
+ * The native layer will use this method to send the response
+ * @param {string | number} randomMethodName
+ * @param {Function} callback
+ * @internal
+ */
+ generateRandomMethod(randomMethodName, callback) {
+ this.globals.ObjectDefineProperty(this.globals.window, randomMethodName, {
+ enumerable: false,
+ // configurable, To allow for deletion later
+ configurable: true,
+ writable: false,
+ /**
+ * @param {any[]} args
+ */
+ value: (...args) => {
+ callback(...args);
+ delete this.globals.window[randomMethodName];
+ }
+ });
+ }
+ /**
+ * @internal
+ * @return {string}
+ */
+ randomString() {
+ return "" + this.globals.getRandomValues(new this.globals.Uint32Array(1))[0];
+ }
+ /**
+ * @internal
+ * @return {string}
+ */
+ createRandMethodName() {
+ return "_" + this.randomString();
+ }
+ /**
+ * @type {{name: string, length: number}}
+ * @internal
+ */
+ algoObj = {
+ name: "AES-GCM",
+ length: 256
+ };
+ /**
+ * @returns {Promise}
+ * @internal
+ */
+ async createRandKey() {
+ const key = await this.globals.generateKey(this.algoObj, true, ["encrypt", "decrypt"]);
+ const exportedKey = await this.globals.exportKey("raw", key);
+ return new this.globals.Uint8Array(exportedKey);
+ }
+ /**
+ * @returns {Uint8Array}
+ * @internal
+ */
+ createRandIv() {
+ return this.globals.getRandomValues(new this.globals.Uint8Array(12));
+ }
+ /**
+ * @param {BufferSource} ciphertext
+ * @param {BufferSource} key
+ * @param {Uint8Array} iv
+ * @returns {Promise}
+ * @internal
+ */
+ async decrypt(ciphertext, key, iv) {
+ const cryptoKey = await this.globals.importKey("raw", key, "AES-GCM", false, ["decrypt"]);
+ const algo = {
+ name: "AES-GCM",
+ iv
+ };
+ const decrypted = await this.globals.decrypt(algo, cryptoKey, ciphertext);
+ const dec = new this.globals.TextDecoder();
+ return dec.decode(decrypted);
+ }
+ /**
+ * When required (such as on macos 10.x), capture the `postMessage` method on
+ * each webkit messageHandler
+ *
+ * @param {string[]} handlerNames
+ */
+ captureWebkitHandlers(handlerNames) {
+ const handlers = window.webkit.messageHandlers;
+ if (!handlers)
+ throw new MissingHandler("window.webkit.messageHandlers was absent", "all");
+ for (const webkitMessageHandlerName of handlerNames) {
+ if (typeof handlers[webkitMessageHandlerName]?.postMessage === "function") {
+ const original = handlers[webkitMessageHandlerName];
+ const bound = handlers[webkitMessageHandlerName].postMessage?.bind(original);
+ this.globals.capturedWebkitHandlers[webkitMessageHandlerName] = bound;
+ delete handlers[webkitMessageHandlerName].postMessage;
+ }
+ }
+ }
+ /**
+ * @param {import('../index.js').Subscription} msg
+ * @param {(value: unknown) => void} callback
+ */
+ subscribe(msg, callback) {
+ if (msg.subscriptionName in this.globals.window) {
+ throw new this.globals.Error(`A subscription with the name ${msg.subscriptionName} already exists`);
+ }
+ this.globals.ObjectDefineProperty(this.globals.window, msg.subscriptionName, {
+ enumerable: false,
+ configurable: true,
+ writable: false,
+ value: (data) => {
+ if (data && isSubscriptionEventFor(msg, data)) {
+ callback(data.params);
+ } else {
+ console.warn("Received a message that did not match the subscription", data);
+ }
+ }
+ });
+ return () => {
+ this.globals.ReflectDeleteProperty(this.globals.window, msg.subscriptionName);
+ };
+ }
+ };
+ var WebkitMessagingConfig = class {
+ /**
+ * @param {object} params
+ * @param {boolean} params.hasModernWebkitAPI
+ * @param {string[]} params.webkitMessageHandlerNames
+ * @param {string} params.secret
+ * @internal
+ */
+ constructor(params) {
+ this.hasModernWebkitAPI = params.hasModernWebkitAPI;
+ this.webkitMessageHandlerNames = params.webkitMessageHandlerNames;
+ this.secret = params.secret;
+ }
+ };
+ var SecureMessagingParams = class {
+ /**
+ * @param {object} params
+ * @param {string} params.methodName
+ * @param {string} params.secret
+ * @param {number[]} params.key
+ * @param {number[]} params.iv
+ */
+ constructor(params) {
+ this.methodName = params.methodName;
+ this.secret = params.secret;
+ this.key = params.key;
+ this.iv = params.iv;
+ }
+ };
+ function captureGlobals() {
+ const globals = {
+ window,
+ getRandomValues: window.crypto.getRandomValues.bind(window.crypto),
+ TextEncoder,
+ TextDecoder,
+ Uint8Array,
+ Uint16Array,
+ Uint32Array,
+ JSONstringify: window.JSON.stringify,
+ JSONparse: window.JSON.parse,
+ Arrayfrom: window.Array.from,
+ Promise: window.Promise,
+ Error: window.Error,
+ ReflectDeleteProperty: window.Reflect.deleteProperty.bind(window.Reflect),
+ ObjectDefineProperty: window.Object.defineProperty,
+ addEventListener: window.addEventListener.bind(window),
+ /** @type {Record} */
+ capturedWebkitHandlers: {}
+ };
+ if (isSecureContext) {
+ globals.generateKey = window.crypto.subtle.generateKey.bind(window.crypto.subtle);
+ globals.exportKey = window.crypto.subtle.exportKey.bind(window.crypto.subtle);
+ globals.importKey = window.crypto.subtle.importKey.bind(window.crypto.subtle);
+ globals.encrypt = window.crypto.subtle.encrypt.bind(window.crypto.subtle);
+ globals.decrypt = window.crypto.subtle.decrypt.bind(window.crypto.subtle);
+ }
+ return globals;
+ }
+
+ // ../messaging/lib/android.js
+ var AndroidMessagingTransport = class {
+ /**
+ * @param {AndroidMessagingConfig} config
+ * @param {MessagingContext} messagingContext
+ * @internal
+ */
+ constructor(config, messagingContext) {
+ this.messagingContext = messagingContext;
+ this.config = config;
+ }
+ /**
+ * @param {NotificationMessage} msg
+ */
+ notify(msg) {
+ try {
+ this.config.sendMessageThrows?.(JSON.stringify(msg));
+ } catch (e3) {
+ console.error(".notify failed", e3);
+ }
+ }
+ /**
+ * @param {RequestMessage} msg
+ * @return {Promise}
+ */
+ request(msg) {
+ return new Promise((resolve, reject) => {
+ const unsub = this.config.subscribe(msg.id, handler);
+ try {
+ this.config.sendMessageThrows?.(JSON.stringify(msg));
+ } catch (e3) {
+ unsub();
+ reject(new Error("request failed to send: " + e3.message || "unknown error"));
+ }
+ function handler(data) {
+ if (isResponseFor(msg, data)) {
+ if (data.result) {
+ resolve(data.result || {});
+ return unsub();
+ }
+ if (data.error) {
+ reject(new Error(data.error.message));
+ return unsub();
+ }
+ unsub();
+ throw new Error("unreachable: must have `result` or `error` key by this point");
+ }
+ }
+ });
+ }
+ /**
+ * @param {Subscription} msg
+ * @param {(value: unknown | undefined) => void} callback
+ */
+ subscribe(msg, callback) {
+ const unsub = this.config.subscribe(msg.subscriptionName, (data) => {
+ if (isSubscriptionEventFor(msg, data)) {
+ callback(data.params || {});
+ }
+ });
+ return () => {
+ unsub();
+ };
+ }
+ };
+ var AndroidMessagingConfig = class {
+ /** @type {(json: string, secret: string) => void} */
+ _capturedHandler;
+ /**
+ * @param {object} params
+ * @param {Record} params.target
+ * @param {boolean} params.debug
+ * @param {string} params.messageSecret - a secret to ensure that messages are only
+ * processed by the correct handler
+ * @param {string} params.javascriptInterface - the name of the javascript interface
+ * registered on the native side
+ * @param {string} params.messageCallback - the name of the callback that the native
+ * side will use to send messages back to the javascript side
+ */
+ constructor(params) {
+ this.target = params.target;
+ this.debug = params.debug;
+ this.javascriptInterface = params.javascriptInterface;
+ this.messageSecret = params.messageSecret;
+ this.messageCallback = params.messageCallback;
+ this.listeners = new globalThis.Map();
+ this._captureGlobalHandler();
+ this._assignHandlerMethod();
+ }
+ /**
+ * The transport can call this to transmit a JSON payload along with a secret
+ * to the native Android handler.
+ *
+ * Note: This can throw - it's up to the transport to handle the error.
+ *
+ * @type {(json: string) => void}
+ * @throws
+ * @internal
+ */
+ sendMessageThrows(json) {
+ this._capturedHandler(json, this.messageSecret);
+ }
+ /**
+ * A subscription on Android is just a named listener. All messages from
+ * android -> are delivered through a single function, and this mapping is used
+ * to route the messages to the correct listener.
+ *
+ * Note: Use this to implement request->response by unsubscribing after the first
+ * response.
+ *
+ * @param {string} id
+ * @param {(msg: MessageResponse | SubscriptionEvent) => void} callback
+ * @returns {() => void}
+ * @internal
+ */
+ subscribe(id, callback) {
+ this.listeners.set(id, callback);
+ return () => {
+ this.listeners.delete(id);
+ };
+ }
+ /**
+ * Accept incoming messages and try to deliver it to a registered listener.
+ *
+ * This code is defensive to prevent any single handler from affecting another if
+ * it throws (producer interference).
+ *
+ * @param {MessageResponse | SubscriptionEvent} payload
+ * @internal
+ */
+ _dispatch(payload) {
+ if (!payload)
+ return this._log("no response");
+ if ("id" in payload) {
+ if (this.listeners.has(payload.id)) {
+ this._tryCatch(() => this.listeners.get(payload.id)?.(payload));
+ } else {
+ this._log("no listeners for ", payload);
+ }
+ }
+ if ("subscriptionName" in payload) {
+ if (this.listeners.has(payload.subscriptionName)) {
+ this._tryCatch(() => this.listeners.get(payload.subscriptionName)?.(payload));
+ } else {
+ this._log("no subscription listeners for ", payload);
+ }
+ }
+ }
+ /**
+ *
+ * @param {(...args: any[]) => any} fn
+ * @param {string} [context]
+ */
+ _tryCatch(fn, context = "none") {
+ try {
+ return fn();
+ } catch (e3) {
+ if (this.debug) {
+ console.error("AndroidMessagingConfig error:", context);
+ console.error(e3);
+ }
+ }
+ }
+ /**
+ * @param {...any} args
+ */
+ _log(...args) {
+ if (this.debug) {
+ console.log("AndroidMessagingConfig", ...args);
+ }
+ }
+ /**
+ * Capture the global handler and remove it from the global object.
+ */
+ _captureGlobalHandler() {
+ const { target, javascriptInterface } = this;
+ if (Object.prototype.hasOwnProperty.call(target, javascriptInterface)) {
+ this._capturedHandler = target[javascriptInterface].process.bind(target[javascriptInterface]);
+ delete target[javascriptInterface];
+ } else {
+ this._capturedHandler = () => {
+ this._log("Android messaging interface not available", javascriptInterface);
+ };
+ }
+ }
+ /**
+ * Assign the incoming handler method to the global object.
+ * This is the method that Android will call to deliver messages.
+ */
+ _assignHandlerMethod() {
+ const responseHandler = (providedSecret, response) => {
+ if (providedSecret === this.messageSecret) {
+ this._dispatch(response);
+ }
+ };
+ Object.defineProperty(this.target, this.messageCallback, {
+ value: responseHandler
+ });
+ }
+ };
+
+ // ../messaging/lib/typed-messages.js
+ function createTypedMessages(base, messaging2) {
+ const asAny = (
+ /** @type {any} */
+ messaging2
+ );
+ return (
+ /** @type {BaseClass} */
+ asAny
+ );
+ }
+
+ // ../messaging/index.js
+ var MessagingContext2 = class {
+ /**
+ * @param {object} params
+ * @param {string} params.context
+ * @param {string} params.featureName
+ * @param {"production" | "development"} params.env
+ * @internal
+ */
+ constructor(params) {
+ this.context = params.context;
+ this.featureName = params.featureName;
+ this.env = params.env;
+ }
+ };
+ var Messaging = class {
+ /**
+ * @param {MessagingContext} messagingContext
+ * @param {MessagingConfig} config
+ */
+ constructor(messagingContext, config) {
+ this.messagingContext = messagingContext;
+ this.transport = getTransport(config, this.messagingContext);
+ }
+ /**
+ * Send a 'fire-and-forget' message.
+ * @throws {MissingHandler}
+ *
+ * @example
+ *
+ * ```ts
+ * const messaging = new Messaging(config)
+ * messaging.notify("foo", {bar: "baz"})
+ * ```
+ * @param {string} name
+ * @param {Record} [data]
+ */
+ notify(name, data = {}) {
+ const message = new NotificationMessage({
+ context: this.messagingContext.context,
+ featureName: this.messagingContext.featureName,
+ method: name,
+ params: data
+ });
+ this.transport.notify(message);
+ }
+ /**
+ * Send a request, and wait for a response
+ * @throws {MissingHandler}
+ *
+ * @example
+ * ```
+ * const messaging = new Messaging(config)
+ * const response = await messaging.request("foo", {bar: "baz"})
+ * ```
+ *
+ * @param {string} name
+ * @param {Record} [data]
+ * @return {Promise}
+ */
+ request(name, data = {}) {
+ const id = globalThis?.crypto?.randomUUID?.() || name + ".response";
+ const message = new RequestMessage({
+ context: this.messagingContext.context,
+ featureName: this.messagingContext.featureName,
+ method: name,
+ params: data,
+ id
+ });
+ return this.transport.request(message);
+ }
+ /**
+ * @param {string} name
+ * @param {(value: unknown) => void} callback
+ * @return {() => void}
+ */
+ subscribe(name, callback) {
+ const msg = new Subscription({
+ context: this.messagingContext.context,
+ featureName: this.messagingContext.featureName,
+ subscriptionName: name
+ });
+ return this.transport.subscribe(msg, callback);
+ }
+ };
+ var TestTransportConfig = class {
+ /**
+ * @param {MessagingTransport} impl
+ */
+ constructor(impl) {
+ this.impl = impl;
+ }
+ };
+ var TestTransport = class {
+ /**
+ * @param {TestTransportConfig} config
+ * @param {MessagingContext} messagingContext
+ */
+ constructor(config, messagingContext) {
+ this.config = config;
+ this.messagingContext = messagingContext;
+ }
+ notify(msg) {
+ return this.config.impl.notify(msg);
+ }
+ request(msg) {
+ return this.config.impl.request(msg);
+ }
+ subscribe(msg, callback) {
+ return this.config.impl.subscribe(msg, callback);
+ }
+ };
+ function getTransport(config, messagingContext) {
+ if (config instanceof WebkitMessagingConfig) {
+ return new WebkitMessagingTransport(config, messagingContext);
+ }
+ if (config instanceof WindowsMessagingConfig) {
+ return new WindowsMessagingTransport(config, messagingContext);
+ }
+ if (config instanceof AndroidMessagingConfig) {
+ return new AndroidMessagingTransport(config, messagingContext);
+ }
+ if (config instanceof TestTransportConfig) {
+ return new TestTransport(config, messagingContext);
+ }
+ throw new Error("unreachable");
+ }
+ var MissingHandler = class extends Error {
+ /**
+ * @param {string} message
+ * @param {string} handlerName
+ */
+ constructor(message, handlerName) {
+ super(message);
+ this.handlerName = handlerName;
+ }
+ };
+
+ // shared/create-special-page-messaging.js
+ function createSpecialPageMessaging(opts) {
+ const messageContext = new MessagingContext2({
+ context: "specialPages",
+ featureName: opts.pageName,
+ env: opts.env
+ });
+ try {
+ if (opts.injectName === "windows") {
+ const opts2 = new WindowsMessagingConfig({
+ methods: {
+ // @ts-expect-error - not in @types/chrome
+ postMessage: window.chrome.webview.postMessage,
+ // @ts-expect-error - not in @types/chrome
+ addEventListener: window.chrome.webview.addEventListener,
+ // @ts-expect-error - not in @types/chrome
+ removeEventListener: window.chrome.webview.removeEventListener
+ }
+ });
+ return new Messaging(messageContext, opts2);
+ } else if (opts.injectName === "apple") {
+ const opts2 = new WebkitMessagingConfig({
+ hasModernWebkitAPI: true,
+ secret: "",
+ webkitMessageHandlerNames: ["specialPages"]
+ });
+ return new Messaging(messageContext, opts2);
+ } else if (opts.injectName === "android") {
+ const opts2 = new AndroidMessagingConfig({
+ messageSecret: "duckduckgo-android-messaging-secret",
+ messageCallback: "messageCallback",
+ javascriptInterface: messageContext.context,
+ target: globalThis,
+ debug: true
+ });
+ return new Messaging(messageContext, opts2);
+ }
+ } catch (e3) {
+ console.error("could not access handlers for %s, falling back to mock interface", opts.injectName);
+ }
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
+ /**
+ * @param {import('@duckduckgo/messaging').NotificationMessage} msg
+ */
+ notify(msg) {
+ console.log(msg);
+ },
+ /**
+ * @param {import('@duckduckgo/messaging').RequestMessage} msg
+ */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ request: (msg) => {
+ console.log(msg);
+ if (msg.method === "initialSetup") {
+ return Promise.resolve({
+ locale: "en",
+ env: opts.env
+ });
+ }
+ return Promise.resolve(null);
+ },
+ /**
+ * @param {import('@duckduckgo/messaging').SubscriptionEvent} msg
+ */
+ subscribe(msg) {
+ console.log(msg);
+ return () => {
+ console.log("teardown");
+ };
+ }
+ });
+ return new Messaging(messageContext, fallback);
+ }
+
+ // shared/environment.js
+ var Environment = class _Environment {
+ /**
+ * @param {object} params
+ * @param {'app' | 'components'} [params.display] - whether to show the application or component list
+ * @param {'production' | 'development'} [params.env] - application environment
+ * @param {URLSearchParams} [params.urlParams] - URL params passed into the page
+ * @param {ImportMeta['injectName']} [params.injectName] - application platform
+ * @param {boolean} [params.willThrow] - whether the application will simulate an error
+ * @param {boolean} [params.debugState] - whether to show debugging UI
+ * @param {string} [params.locale] - for applications strings
+ * @param {number} [params.textLength] - what ratio of text should be used. Set a number higher than 1 to have longer strings for testing
+ */
+ constructor({
+ env = "production",
+ urlParams = new URLSearchParams(location.search),
+ injectName = "windows",
+ willThrow = urlParams.get("willThrow") === "true",
+ debugState = urlParams.has("debugState"),
+ display = "app",
+ locale = "en",
+ textLength = 1
+ } = {}) {
+ this.display = display;
+ this.urlParams = urlParams;
+ this.injectName = injectName;
+ this.willThrow = willThrow;
+ this.debugState = debugState;
+ this.env = env;
+ this.locale = locale;
+ this.textLength = textLength;
+ }
+ /**
+ * @param {string|null|undefined} injectName
+ * @returns {Environment}
+ */
+ withInjectName(injectName) {
+ if (!injectName)
+ return this;
+ if (!isInjectName(injectName))
+ return this;
+ return new _Environment({
+ ...this,
+ injectName
+ });
+ }
+ /**
+ * @param {string|null|undefined} env
+ * @returns {Environment}
+ */
+ withEnv(env) {
+ if (!env)
+ return this;
+ if (env !== "production" && env !== "development")
+ return this;
+ return new _Environment({
+ ...this,
+ env
+ });
+ }
+ /**
+ * @param {string|null|undefined} display
+ * @returns {Environment}
+ */
+ withDisplay(display) {
+ if (!display)
+ return this;
+ if (display !== "app" && display !== "components")
+ return this;
+ return new _Environment({
+ ...this,
+ display
+ });
+ }
+ /**
+ * @param {string|null|undefined} locale
+ * @returns {Environment}
+ */
+ withLocale(locale) {
+ if (!locale)
+ return this;
+ if (typeof locale !== "string")
+ return this;
+ if (locale.length !== 2)
+ return this;
+ return new _Environment({
+ ...this,
+ locale
+ });
+ }
+ /**
+ * @param {string|number|null|undefined} length
+ * @returns {Environment}
+ */
+ withTextLength(length) {
+ if (!length)
+ return this;
+ const num = Number(length);
+ if (num >= 1 && num <= 2) {
+ return new _Environment({
+ ...this,
+ textLength: num
+ });
+ }
+ return this;
+ }
+ };
+ function isInjectName(input) {
+ const allowed = ["windows", "apple", "integration", "android"];
+ return allowed.includes(input);
+ }
+
+ // pages/new-tab/src/js/mock-transport.js
+ function mockTransport() {
+ const channel = new BroadcastChannel("ntp");
+ function broadcast() {
+ setTimeout(() => {
+ channel.postMessage({
+ change: "ntp.widgetConfig"
+ });
+ }, 100);
+ }
+ function read(name) {
+ try {
+ const item = localStorage.getItem(name);
+ if (!item)
+ return null;
+ console.log("did read from LS", item);
+ return JSON.parse(item);
+ } catch (e3) {
+ console.error("Failed to parse initialSetup from localStorage", e3);
+ return null;
+ }
+ }
+ function write(name, value) {
+ try {
+ localStorage.setItem(name, JSON.stringify(value));
+ console.log("\u2705 did write");
+ } catch (e3) {
+ console.error("Failed to write", e3);
+ }
+ }
+ return new TestTransportConfig({
+ notify(msg) {
+ switch (msg.method) {
+ case "setWidgetConfig": {
+ if (!msg.params)
+ throw new Error("unreachable");
+ write("ntp.widgetConfig", msg.params);
+ broadcast();
+ return;
+ }
+ default: {
+ console.warn("unhandled notification", msg);
+ }
+ }
+ },
+ subscribe(sub, cb) {
+ switch (sub.subscriptionName) {
+ case "onWidgetConfigUpdated": {
+ const controller = new AbortController();
+ channel.addEventListener("message", () => {
+ const values = read("ntp.widgetConfig");
+ if (values) {
+ cb(values);
+ }
+ }, { signal: controller.signal });
+ return () => controller.abort();
+ }
+ }
+ return () => {
+ };
+ },
+ // eslint-ignore-next-line require-await
+ request(msg) {
+ switch (msg.method) {
+ case "initialSetup": {
+ const widgetsFromStorage = read("ntp.widgets") || {
+ widgets: [
+ { id: "favorites" },
+ { id: "privacyStats" }
+ ]
+ };
+ const widgetConfigFromStorage = read("ntp.widgetConfig") || {
+ widgetConfig: [
+ { id: "favorites", visibility: "visible" },
+ { id: "privacyStats", visibility: "visible" }
+ ]
+ };
+ return Promise.resolve({
+ widgets: widgetsFromStorage.widgets,
+ widgetConfig: widgetConfigFromStorage.widgetConfig,
+ platform: { name: "integration" },
+ env: "development",
+ locale: "en"
+ });
+ }
+ default: {
+ return Promise.reject(new Error("unhandled request"));
+ }
+ }
+ }
+ });
+ }
+
+ // pages/new-tab/src/js/index.js
+ var NewTabPage = class {
+ /**
+ * @param {import("@duckduckgo/messaging").Messaging} messaging
+ * @param {ImportMeta['injectName']} injectName
+ */
+ constructor(messaging2, injectName) {
+ this.messaging = createTypedMessages(this, messaging2);
+ this.injectName = injectName;
+ }
+ /**
+ * @return {Promise}
+ */
+ init() {
+ return this.messaging.request("initialSetup");
+ }
+ /**
+ * @param {string} message
+ */
+ reportInitException(message) {
+ this.messaging.notify("reportInitException", { message });
+ }
+ /**
+ * This will be sent if the application has loaded, but a client-side error
+ * has occurred that cannot be recovered from
+ * @param {{message: string}} params
+ */
+ reportPageException(params) {
+ this.messaging.notify("reportPageException", params);
+ }
+ };
+ var baseEnvironment = new Environment().withInjectName("integration").withEnv("production");
+ var messaging = createSpecialPageMessaging({
+ injectName: "integration",
+ env: "production",
+ pageName: "newTabPage",
+ mockTransport: () => {
+ if (baseEnvironment.injectName !== "integration")
+ return null;
+ if (window.__playwright_01)
+ return null;
+ let mock = null;
+ $INTEGRATION:
+ mock = mockTransport();
+ return mock;
+ }
+ });
+ var newTabMessaging = new NewTabPage(messaging, "integration");
+ init(newTabMessaging, baseEnvironment).catch((e3) => {
+ console.error(e3);
+ const msg = typeof e3?.message === "string" ? e3.message : "unknown init error";
+ newTabMessaging.reportInitException(msg);
+ });
+})();
diff --git a/build/integration/pages/new-tab/js/inline.js b/build/integration/pages/new-tab/js/inline.js
new file mode 100644
index 000000000..5a71d2623
--- /dev/null
+++ b/build/integration/pages/new-tab/js/inline.js
@@ -0,0 +1,3 @@
+"use strict";
+(() => {
+})();
diff --git a/build/integration/pages/new-tab/js/mock-transport.js b/build/integration/pages/new-tab/js/mock-transport.js
new file mode 100644
index 000000000..ca6b115d2
--- /dev/null
+++ b/build/integration/pages/new-tab/js/mock-transport.js
@@ -0,0 +1,103 @@
+import { TestTransportConfig } from '@duckduckgo/messaging'
+
+export function mockTransport () {
+ const channel = new BroadcastChannel('ntp')
+
+ function broadcast () {
+ setTimeout(() => {
+ channel.postMessage({
+ change: 'ntp.widgetConfig'
+ })
+ }, 100)
+ }
+
+ /**
+ * @param {string} name
+ * @return {Record|null}
+ */
+ function read (name) {
+ try {
+ const item = localStorage.getItem(name)
+ if (!item) return null
+ console.log('did read from LS', item)
+ return JSON.parse(item)
+ } catch (e) {
+ console.error('Failed to parse initialSetup from localStorage', e)
+ return null
+ }
+ }
+
+ /**
+ * @param {string} name
+ * @param {Record} value
+ */
+ function write (name, value) {
+ try {
+ localStorage.setItem(name, JSON.stringify(value))
+ console.log('✅ did write')
+ } catch (e) {
+ console.error('Failed to write', e)
+ }
+ }
+
+ return new TestTransportConfig({
+ notify (msg) {
+ switch (msg.method) {
+ case 'setWidgetConfig': {
+ if (!msg.params) throw new Error('unreachable')
+ write('ntp.widgetConfig', msg.params)
+ broadcast()
+ return
+ }
+ default: {
+ console.warn('unhandled notification', msg)
+ }
+ }
+ },
+ subscribe (sub, cb) {
+ switch (sub.subscriptionName) {
+ case 'onWidgetConfigUpdated': {
+ const controller = new AbortController()
+ // console.log('sub?', sub, cb);
+ channel.addEventListener('message', () => {
+ const values = read('ntp.widgetConfig')
+ if (values) {
+ cb(values)
+ }
+ }, { signal: controller.signal })
+ return () => controller.abort()
+ }
+ }
+ return () => {}
+ },
+ // eslint-ignore-next-line require-await
+ request (msg) {
+ switch (msg.method) {
+ case 'initialSetup': {
+ const widgetsFromStorage = read('ntp.widgets') || {
+ widgets: [
+ { id: 'favorites' },
+ { id: 'privacyStats' }
+ ]
+ }
+ const widgetConfigFromStorage = read('ntp.widgetConfig') || {
+ widgetConfig: [
+ { id: 'favorites', visibility: 'visible' },
+ { id: 'privacyStats', visibility: 'visible' }
+ ]
+ }
+ return Promise.resolve({
+ widgets: widgetsFromStorage.widgets,
+ widgetConfig: widgetConfigFromStorage.widgetConfig,
+ platform: { name: 'integration' },
+ env: 'development',
+ locale: 'en'
+ })
+ }
+ default: {
+ return Promise.reject(new Error('unhandled request'))
+ }
+ }
+ }
+ })
+}
diff --git a/build/integration/pages/new-tab/locales/en/newtab.json b/build/integration/pages/new-tab/locales/en/newtab.json
new file mode 100644
index 000000000..6d61912fc
--- /dev/null
+++ b/build/integration/pages/new-tab/locales/en/newtab.json
@@ -0,0 +1,16 @@
+{
+ "smartling": {
+ "string_format": "icu",
+ "translate_paths": [
+ {
+ "path": "*/title",
+ "key": "{*}/title",
+ "instruction": "*/note"
+ }
+ ]
+ },
+ "helloWorld": {
+ "title": "Hello world!",
+ "note": "here!"
+ }
+}
diff --git a/build/integration/pages/onboarding/js/index.js b/build/integration/pages/onboarding/js/index.js
index d77743314..998401264 100644
--- a/build/integration/pages/onboarding/js/index.js
+++ b/build/integration/pages/onboarding/js/index.js
@@ -8440,7 +8440,7 @@
} catch (e3) {
console.error("could not access handlers for %s, falling back to mock interface", opts.injectName);
}
- const fallback = new TestTransportConfig({
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
/**
* @param {import('@duckduckgo/messaging').NotificationMessage} msg
*/
diff --git a/build/integration/pages/release-notes/js/index.js b/build/integration/pages/release-notes/js/index.js
index 29a845dc1..51ee5b01b 100644
--- a/build/integration/pages/release-notes/js/index.js
+++ b/build/integration/pages/release-notes/js/index.js
@@ -2051,7 +2051,7 @@
} catch (e3) {
console.error("could not access handlers for %s, falling back to mock interface", opts.injectName);
}
- const fallback = new TestTransportConfig({
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
/**
* @param {import('@duckduckgo/messaging').NotificationMessage} msg
*/
diff --git a/build/integration/pages/special-error/js/index.js b/build/integration/pages/special-error/js/index.js
index cb6005c4b..76e9c9512 100644
--- a/build/integration/pages/special-error/js/index.js
+++ b/build/integration/pages/special-error/js/index.js
@@ -1135,7 +1135,7 @@
} catch (e3) {
console.error("could not access handlers for %s, falling back to mock interface", opts.injectName);
}
- const fallback = new TestTransportConfig({
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
/**
* @param {import('@duckduckgo/messaging').NotificationMessage} msg
*/
diff --git a/build/windows/pages/duckplayer/js/index.css b/build/windows/pages/duckplayer/js/index.css
index de2906b8f..f0299a0ba 100644
--- a/build/windows/pages/duckplayer/js/index.css
+++ b/build/windows/pages/duckplayer/js/index.css
@@ -48,7 +48,7 @@ body[data-display=app] {
padding: 16px;
}
-/* pages/duckplayer/app/components/Fallback.module.css */
+/* shared/components/Fallback/Fallback.module.css */
.Fallback_fallback {
height: 100%;
width: 100%;
diff --git a/build/windows/pages/duckplayer/js/index.js b/build/windows/pages/duckplayer/js/index.js
index 3e234ae3e..e0d2ca16c 100644
--- a/build/windows/pages/duckplayer/js/index.js
+++ b/build/windows/pages/duckplayer/js/index.js
@@ -1135,7 +1135,7 @@
} catch (e3) {
console.error("could not access handlers for %s, falling back to mock interface", opts.injectName);
}
- const fallback = new TestTransportConfig({
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
/**
* @param {import('@duckduckgo/messaging').NotificationMessage} msg
*/
@@ -2258,12 +2258,12 @@
return q2(UserValuesContext).setEnabled;
}
- // pages/duckplayer/app/components/Fallback.module.css
+ // shared/components/Fallback/Fallback.module.css
var Fallback_default = {
fallback: "Fallback_fallback"
};
- // pages/duckplayer/app/components/Fallback.jsx
+ // shared/components/Fallback/Fallback.jsx
function Fallback({ showDetails }) {
return /* @__PURE__ */ y("div", { class: Fallback_default.fallback }, /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", null, "Something went wrong!"), showDetails && /* @__PURE__ */ y("p", null, "Please check logs for a message called ", /* @__PURE__ */ y("code", null, "reportPageException"))));
}
diff --git a/build/windows/pages/new-tab/index.html b/build/windows/pages/new-tab/index.html
new file mode 100644
index 000000000..6d0102db4
--- /dev/null
+++ b/build/windows/pages/new-tab/index.html
@@ -0,0 +1,14 @@
+
+
+
+ New Tab Page
+
+
+
+
+
+
+
+
+
+
diff --git a/build/windows/pages/new-tab/js/index.css b/build/windows/pages/new-tab/js/index.css
new file mode 100644
index 000000000..55225c2b1
--- /dev/null
+++ b/build/windows/pages/new-tab/js/index.css
@@ -0,0 +1,58 @@
+/* pages/new-tab/app/styles/base.css */
+*,
+*:after,
+*:before {
+ box-sizing: border-box;
+}
+html[data-reduced-motion=true] * {
+ animation: none !important;
+ transition: none !important;
+}
+body {
+ font-family: system-ui;
+ margin: 0;
+ height: 100vh;
+ width: 100%;
+ overflow-x: hidden;
+ user-select: none;
+ -webkit-user-select: none;
+ cursor: default;
+ color: var(--theme-txt-color);
+ background: var(--theme-page-bg);
+}
+body > main {
+ width: 100%;
+}
+h1,
+h2,
+h3,
+h4 {
+ margin: 0;
+}
+button {
+ font-family: system-ui, sans-serif;
+}
+ul {
+ margin: 0;
+ padding: 0;
+}
+li {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+/* pages/new-tab/app/components/App.module.css */
+.App_layout {
+ padding-top: var(--sp-16);
+ padding-bottom: var(--sp-16);
+ max-width: 504px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* shared/components/Fallback/Fallback.module.css */
+.Fallback_fallback {
+ height: 100%;
+ width: 100%;
+}
diff --git a/build/windows/pages/new-tab/js/index.js b/build/windows/pages/new-tab/js/index.js
new file mode 100644
index 000000000..ba4c84271
--- /dev/null
+++ b/build/windows/pages/new-tab/js/index.js
@@ -0,0 +1,2078 @@
+"use strict";
+(() => {
+ // ../../node_modules/preact/dist/preact.module.js
+ var n;
+ var l;
+ var u;
+ var t;
+ var i;
+ var o;
+ var r;
+ var f;
+ var e;
+ var c = {};
+ var s = [];
+ var a = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;
+ var h = Array.isArray;
+ function v(n2, l3) {
+ for (var u3 in l3)
+ n2[u3] = l3[u3];
+ return n2;
+ }
+ function p(n2) {
+ var l3 = n2.parentNode;
+ l3 && l3.removeChild(n2);
+ }
+ function y(l3, u3, t3) {
+ var i3, o3, r3, f3 = {};
+ for (r3 in u3)
+ "key" == r3 ? i3 = u3[r3] : "ref" == r3 ? o3 = u3[r3] : f3[r3] = u3[r3];
+ if (arguments.length > 2 && (f3.children = arguments.length > 3 ? n.call(arguments, 2) : t3), "function" == typeof l3 && null != l3.defaultProps)
+ for (r3 in l3.defaultProps)
+ void 0 === f3[r3] && (f3[r3] = l3.defaultProps[r3]);
+ return d(l3, f3, i3, o3, null);
+ }
+ function d(n2, t3, i3, o3, r3) {
+ var f3 = { type: n2, props: t3, key: i3, ref: o3, __k: null, __: null, __b: 0, __e: null, __d: void 0, __c: null, constructor: void 0, __v: null == r3 ? ++u : r3, __i: -1, __u: 0 };
+ return null == r3 && null != l.vnode && l.vnode(f3), f3;
+ }
+ function g(n2) {
+ return n2.children;
+ }
+ function b(n2, l3) {
+ this.props = n2, this.context = l3;
+ }
+ function m(n2, l3) {
+ if (null == l3)
+ return n2.__ ? m(n2.__, n2.__i + 1) : null;
+ for (var u3; l3 < n2.__k.length; l3++)
+ if (null != (u3 = n2.__k[l3]) && null != u3.__e)
+ return u3.__e;
+ return "function" == typeof n2.type ? m(n2) : null;
+ }
+ function k(n2) {
+ var l3, u3;
+ if (null != (n2 = n2.__) && null != n2.__c) {
+ for (n2.__e = n2.__c.base = null, l3 = 0; l3 < n2.__k.length; l3++)
+ if (null != (u3 = n2.__k[l3]) && null != u3.__e) {
+ n2.__e = n2.__c.base = u3.__e;
+ break;
+ }
+ return k(n2);
+ }
+ }
+ function w(n2) {
+ (!n2.__d && (n2.__d = true) && i.push(n2) && !x.__r++ || o !== l.debounceRendering) && ((o = l.debounceRendering) || r)(x);
+ }
+ function x() {
+ var n2, u3, t3, o3, r3, e3, c3, s3, a3;
+ for (i.sort(f); n2 = i.shift(); )
+ n2.__d && (u3 = i.length, o3 = void 0, e3 = (r3 = (t3 = n2).__v).__e, s3 = [], a3 = [], (c3 = t3.__P) && ((o3 = v({}, r3)).__v = r3.__v + 1, l.vnode && l.vnode(o3), L(c3, o3, r3, t3.__n, void 0 !== c3.ownerSVGElement, 32 & r3.__u ? [e3] : null, s3, null == e3 ? m(r3) : e3, !!(32 & r3.__u), a3), o3.__.__k[o3.__i] = o3, M(s3, o3, a3), o3.__e != e3 && k(o3)), i.length > u3 && i.sort(f));
+ x.__r = 0;
+ }
+ function C(n2, l3, u3, t3, i3, o3, r3, f3, e3, a3, h3) {
+ var v3, p3, y2, d3, _2, g3 = t3 && t3.__k || s, b3 = l3.length;
+ for (u3.__d = e3, P(u3, l3, g3), e3 = u3.__d, v3 = 0; v3 < b3; v3++)
+ null != (y2 = u3.__k[v3]) && "boolean" != typeof y2 && "function" != typeof y2 && (p3 = -1 === y2.__i ? c : g3[y2.__i] || c, y2.__i = v3, L(n2, y2, p3, i3, o3, r3, f3, e3, a3, h3), d3 = y2.__e, y2.ref && p3.ref != y2.ref && (p3.ref && z(p3.ref, null, y2), h3.push(y2.ref, y2.__c || d3, y2)), null == _2 && null != d3 && (_2 = d3), 65536 & y2.__u || p3.__k === y2.__k ? e3 = S(y2, e3, n2) : "function" == typeof y2.type && void 0 !== y2.__d ? e3 = y2.__d : d3 && (e3 = d3.nextSibling), y2.__d = void 0, y2.__u &= -196609);
+ u3.__d = e3, u3.__e = _2;
+ }
+ function P(n2, l3, u3) {
+ var t3, i3, o3, r3, f3, e3 = l3.length, c3 = u3.length, s3 = c3, a3 = 0;
+ for (n2.__k = [], t3 = 0; t3 < e3; t3++)
+ null != (i3 = n2.__k[t3] = null == (i3 = l3[t3]) || "boolean" == typeof i3 || "function" == typeof i3 ? null : "string" == typeof i3 || "number" == typeof i3 || "bigint" == typeof i3 || i3.constructor == String ? d(null, i3, null, null, i3) : h(i3) ? d(g, { children: i3 }, null, null, null) : void 0 === i3.constructor && i3.__b > 0 ? d(i3.type, i3.props, i3.key, i3.ref ? i3.ref : null, i3.__v) : i3) ? (i3.__ = n2, i3.__b = n2.__b + 1, f3 = H(i3, u3, r3 = t3 + a3, s3), i3.__i = f3, o3 = null, -1 !== f3 && (s3--, (o3 = u3[f3]) && (o3.__u |= 131072)), null == o3 || null === o3.__v ? (-1 == f3 && a3--, "function" != typeof i3.type && (i3.__u |= 65536)) : f3 !== r3 && (f3 === r3 + 1 ? a3++ : f3 > r3 ? s3 > e3 - r3 ? a3 += f3 - r3 : a3-- : a3 = f3 < r3 && f3 == r3 - 1 ? f3 - r3 : 0, f3 !== t3 + a3 && (i3.__u |= 65536))) : (o3 = u3[t3]) && null == o3.key && o3.__e && (o3.__e == n2.__d && (n2.__d = m(o3)), N(o3, o3, false), u3[t3] = null, s3--);
+ if (s3)
+ for (t3 = 0; t3 < c3; t3++)
+ null != (o3 = u3[t3]) && 0 == (131072 & o3.__u) && (o3.__e == n2.__d && (n2.__d = m(o3)), N(o3, o3));
+ }
+ function S(n2, l3, u3) {
+ var t3, i3;
+ if ("function" == typeof n2.type) {
+ for (t3 = n2.__k, i3 = 0; t3 && i3 < t3.length; i3++)
+ t3[i3] && (t3[i3].__ = n2, l3 = S(t3[i3], l3, u3));
+ return l3;
+ }
+ return n2.__e != l3 && (u3.insertBefore(n2.__e, l3 || null), l3 = n2.__e), l3 && l3.nextSibling;
+ }
+ function H(n2, l3, u3, t3) {
+ var i3 = n2.key, o3 = n2.type, r3 = u3 - 1, f3 = u3 + 1, e3 = l3[u3];
+ if (null === e3 || e3 && i3 == e3.key && o3 === e3.type)
+ return u3;
+ if (t3 > (null != e3 && 0 == (131072 & e3.__u) ? 1 : 0))
+ for (; r3 >= 0 || f3 < l3.length; ) {
+ if (r3 >= 0) {
+ if ((e3 = l3[r3]) && 0 == (131072 & e3.__u) && i3 == e3.key && o3 === e3.type)
+ return r3;
+ r3--;
+ }
+ if (f3 < l3.length) {
+ if ((e3 = l3[f3]) && 0 == (131072 & e3.__u) && i3 == e3.key && o3 === e3.type)
+ return f3;
+ f3++;
+ }
+ }
+ return -1;
+ }
+ function I(n2, l3, u3) {
+ "-" === l3[0] ? n2.setProperty(l3, null == u3 ? "" : u3) : n2[l3] = null == u3 ? "" : "number" != typeof u3 || a.test(l3) ? u3 : u3 + "px";
+ }
+ function T(n2, l3, u3, t3, i3) {
+ var o3;
+ n:
+ if ("style" === l3)
+ if ("string" == typeof u3)
+ n2.style.cssText = u3;
+ else {
+ if ("string" == typeof t3 && (n2.style.cssText = t3 = ""), t3)
+ for (l3 in t3)
+ u3 && l3 in u3 || I(n2.style, l3, "");
+ if (u3)
+ for (l3 in u3)
+ t3 && u3[l3] === t3[l3] || I(n2.style, l3, u3[l3]);
+ }
+ else if ("o" === l3[0] && "n" === l3[1])
+ o3 = l3 !== (l3 = l3.replace(/(PointerCapture)$|Capture$/, "$1")), l3 = l3.toLowerCase() in n2 ? l3.toLowerCase().slice(2) : l3.slice(2), n2.l || (n2.l = {}), n2.l[l3 + o3] = u3, u3 ? t3 ? u3.u = t3.u : (u3.u = Date.now(), n2.addEventListener(l3, o3 ? D : A, o3)) : n2.removeEventListener(l3, o3 ? D : A, o3);
+ else {
+ if (i3)
+ l3 = l3.replace(/xlink(H|:h)/, "h").replace(/sName$/, "s");
+ else if ("width" !== l3 && "height" !== l3 && "href" !== l3 && "list" !== l3 && "form" !== l3 && "tabIndex" !== l3 && "download" !== l3 && "rowSpan" !== l3 && "colSpan" !== l3 && "role" !== l3 && l3 in n2)
+ try {
+ n2[l3] = null == u3 ? "" : u3;
+ break n;
+ } catch (n3) {
+ }
+ "function" == typeof u3 || (null == u3 || false === u3 && "-" !== l3[4] ? n2.removeAttribute(l3) : n2.setAttribute(l3, u3));
+ }
+ }
+ function A(n2) {
+ var u3 = this.l[n2.type + false];
+ if (n2.t) {
+ if (n2.t <= u3.u)
+ return;
+ } else
+ n2.t = Date.now();
+ return u3(l.event ? l.event(n2) : n2);
+ }
+ function D(n2) {
+ return this.l[n2.type + true](l.event ? l.event(n2) : n2);
+ }
+ function L(n2, u3, t3, i3, o3, r3, f3, e3, c3, s3) {
+ var a3, p3, y2, d3, _2, m3, k3, w3, x2, P2, S2, $, H2, I2, T3, A2 = u3.type;
+ if (void 0 !== u3.constructor)
+ return null;
+ 128 & t3.__u && (c3 = !!(32 & t3.__u), r3 = [e3 = u3.__e = t3.__e]), (a3 = l.__b) && a3(u3);
+ n:
+ if ("function" == typeof A2)
+ try {
+ if (w3 = u3.props, x2 = (a3 = A2.contextType) && i3[a3.__c], P2 = a3 ? x2 ? x2.props.value : a3.__ : i3, t3.__c ? k3 = (p3 = u3.__c = t3.__c).__ = p3.__E : ("prototype" in A2 && A2.prototype.render ? u3.__c = p3 = new A2(w3, P2) : (u3.__c = p3 = new b(w3, P2), p3.constructor = A2, p3.render = O), x2 && x2.sub(p3), p3.props = w3, p3.state || (p3.state = {}), p3.context = P2, p3.__n = i3, y2 = p3.__d = true, p3.__h = [], p3._sb = []), null == p3.__s && (p3.__s = p3.state), null != A2.getDerivedStateFromProps && (p3.__s == p3.state && (p3.__s = v({}, p3.__s)), v(p3.__s, A2.getDerivedStateFromProps(w3, p3.__s))), d3 = p3.props, _2 = p3.state, p3.__v = u3, y2)
+ null == A2.getDerivedStateFromProps && null != p3.componentWillMount && p3.componentWillMount(), null != p3.componentDidMount && p3.__h.push(p3.componentDidMount);
+ else {
+ if (null == A2.getDerivedStateFromProps && w3 !== d3 && null != p3.componentWillReceiveProps && p3.componentWillReceiveProps(w3, P2), !p3.__e && (null != p3.shouldComponentUpdate && false === p3.shouldComponentUpdate(w3, p3.__s, P2) || u3.__v === t3.__v)) {
+ for (u3.__v !== t3.__v && (p3.props = w3, p3.state = p3.__s, p3.__d = false), u3.__e = t3.__e, u3.__k = t3.__k, u3.__k.forEach(function(n3) {
+ n3 && (n3.__ = u3);
+ }), S2 = 0; S2 < p3._sb.length; S2++)
+ p3.__h.push(p3._sb[S2]);
+ p3._sb = [], p3.__h.length && f3.push(p3);
+ break n;
+ }
+ null != p3.componentWillUpdate && p3.componentWillUpdate(w3, p3.__s, P2), null != p3.componentDidUpdate && p3.__h.push(function() {
+ p3.componentDidUpdate(d3, _2, m3);
+ });
+ }
+ if (p3.context = P2, p3.props = w3, p3.__P = n2, p3.__e = false, $ = l.__r, H2 = 0, "prototype" in A2 && A2.prototype.render) {
+ for (p3.state = p3.__s, p3.__d = false, $ && $(u3), a3 = p3.render(p3.props, p3.state, p3.context), I2 = 0; I2 < p3._sb.length; I2++)
+ p3.__h.push(p3._sb[I2]);
+ p3._sb = [];
+ } else
+ do {
+ p3.__d = false, $ && $(u3), a3 = p3.render(p3.props, p3.state, p3.context), p3.state = p3.__s;
+ } while (p3.__d && ++H2 < 25);
+ p3.state = p3.__s, null != p3.getChildContext && (i3 = v(v({}, i3), p3.getChildContext())), y2 || null == p3.getSnapshotBeforeUpdate || (m3 = p3.getSnapshotBeforeUpdate(d3, _2)), C(n2, h(T3 = null != a3 && a3.type === g && null == a3.key ? a3.props.children : a3) ? T3 : [T3], u3, t3, i3, o3, r3, f3, e3, c3, s3), p3.base = u3.__e, u3.__u &= -161, p3.__h.length && f3.push(p3), k3 && (p3.__E = p3.__ = null);
+ } catch (n3) {
+ u3.__v = null, c3 || null != r3 ? (u3.__e = e3, u3.__u |= c3 ? 160 : 32, r3[r3.indexOf(e3)] = null) : (u3.__e = t3.__e, u3.__k = t3.__k), l.__e(n3, u3, t3);
+ }
+ else
+ null == r3 && u3.__v === t3.__v ? (u3.__k = t3.__k, u3.__e = t3.__e) : u3.__e = j(t3.__e, u3, t3, i3, o3, r3, f3, c3, s3);
+ (a3 = l.diffed) && a3(u3);
+ }
+ function M(n2, u3, t3) {
+ u3.__d = void 0;
+ for (var i3 = 0; i3 < t3.length; i3++)
+ z(t3[i3], t3[++i3], t3[++i3]);
+ l.__c && l.__c(u3, n2), n2.some(function(u4) {
+ try {
+ n2 = u4.__h, u4.__h = [], n2.some(function(n3) {
+ n3.call(u4);
+ });
+ } catch (n3) {
+ l.__e(n3, u4.__v);
+ }
+ });
+ }
+ function j(l3, u3, t3, i3, o3, r3, f3, e3, s3) {
+ var a3, v3, y2, d3, _2, g3, b3, k3 = t3.props, w3 = u3.props, x2 = u3.type;
+ if ("svg" === x2 && (o3 = true), null != r3) {
+ for (a3 = 0; a3 < r3.length; a3++)
+ if ((_2 = r3[a3]) && "setAttribute" in _2 == !!x2 && (x2 ? _2.localName === x2 : 3 === _2.nodeType)) {
+ l3 = _2, r3[a3] = null;
+ break;
+ }
+ }
+ if (null == l3) {
+ if (null === x2)
+ return document.createTextNode(w3);
+ l3 = o3 ? document.createElementNS("http://www.w3.org/2000/svg", x2) : document.createElement(x2, w3.is && w3), r3 = null, e3 = false;
+ }
+ if (null === x2)
+ k3 === w3 || e3 && l3.data === w3 || (l3.data = w3);
+ else {
+ if (r3 = r3 && n.call(l3.childNodes), k3 = t3.props || c, !e3 && null != r3)
+ for (k3 = {}, a3 = 0; a3 < l3.attributes.length; a3++)
+ k3[(_2 = l3.attributes[a3]).name] = _2.value;
+ for (a3 in k3)
+ _2 = k3[a3], "children" == a3 || ("dangerouslySetInnerHTML" == a3 ? y2 = _2 : "key" === a3 || a3 in w3 || T(l3, a3, null, _2, o3));
+ for (a3 in w3)
+ _2 = w3[a3], "children" == a3 ? d3 = _2 : "dangerouslySetInnerHTML" == a3 ? v3 = _2 : "value" == a3 ? g3 = _2 : "checked" == a3 ? b3 = _2 : "key" === a3 || e3 && "function" != typeof _2 || k3[a3] === _2 || T(l3, a3, _2, k3[a3], o3);
+ if (v3)
+ e3 || y2 && (v3.__html === y2.__html || v3.__html === l3.innerHTML) || (l3.innerHTML = v3.__html), u3.__k = [];
+ else if (y2 && (l3.innerHTML = ""), C(l3, h(d3) ? d3 : [d3], u3, t3, i3, o3 && "foreignObject" !== x2, r3, f3, r3 ? r3[0] : t3.__k && m(t3, 0), e3, s3), null != r3)
+ for (a3 = r3.length; a3--; )
+ null != r3[a3] && p(r3[a3]);
+ e3 || (a3 = "value", void 0 !== g3 && (g3 !== l3[a3] || "progress" === x2 && !g3 || "option" === x2 && g3 !== k3[a3]) && T(l3, a3, g3, k3[a3], false), a3 = "checked", void 0 !== b3 && b3 !== l3[a3] && T(l3, a3, b3, k3[a3], false));
+ }
+ return l3;
+ }
+ function z(n2, u3, t3) {
+ try {
+ "function" == typeof n2 ? n2(u3) : n2.current = u3;
+ } catch (n3) {
+ l.__e(n3, t3);
+ }
+ }
+ function N(n2, u3, t3) {
+ var i3, o3;
+ if (l.unmount && l.unmount(n2), (i3 = n2.ref) && (i3.current && i3.current !== n2.__e || z(i3, null, u3)), null != (i3 = n2.__c)) {
+ if (i3.componentWillUnmount)
+ try {
+ i3.componentWillUnmount();
+ } catch (n3) {
+ l.__e(n3, u3);
+ }
+ i3.base = i3.__P = null, n2.__c = void 0;
+ }
+ if (i3 = n2.__k)
+ for (o3 = 0; o3 < i3.length; o3++)
+ i3[o3] && N(i3[o3], u3, t3 || "function" != typeof n2.type);
+ t3 || null == n2.__e || p(n2.__e), n2.__ = n2.__e = n2.__d = void 0;
+ }
+ function O(n2, l3, u3) {
+ return this.constructor(n2, u3);
+ }
+ function q(u3, t3, i3) {
+ var o3, r3, f3, e3;
+ l.__ && l.__(u3, t3), r3 = (o3 = "function" == typeof i3) ? null : i3 && i3.__k || t3.__k, f3 = [], e3 = [], L(t3, u3 = (!o3 && i3 || t3).__k = y(g, null, [u3]), r3 || c, c, void 0 !== t3.ownerSVGElement, !o3 && i3 ? [i3] : r3 ? null : t3.firstChild ? n.call(t3.childNodes) : null, f3, !o3 && i3 ? i3 : r3 ? r3.__e : t3.firstChild, o3, e3), M(f3, u3, e3);
+ }
+ function F(n2, l3) {
+ var u3 = { __c: l3 = "__cC" + e++, __: n2, Consumer: function(n3, l4) {
+ return n3.children(l4);
+ }, Provider: function(n3) {
+ var u4, t3;
+ return this.getChildContext || (u4 = [], (t3 = {})[l3] = this, this.getChildContext = function() {
+ return t3;
+ }, this.shouldComponentUpdate = function(n4) {
+ this.props.value !== n4.value && u4.some(function(n5) {
+ n5.__e = true, w(n5);
+ });
+ }, this.sub = function(n4) {
+ u4.push(n4);
+ var l4 = n4.componentWillUnmount;
+ n4.componentWillUnmount = function() {
+ u4.splice(u4.indexOf(n4), 1), l4 && l4.call(n4);
+ };
+ }), n3.children;
+ } };
+ return u3.Provider.__ = u3.Consumer.contextType = u3;
+ }
+ n = s.slice, l = { __e: function(n2, l3, u3, t3) {
+ for (var i3, o3, r3; l3 = l3.__; )
+ if ((i3 = l3.__c) && !i3.__)
+ try {
+ if ((o3 = i3.constructor) && null != o3.getDerivedStateFromError && (i3.setState(o3.getDerivedStateFromError(n2)), r3 = i3.__d), null != i3.componentDidCatch && (i3.componentDidCatch(n2, t3 || {}), r3 = i3.__d), r3)
+ return i3.__E = i3;
+ } catch (l4) {
+ n2 = l4;
+ }
+ throw n2;
+ } }, u = 0, t = function(n2) {
+ return null != n2 && null == n2.constructor;
+ }, b.prototype.setState = function(n2, l3) {
+ var u3;
+ u3 = null != this.__s && this.__s !== this.state ? this.__s : this.__s = v({}, this.state), "function" == typeof n2 && (n2 = n2(v({}, u3), this.props)), n2 && v(u3, n2), null != n2 && this.__v && (l3 && this._sb.push(l3), w(this));
+ }, b.prototype.forceUpdate = function(n2) {
+ this.__v && (this.__e = true, n2 && this.__h.push(n2), w(this));
+ }, b.prototype.render = g, i = [], r = "function" == typeof Promise ? Promise.prototype.then.bind(Promise.resolve()) : setTimeout, f = function(n2, l3) {
+ return n2.__v.__b - l3.__v.__b;
+ }, x.__r = 0, e = 0;
+
+ // ../../node_modules/preact/devtools/dist/devtools.module.js
+ "undefined" != typeof window && window.__PREACT_DEVTOOLS__ && window.__PREACT_DEVTOOLS__.attachPreact("10.19.3", l, { Fragment: g, Component: b });
+
+ // pages/new-tab/app/components/App.module.css
+ var App_default = {
+ layout: "App_layout"
+ };
+
+ // pages/new-tab/app/components/App.js
+ function App({ children }) {
+ return /* @__PURE__ */ y("div", { className: App_default.layout }, children);
+ }
+
+ // ../../node_modules/preact/hooks/dist/hooks.module.js
+ var t2;
+ var r2;
+ var u2;
+ var i2;
+ var o2 = 0;
+ var f2 = [];
+ var c2 = [];
+ var e2 = l.__b;
+ var a2 = l.__r;
+ var v2 = l.diffed;
+ var l2 = l.__c;
+ var m2 = l.unmount;
+ function d2(t3, u3) {
+ l.__h && l.__h(r2, t3, o2 || u3), o2 = 0;
+ var i3 = r2.__H || (r2.__H = { __: [], __h: [] });
+ return t3 >= i3.__.length && i3.__.push({ __V: c2 }), i3.__[t3];
+ }
+ function h2(n2) {
+ return o2 = 1, s2(B, n2);
+ }
+ function s2(n2, u3, i3) {
+ var o3 = d2(t2++, 2);
+ if (o3.t = n2, !o3.__c && (o3.__ = [i3 ? i3(u3) : B(void 0, u3), function(n3) {
+ var t3 = o3.__N ? o3.__N[0] : o3.__[0], r3 = o3.t(t3, n3);
+ t3 !== r3 && (o3.__N = [r3, o3.__[1]], o3.__c.setState({}));
+ }], o3.__c = r2, !r2.u)) {
+ var f3 = function(n3, t3, r3) {
+ if (!o3.__c.__H)
+ return true;
+ var u4 = o3.__c.__H.__.filter(function(n4) {
+ return n4.__c;
+ });
+ if (u4.every(function(n4) {
+ return !n4.__N;
+ }))
+ return !c3 || c3.call(this, n3, t3, r3);
+ var i4 = false;
+ return u4.forEach(function(n4) {
+ if (n4.__N) {
+ var t4 = n4.__[0];
+ n4.__ = n4.__N, n4.__N = void 0, t4 !== n4.__[0] && (i4 = true);
+ }
+ }), !(!i4 && o3.__c.props === n3) && (!c3 || c3.call(this, n3, t3, r3));
+ };
+ r2.u = true;
+ var c3 = r2.shouldComponentUpdate, e3 = r2.componentWillUpdate;
+ r2.componentWillUpdate = function(n3, t3, r3) {
+ if (this.__e) {
+ var u4 = c3;
+ c3 = void 0, f3(n3, t3, r3), c3 = u4;
+ }
+ e3 && e3.call(this, n3, t3, r3);
+ }, r2.shouldComponentUpdate = f3;
+ }
+ return o3.__N || o3.__;
+ }
+ function p2(u3, i3) {
+ var o3 = d2(t2++, 3);
+ !l.__s && z2(o3.__H, i3) && (o3.__ = u3, o3.i = i3, r2.__H.__h.push(o3));
+ }
+ function F2(n2, r3) {
+ var u3 = d2(t2++, 7);
+ return z2(u3.__H, r3) ? (u3.__V = n2(), u3.i = r3, u3.__h = n2, u3.__V) : u3.__;
+ }
+ function T2(n2, t3) {
+ return o2 = 8, F2(function() {
+ return n2;
+ }, t3);
+ }
+ function q2(n2) {
+ var u3 = r2.context[n2.__c], i3 = d2(t2++, 9);
+ return i3.c = n2, u3 ? (null == i3.__ && (i3.__ = true, u3.sub(r2)), u3.props.value) : n2.__;
+ }
+ function b2() {
+ for (var t3; t3 = f2.shift(); )
+ if (t3.__P && t3.__H)
+ try {
+ t3.__H.__h.forEach(k2), t3.__H.__h.forEach(w2), t3.__H.__h = [];
+ } catch (r3) {
+ t3.__H.__h = [], l.__e(r3, t3.__v);
+ }
+ }
+ l.__b = function(n2) {
+ r2 = null, e2 && e2(n2);
+ }, l.__r = function(n2) {
+ a2 && a2(n2), t2 = 0;
+ var i3 = (r2 = n2.__c).__H;
+ i3 && (u2 === r2 ? (i3.__h = [], r2.__h = [], i3.__.forEach(function(n3) {
+ n3.__N && (n3.__ = n3.__N), n3.__V = c2, n3.__N = n3.i = void 0;
+ })) : (i3.__h.forEach(k2), i3.__h.forEach(w2), i3.__h = [], t2 = 0)), u2 = r2;
+ }, l.diffed = function(t3) {
+ v2 && v2(t3);
+ var o3 = t3.__c;
+ o3 && o3.__H && (o3.__H.__h.length && (1 !== f2.push(o3) && i2 === l.requestAnimationFrame || ((i2 = l.requestAnimationFrame) || j2)(b2)), o3.__H.__.forEach(function(n2) {
+ n2.i && (n2.__H = n2.i), n2.__V !== c2 && (n2.__ = n2.__V), n2.i = void 0, n2.__V = c2;
+ })), u2 = r2 = null;
+ }, l.__c = function(t3, r3) {
+ r3.some(function(t4) {
+ try {
+ t4.__h.forEach(k2), t4.__h = t4.__h.filter(function(n2) {
+ return !n2.__ || w2(n2);
+ });
+ } catch (u3) {
+ r3.some(function(n2) {
+ n2.__h && (n2.__h = []);
+ }), r3 = [], l.__e(u3, t4.__v);
+ }
+ }), l2 && l2(t3, r3);
+ }, l.unmount = function(t3) {
+ m2 && m2(t3);
+ var r3, u3 = t3.__c;
+ u3 && u3.__H && (u3.__H.__.forEach(function(n2) {
+ try {
+ k2(n2);
+ } catch (n3) {
+ r3 = n3;
+ }
+ }), u3.__H = void 0, r3 && l.__e(r3, u3.__v));
+ };
+ var g2 = "function" == typeof requestAnimationFrame;
+ function j2(n2) {
+ var t3, r3 = function() {
+ clearTimeout(u3), g2 && cancelAnimationFrame(t3), setTimeout(n2);
+ }, u3 = setTimeout(r3, 100);
+ g2 && (t3 = requestAnimationFrame(r3));
+ }
+ function k2(n2) {
+ var t3 = r2, u3 = n2.__c;
+ "function" == typeof u3 && (n2.__c = void 0, u3()), r2 = t3;
+ }
+ function w2(n2) {
+ var t3 = r2;
+ n2.__c = n2.__(), r2 = t3;
+ }
+ function z2(n2, t3) {
+ return !n2 || n2.length !== t3.length || t3.some(function(t4, r3) {
+ return t4 !== n2[r3];
+ });
+ }
+ function B(n2, t3) {
+ return "function" == typeof t3 ? t3(n2) : t3;
+ }
+
+ // shared/components/EnvironmentProvider.js
+ var EnvironmentContext = F({
+ isReducedMotion: false,
+ isDarkMode: false,
+ debugState: false,
+ injectName: (
+ /** @type {import('../environment').Environment['injectName']} */
+ "windows"
+ ),
+ willThrow: false
+ });
+ var THEME_QUERY = "(prefers-color-scheme: dark)";
+ var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
+ function EnvironmentProvider({ children, debugState, willThrow = false, injectName = "windows" }) {
+ const [theme, setTheme] = h2(window.matchMedia(THEME_QUERY).matches ? "dark" : "light");
+ const [isReducedMotion, setReducedMotion] = h2(window.matchMedia(REDUCED_MOTION_QUERY).matches);
+ p2(() => {
+ const mediaQueryList = window.matchMedia(THEME_QUERY);
+ const listener = (e3) => setTheme(e3.matches ? "dark" : "light");
+ mediaQueryList.addEventListener("change", listener);
+ return () => mediaQueryList.removeEventListener("change", listener);
+ }, []);
+ p2(() => {
+ const mediaQueryList = window.matchMedia(REDUCED_MOTION_QUERY);
+ const listener = (e3) => setter(e3.matches);
+ mediaQueryList.addEventListener("change", listener);
+ setter(mediaQueryList.matches);
+ function setter(value) {
+ document.documentElement.dataset.reducedMotion = String(value);
+ setReducedMotion(value);
+ }
+ window.addEventListener("toggle-reduced-motion", () => {
+ setter(true);
+ });
+ return () => mediaQueryList.removeEventListener("change", listener);
+ }, []);
+ return /* @__PURE__ */ y(EnvironmentContext.Provider, { value: {
+ isReducedMotion,
+ debugState,
+ isDarkMode: theme === "dark",
+ injectName,
+ willThrow
+ } }, children);
+ }
+ function UpdateEnvironment({ search }) {
+ p2(() => {
+ const params = new URLSearchParams(search);
+ if (params.has("reduced-motion")) {
+ setTimeout(() => {
+ window.dispatchEvent(new CustomEvent("toggle-reduced-motion"));
+ }, 0);
+ }
+ }, [search]);
+ return null;
+ }
+
+ // shared/components/Fallback/Fallback.module.css
+ var Fallback_default = {
+ fallback: "Fallback_fallback"
+ };
+
+ // shared/components/Fallback/Fallback.jsx
+ function Fallback({ showDetails }) {
+ return /* @__PURE__ */ y("div", { class: Fallback_default.fallback }, /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", null, "Something went wrong!"), showDetails && /* @__PURE__ */ y("p", null, "Please check logs for a message called ", /* @__PURE__ */ y("code", null, "reportPageException"))));
+ }
+
+ // shared/components/ErrorBoundary.js
+ var ErrorBoundary = class extends b {
+ /**
+ * @param {{didCatch: (params: {error: Error; info: any}) => void}} props
+ */
+ constructor(props) {
+ super(props);
+ this.state = { hasError: false };
+ }
+ static getDerivedStateFromError() {
+ return { hasError: true };
+ }
+ componentDidCatch(error, info) {
+ console.error(error);
+ console.log(info);
+ this.props.didCatch({ error, info });
+ }
+ render() {
+ if (this.state.hasError) {
+ return this.props.fallback;
+ }
+ return this.props.children;
+ }
+ };
+
+ // pages/new-tab/app/settings.provider.js
+ var SettingsContext = F(
+ /** @type {{settings: import("./settings.js").Settings}} */
+ {}
+ );
+ function SettingsProvider({ settings, children }) {
+ return /* @__PURE__ */ y(SettingsContext.Provider, { value: { settings } }, children);
+ }
+
+ // shared/translations.js
+ function apply(subject, replacements, textLength = 1) {
+ if (typeof subject !== "string" || subject.length === 0)
+ return "";
+ let out = subject;
+ if (replacements) {
+ for (let [name, value] of Object.entries(replacements)) {
+ if (typeof value !== "string")
+ value = "";
+ out = out.replaceAll(`{${name}}`, value);
+ }
+ }
+ if (textLength !== 1 && textLength > 0 && textLength <= 2) {
+ const targetLen = Math.ceil(out.length * textLength);
+ const target = Math.ceil(textLength);
+ const combined = out.repeat(target);
+ return combined.slice(0, targetLen);
+ }
+ return out;
+ }
+
+ // shared/components/TranslationsProvider.js
+ var TranslationContext = F({
+ /** @type {LocalTranslationFn} */
+ t: () => {
+ throw new Error("must implement");
+ }
+ });
+ function TranslationProvider({ children, translationObject, fallback, textLength = 1 }) {
+ function t3(inputKey, replacements) {
+ const subject = translationObject?.[inputKey]?.title || fallback?.[inputKey]?.title;
+ return apply(subject, replacements, textLength);
+ }
+ return /* @__PURE__ */ y(TranslationContext.Provider, { value: { t: t3 } }, children);
+ }
+
+ // pages/new-tab/src/locales/en/newtab.json
+ var newtab_default = {
+ smartling: {
+ string_format: "icu",
+ translate_paths: [
+ {
+ path: "*/title",
+ key: "{*}/title",
+ instruction: "*/note"
+ }
+ ]
+ },
+ helloWorld: {
+ title: "Hello world!",
+ note: "here!"
+ }
+ };
+
+ // pages/new-tab/app/types.js
+ var MessagingContext = F(
+ /** @type {import("../src/js/index.js").NewTabPage} */
+ {}
+ );
+
+ // pages/new-tab/app/widget-list/widget-config.js
+ var WidgetConfigAPI = class {
+ /**
+ * @param {import("../../src/js/index.js").NewTabPage} ntp - The internal data feed, expected to have a `subscribe` method.
+ * @param {WidgetConfig} initialData - Initial widget configuration.
+ */
+ constructor(ntp, initialData) {
+ this.ntp = ntp;
+ this.manager = new WidgetConfigManager(ntp, initialData);
+ }
+ /**
+ * @param {(data: WidgetConfig) => void} cb
+ * @return {() => void} - call this returned method to dispose of the subscription
+ */
+ onUpdate(cb) {
+ const controller = new AbortController();
+ this.manager.eventTarget.addEventListener(this.manager.DATA_CHANGE_EVT, (evt) => {
+ cb(evt.detail);
+ }, { signal: controller.signal });
+ return () => controller.abort();
+ }
+ /**
+ * @param {string} id
+ */
+ show(id) {
+ const next = this.manager.inMemoryData.map((w3) => {
+ if (w3.id === id)
+ return { ...w3, visibility: (
+ /** @type {const} */
+ "visible"
+ ) };
+ return w3;
+ });
+ this.manager.update(next);
+ }
+ /**
+ * @param {string} id
+ */
+ hide(id) {
+ const next = this.manager.inMemoryData.map((w3) => {
+ if (w3.id === id)
+ return { ...w3, visibility: (
+ /** @type {const} */
+ "hidden"
+ ) };
+ return w3;
+ });
+ this.manager.update(next);
+ }
+ };
+ var WidgetConfigManager = class {
+ debounceTimer = null;
+ eventTarget = new EventTarget();
+ DATA_CHANGE_EVT = "dataChanged";
+ DEBOUNCE_TIME_MS = 200;
+ /**
+ * @param {import("../../src/js/index.js").NewTabPage} ntp - The internal data feed, expected to have a `subscribe` method.
+ * @param {WidgetConfig} initialData - Initial widget configuration.
+ */
+ constructor(ntp, initialData) {
+ this.ntp = ntp;
+ this.inMemoryData = initialData;
+ this.setupSubscriptionStream();
+ }
+ /**
+ * Sets up the subscription stream from the data feed, which behaves like an input source
+ * that updates in-memory data but does not trigger persistence.
+ */
+ setupSubscriptionStream() {
+ this.ntp.messaging.subscribe("onWidgetConfigUpdated", (newData) => {
+ this.updateInMemoryData(newData.widgetConfig, "subscription");
+ });
+ }
+ /**
+ * Manually trigger an update, for example, from a UI element.
+ * @param {WidgetConfig} newData - The new widget configuration to update.
+ */
+ update(newData) {
+ this.updateInMemoryData(newData, "manual");
+ }
+ /**
+ * Updates the in-memory data and triggers persistence if the source is a manual update.
+ * This method centralizes all state updates.
+ * @param {WidgetConfig} newData - The new widget configuration to update in memory.
+ * @param {'manual' | 'subscription'} source - The source of the update. Either 'subscription' or 'manual'.
+ */
+ updateInMemoryData(newData, source) {
+ this.log(`Updating in-memory data from '${source}:'`, newData);
+ this.inMemoryData = structuredClone(newData);
+ this.broadcastChange();
+ if (source === "manual") {
+ this.clearDebounceTimer();
+ this.debounceTimer = /** @type {any} */
+ setTimeout(() => {
+ this.persist();
+ }, this.DEBOUNCE_TIME_MS);
+ }
+ }
+ /**
+ * Clears the debounce timer if it exists, simulating the switchMap behavior.
+ */
+ clearDebounceTimer() {
+ if (this.debounceTimer) {
+ this.log("Clearing previous debounce timer.");
+ clearTimeout(this.debounceTimer);
+ this.debounceTimer = null;
+ }
+ }
+ /**
+ * Broadcasts the current state to external subscribers using EventTarget.
+ */
+ broadcastChange() {
+ this.log("Broadcasting change to external listeners:", this.inMemoryData);
+ this.eventTarget.dispatchEvent(new CustomEvent(this.DATA_CHANGE_EVT, { detail: this.inMemoryData }));
+ }
+ /**
+ * Persists the current in-memory widget configuration state to the internal data feed.
+ */
+ persist() {
+ this.log("will persist data to backend:", this.inMemoryData);
+ this.ntp.messaging.notify("setWidgetConfig", { widgetConfig: this.inMemoryData });
+ }
+ /**
+ * Logs messages to the console for tracing internal state updates.
+ * @param {string} message - The message to log.
+ * @param {any} data - The associated data for the message.
+ */
+ log(message, data = null) {
+ console.log(`[WidgetConfigManager] ${message}`, data);
+ }
+ };
+
+ // pages/new-tab/app/widget-list/widget-config.provider.js
+ var WidgetConfigContext = F({
+ /** @type {WidgetList} */
+ widgets: [],
+ /** @type {WidgetConfig} */
+ widgetConfig: [],
+ /** @type {(name:string) => void} */
+ hide: () => {
+ },
+ /** @type {(name:string) => void} */
+ show: () => {
+ }
+ });
+ var WidgetConfigDispatchContext = F({
+ dispatch: null
+ });
+ function WidgetConfigProvider(props) {
+ const [data, setData] = h2(props.widgetConfig);
+ p2(() => {
+ const unsub = props.api.onUpdate((widgetConfig) => {
+ setData(widgetConfig);
+ });
+ return () => unsub();
+ }, [props.api]);
+ function hide(name) {
+ console.log("will hide", name);
+ props.api.hide(name);
+ }
+ function show(name) {
+ console.log("will show", name);
+ props.api.show(name);
+ }
+ return /* @__PURE__ */ y(WidgetConfigContext.Provider, { value: {
+ // this field is static for the lifespan of the page
+ widgets: props.widgets,
+ // this will be updated via subscriptions
+ widgetConfig: data,
+ hide,
+ show
+ } }, props.children);
+ }
+ var WidgetVisibilityContext = F({
+ visibility: (
+ /** @type {WidgetConfigItem['visibility']} */
+ "visible"
+ ),
+ id: (
+ /** @type {WidgetConfigItem['id']} */
+ ""
+ ),
+ toggle: () => {
+ }
+ });
+ function useVisibility() {
+ return q2(WidgetVisibilityContext);
+ }
+ function WidgetVisibilityProvider(props) {
+ const { widgetConfig, show, hide } = q2(WidgetConfigContext);
+ const toggle = T2(() => {
+ const matching = widgetConfig.find((x2) => x2.id === props.id);
+ if (matching?.visibility === "visible") {
+ hide(props.id);
+ } else {
+ show(props.id);
+ }
+ }, [props.id, widgetConfig]);
+ return /* @__PURE__ */ y(WidgetVisibilityContext.Provider, { value: {
+ visibility: props.visibility,
+ id: props.id,
+ toggle
+ } }, props.children);
+ }
+
+ // pages/new-tab/app/widget-list/WidgetList.js
+ var widgetMap = {
+ favorites: () => /* @__PURE__ */ y(Favorites, null),
+ privacyStats: () => /* @__PURE__ */ y(PrivacyStats, null)
+ };
+ function WidgetList() {
+ const { widgets, widgetConfig } = q2(WidgetConfigContext);
+ return /* @__PURE__ */ y("div", null, widgets.map((widget) => {
+ const matchingConfig = widgetConfig.find((item) => item.id === widget.id);
+ if (!matchingConfig) {
+ console.warn("missing config for widget: ", widget);
+ return null;
+ }
+ return /* @__PURE__ */ y(g, { key: widget.id }, /* @__PURE__ */ y(
+ WidgetVisibilityProvider,
+ {
+ visibility: matchingConfig.visibility,
+ id: matchingConfig.id
+ },
+ widgetMap[widget.id]?.()
+ ));
+ }));
+ }
+ function Favorites() {
+ const { visibility, id, toggle } = useVisibility();
+ return /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", { style: { opacity: visibility === "visible" ? "1" : "0.2" } }, "Favorites Component"), /* @__PURE__ */ y("code", null, /* @__PURE__ */ y("b", null, id, " visibility: ", visibility)), " ", /* @__PURE__ */ y("button", { type: "button", onClick: toggle }, "Toggle Favorites"));
+ }
+ function PrivacyStats() {
+ const { visibility, id, toggle } = useVisibility();
+ return /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", { style: { opacity: visibility === "visible" ? "1" : "0.2" } }, "Privacy Stats Component"), /* @__PURE__ */ y("code", null, /* @__PURE__ */ y("b", null, id, " visibility: ", visibility)), " ", /* @__PURE__ */ y("button", { type: "button", onClick: toggle }, "Toggle Privacy Stats"));
+ }
+
+ // pages/new-tab/app/settings.js
+ var Settings = class _Settings {
+ /**
+ * @param {object} params
+ * @param {{name: ImportMeta['platform']}} [params.platform]
+ */
+ constructor({
+ platform = { name: "windows" }
+ }) {
+ this.platform = platform;
+ }
+ withPlatformName(name) {
+ const valid = ["windows", "macos", "ios", "android"];
+ if (valid.includes(
+ /** @type {any} */
+ name
+ )) {
+ return new _Settings({
+ ...this,
+ platform: { name }
+ });
+ }
+ return this;
+ }
+ };
+
+ // pages/new-tab/app/index.js
+ async function init(messaging2, baseEnvironment2) {
+ const init2 = await messaging2.init();
+ if (!Array.isArray(init2.widgets)) {
+ throw new Error("missing critical initialSetup.widgets array");
+ }
+ if (!Array.isArray(init2.widgetConfig)) {
+ throw new Error("missing critical initialSetup.widgetConfig array");
+ }
+ const widgetConfigAPI = new WidgetConfigAPI(messaging2, init2.widgetConfig);
+ const environment = baseEnvironment2.withEnv(init2.env).withLocale(init2.locale).withLocale(baseEnvironment2.urlParams.get("locale")).withTextLength(baseEnvironment2.urlParams.get("textLength")).withDisplay(baseEnvironment2.urlParams.get("display"));
+ console.log("environment:", environment);
+ console.log("locale:", environment.locale);
+ const strings = environment.locale === "en" ? newtab_default : await fetch(`./locales/${environment.locale}/new-tab.json`).then((x2) => x2.json()).catch((e3) => {
+ console.error("Could not load locale", environment.locale, e3);
+ return newtab_default;
+ });
+ const settings = new Settings({}).withPlatformName(baseEnvironment2.injectName).withPlatformName(init2.platform?.name).withPlatformName(baseEnvironment2.urlParams.get("platform"));
+ const didCatch = (error) => {
+ const message = error?.message || error?.error || "unknown";
+ messaging2.reportPageException({ message });
+ };
+ const root = document.querySelector("#app");
+ if (!root)
+ throw new Error("could not render, root element missing");
+ q(
+ /* @__PURE__ */ y(
+ EnvironmentProvider,
+ {
+ debugState: environment.debugState,
+ injectName: environment.injectName,
+ willThrow: environment.willThrow
+ },
+ /* @__PURE__ */ y(ErrorBoundary, { didCatch, fallback: /* @__PURE__ */ y(Fallback, { showDetails: environment.env === "development" }) }, /* @__PURE__ */ y(UpdateEnvironment, { search: window.location.search }), /* @__PURE__ */ y(MessagingContext.Provider, { value: messaging2 }, /* @__PURE__ */ y(SettingsProvider, { settings }, /* @__PURE__ */ y(TranslationProvider, { translationObject: strings, fallback: strings, textLength: environment.textLength }, /* @__PURE__ */ y(WidgetConfigProvider, { api: widgetConfigAPI, widgetConfig: init2.widgetConfig, widgets: init2.widgets }, /* @__PURE__ */ y(App, null, /* @__PURE__ */ y(WidgetList, null)))))))
+ ),
+ root
+ );
+ }
+
+ // ../messaging/lib/windows.js
+ var WindowsMessagingTransport = class {
+ /**
+ * @param {WindowsMessagingConfig} config
+ * @param {import('../index.js').MessagingContext} messagingContext
+ * @internal
+ */
+ constructor(config, messagingContext) {
+ this.messagingContext = messagingContext;
+ this.config = config;
+ this.globals = {
+ window,
+ JSONparse: window.JSON.parse,
+ JSONstringify: window.JSON.stringify,
+ Promise: window.Promise,
+ Error: window.Error,
+ String: window.String
+ };
+ for (const [methodName, fn] of Object.entries(this.config.methods)) {
+ if (typeof fn !== "function") {
+ throw new Error("cannot create WindowsMessagingTransport, missing the method: " + methodName);
+ }
+ }
+ }
+ /**
+ * @param {import('../index.js').NotificationMessage} msg
+ */
+ notify(msg) {
+ const data = this.globals.JSONparse(this.globals.JSONstringify(msg.params || {}));
+ const notification = WindowsNotification.fromNotification(msg, data);
+ this.config.methods.postMessage(notification);
+ }
+ /**
+ * @param {import('../index.js').RequestMessage} msg
+ * @param {{signal?: AbortSignal}} opts
+ * @return {Promise}
+ */
+ request(msg, opts = {}) {
+ const data = this.globals.JSONparse(this.globals.JSONstringify(msg.params || {}));
+ const outgoing = WindowsRequestMessage.fromRequest(msg, data);
+ this.config.methods.postMessage(outgoing);
+ const comparator = (eventData) => {
+ return eventData.featureName === msg.featureName && eventData.context === msg.context && eventData.id === msg.id;
+ };
+ function isMessageResponse(data2) {
+ if ("result" in data2)
+ return true;
+ if ("error" in data2)
+ return true;
+ return false;
+ }
+ return new this.globals.Promise((resolve, reject) => {
+ try {
+ this._subscribe(comparator, opts, (value, unsubscribe) => {
+ unsubscribe();
+ if (!isMessageResponse(value)) {
+ console.warn("unknown response type", value);
+ return reject(new this.globals.Error("unknown response"));
+ }
+ if (value.result) {
+ return resolve(value.result);
+ }
+ const message = this.globals.String(value.error?.message || "unknown error");
+ reject(new this.globals.Error(message));
+ });
+ } catch (e3) {
+ reject(e3);
+ }
+ });
+ }
+ /**
+ * @param {import('../index.js').Subscription} msg
+ * @param {(value: unknown | undefined) => void} callback
+ */
+ subscribe(msg, callback) {
+ const comparator = (eventData) => {
+ return eventData.featureName === msg.featureName && eventData.context === msg.context && eventData.subscriptionName === msg.subscriptionName;
+ };
+ const cb = (eventData) => {
+ return callback(eventData.params);
+ };
+ return this._subscribe(comparator, {}, cb);
+ }
+ /**
+ * @typedef {import('../index.js').MessageResponse | import('../index.js').SubscriptionEvent} Incoming
+ */
+ /**
+ * @param {(eventData: any) => boolean} comparator
+ * @param {{signal?: AbortSignal}} options
+ * @param {(value: Incoming, unsubscribe: (()=>void)) => void} callback
+ * @internal
+ */
+ _subscribe(comparator, options, callback) {
+ if (options?.signal?.aborted) {
+ throw new DOMException("Aborted", "AbortError");
+ }
+ let teardown;
+ const idHandler = (event) => {
+ if (this.messagingContext.env === "production") {
+ if (event.origin !== null && event.origin !== void 0) {
+ console.warn("ignoring because evt.origin is not `null` or `undefined`");
+ return;
+ }
+ }
+ if (!event.data) {
+ console.warn("data absent from message");
+ return;
+ }
+ if (comparator(event.data)) {
+ if (!teardown)
+ throw new Error("unreachable");
+ callback(event.data, teardown);
+ }
+ };
+ const abortHandler = () => {
+ teardown?.();
+ throw new DOMException("Aborted", "AbortError");
+ };
+ this.config.methods.addEventListener("message", idHandler);
+ options?.signal?.addEventListener("abort", abortHandler);
+ teardown = () => {
+ this.config.methods.removeEventListener("message", idHandler);
+ options?.signal?.removeEventListener("abort", abortHandler);
+ };
+ return () => {
+ teardown?.();
+ };
+ }
+ };
+ var WindowsMessagingConfig = class {
+ /**
+ * @param {object} params
+ * @param {WindowsInteropMethods} params.methods
+ * @internal
+ */
+ constructor(params) {
+ this.methods = params.methods;
+ this.platform = "windows";
+ }
+ };
+ var WindowsNotification = class {
+ /**
+ * @param {object} params
+ * @param {string} params.Feature
+ * @param {string} params.SubFeatureName
+ * @param {string} params.Name
+ * @param {Record} [params.Data]
+ * @internal
+ */
+ constructor(params) {
+ this.Feature = params.Feature;
+ this.SubFeatureName = params.SubFeatureName;
+ this.Name = params.Name;
+ this.Data = params.Data;
+ }
+ /**
+ * Helper to convert a {@link NotificationMessage} to a format that Windows can support
+ * @param {NotificationMessage} notification
+ * @returns {WindowsNotification}
+ */
+ static fromNotification(notification, data) {
+ const output = {
+ Data: data,
+ Feature: notification.context,
+ SubFeatureName: notification.featureName,
+ Name: notification.method
+ };
+ return output;
+ }
+ };
+ var WindowsRequestMessage = class {
+ /**
+ * @param {object} params
+ * @param {string} params.Feature
+ * @param {string} params.SubFeatureName
+ * @param {string} params.Name
+ * @param {Record} [params.Data]
+ * @param {string} [params.Id]
+ * @internal
+ */
+ constructor(params) {
+ this.Feature = params.Feature;
+ this.SubFeatureName = params.SubFeatureName;
+ this.Name = params.Name;
+ this.Data = params.Data;
+ this.Id = params.Id;
+ }
+ /**
+ * Helper to convert a {@link RequestMessage} to a format that Windows can support
+ * @param {RequestMessage} msg
+ * @param {Record} data
+ * @returns {WindowsRequestMessage}
+ */
+ static fromRequest(msg, data) {
+ const output = {
+ Data: data,
+ Feature: msg.context,
+ SubFeatureName: msg.featureName,
+ Name: msg.method,
+ Id: msg.id
+ };
+ return output;
+ }
+ };
+
+ // ../messaging/schema.js
+ var RequestMessage = class {
+ /**
+ * @param {object} params
+ * @param {string} params.context
+ * @param {string} params.featureName
+ * @param {string} params.method
+ * @param {string} params.id
+ * @param {Record} [params.params]
+ * @internal
+ */
+ constructor(params) {
+ this.context = params.context;
+ this.featureName = params.featureName;
+ this.method = params.method;
+ this.id = params.id;
+ this.params = params.params;
+ }
+ };
+ var NotificationMessage = class {
+ /**
+ * @param {object} params
+ * @param {string} params.context
+ * @param {string} params.featureName
+ * @param {string} params.method
+ * @param {Record} [params.params]
+ * @internal
+ */
+ constructor(params) {
+ this.context = params.context;
+ this.featureName = params.featureName;
+ this.method = params.method;
+ this.params = params.params;
+ }
+ };
+ var Subscription = class {
+ /**
+ * @param {object} params
+ * @param {string} params.context
+ * @param {string} params.featureName
+ * @param {string} params.subscriptionName
+ * @internal
+ */
+ constructor(params) {
+ this.context = params.context;
+ this.featureName = params.featureName;
+ this.subscriptionName = params.subscriptionName;
+ }
+ };
+ function isResponseFor(request, data) {
+ if ("result" in data) {
+ return data.featureName === request.featureName && data.context === request.context && data.id === request.id;
+ }
+ if ("error" in data) {
+ if ("message" in data.error) {
+ return true;
+ }
+ }
+ return false;
+ }
+ function isSubscriptionEventFor(sub, data) {
+ if ("subscriptionName" in data) {
+ return data.featureName === sub.featureName && data.context === sub.context && data.subscriptionName === sub.subscriptionName;
+ }
+ return false;
+ }
+
+ // ../messaging/lib/webkit.js
+ var WebkitMessagingTransport = class {
+ /**
+ * @param {WebkitMessagingConfig} config
+ * @param {import('../index.js').MessagingContext} messagingContext
+ */
+ constructor(config, messagingContext) {
+ this.messagingContext = messagingContext;
+ this.config = config;
+ this.globals = captureGlobals();
+ if (!this.config.hasModernWebkitAPI) {
+ this.captureWebkitHandlers(this.config.webkitMessageHandlerNames);
+ }
+ }
+ /**
+ * Sends message to the webkit layer (fire and forget)
+ * @param {String} handler
+ * @param {*} data
+ * @internal
+ */
+ wkSend(handler, data = {}) {
+ if (!(handler in this.globals.window.webkit.messageHandlers)) {
+ throw new MissingHandler(`Missing webkit handler: '${handler}'`, handler);
+ }
+ if (!this.config.hasModernWebkitAPI) {
+ const outgoing = {
+ ...data,
+ messageHandling: {
+ ...data.messageHandling,
+ secret: this.config.secret
+ }
+ };
+ if (!(handler in this.globals.capturedWebkitHandlers)) {
+ throw new MissingHandler(`cannot continue, method ${handler} not captured on macos < 11`, handler);
+ } else {
+ return this.globals.capturedWebkitHandlers[handler](outgoing);
+ }
+ }
+ return this.globals.window.webkit.messageHandlers[handler].postMessage?.(data);
+ }
+ /**
+ * Sends message to the webkit layer and waits for the specified response
+ * @param {String} handler
+ * @param {import('../index.js').RequestMessage} data
+ * @returns {Promise<*>}
+ * @internal
+ */
+ async wkSendAndWait(handler, data) {
+ if (this.config.hasModernWebkitAPI) {
+ const response = await this.wkSend(handler, data);
+ return this.globals.JSONparse(response || "{}");
+ }
+ try {
+ const randMethodName = this.createRandMethodName();
+ const key = await this.createRandKey();
+ const iv = this.createRandIv();
+ const {
+ ciphertext,
+ tag
+ } = await new this.globals.Promise((resolve) => {
+ this.generateRandomMethod(randMethodName, resolve);
+ data.messageHandling = new SecureMessagingParams({
+ methodName: randMethodName,
+ secret: this.config.secret,
+ key: this.globals.Arrayfrom(key),
+ iv: this.globals.Arrayfrom(iv)
+ });
+ this.wkSend(handler, data);
+ });
+ const cipher = new this.globals.Uint8Array([...ciphertext, ...tag]);
+ const decrypted = await this.decrypt(cipher, key, iv);
+ return this.globals.JSONparse(decrypted || "{}");
+ } catch (e3) {
+ if (e3 instanceof MissingHandler) {
+ throw e3;
+ } else {
+ console.error("decryption failed", e3);
+ console.error(e3);
+ return { error: e3 };
+ }
+ }
+ }
+ /**
+ * @param {import('../index.js').NotificationMessage} msg
+ */
+ notify(msg) {
+ this.wkSend(msg.context, msg);
+ }
+ /**
+ * @param {import('../index.js').RequestMessage} msg
+ */
+ async request(msg) {
+ const data = await this.wkSendAndWait(msg.context, msg);
+ if (isResponseFor(msg, data)) {
+ if (data.result) {
+ return data.result || {};
+ }
+ if (data.error) {
+ throw new Error(data.error.message);
+ }
+ }
+ throw new Error("an unknown error occurred");
+ }
+ /**
+ * Generate a random method name and adds it to the global scope
+ * The native layer will use this method to send the response
+ * @param {string | number} randomMethodName
+ * @param {Function} callback
+ * @internal
+ */
+ generateRandomMethod(randomMethodName, callback) {
+ this.globals.ObjectDefineProperty(this.globals.window, randomMethodName, {
+ enumerable: false,
+ // configurable, To allow for deletion later
+ configurable: true,
+ writable: false,
+ /**
+ * @param {any[]} args
+ */
+ value: (...args) => {
+ callback(...args);
+ delete this.globals.window[randomMethodName];
+ }
+ });
+ }
+ /**
+ * @internal
+ * @return {string}
+ */
+ randomString() {
+ return "" + this.globals.getRandomValues(new this.globals.Uint32Array(1))[0];
+ }
+ /**
+ * @internal
+ * @return {string}
+ */
+ createRandMethodName() {
+ return "_" + this.randomString();
+ }
+ /**
+ * @type {{name: string, length: number}}
+ * @internal
+ */
+ algoObj = {
+ name: "AES-GCM",
+ length: 256
+ };
+ /**
+ * @returns {Promise}
+ * @internal
+ */
+ async createRandKey() {
+ const key = await this.globals.generateKey(this.algoObj, true, ["encrypt", "decrypt"]);
+ const exportedKey = await this.globals.exportKey("raw", key);
+ return new this.globals.Uint8Array(exportedKey);
+ }
+ /**
+ * @returns {Uint8Array}
+ * @internal
+ */
+ createRandIv() {
+ return this.globals.getRandomValues(new this.globals.Uint8Array(12));
+ }
+ /**
+ * @param {BufferSource} ciphertext
+ * @param {BufferSource} key
+ * @param {Uint8Array} iv
+ * @returns {Promise}
+ * @internal
+ */
+ async decrypt(ciphertext, key, iv) {
+ const cryptoKey = await this.globals.importKey("raw", key, "AES-GCM", false, ["decrypt"]);
+ const algo = {
+ name: "AES-GCM",
+ iv
+ };
+ const decrypted = await this.globals.decrypt(algo, cryptoKey, ciphertext);
+ const dec = new this.globals.TextDecoder();
+ return dec.decode(decrypted);
+ }
+ /**
+ * When required (such as on macos 10.x), capture the `postMessage` method on
+ * each webkit messageHandler
+ *
+ * @param {string[]} handlerNames
+ */
+ captureWebkitHandlers(handlerNames) {
+ const handlers = window.webkit.messageHandlers;
+ if (!handlers)
+ throw new MissingHandler("window.webkit.messageHandlers was absent", "all");
+ for (const webkitMessageHandlerName of handlerNames) {
+ if (typeof handlers[webkitMessageHandlerName]?.postMessage === "function") {
+ const original = handlers[webkitMessageHandlerName];
+ const bound = handlers[webkitMessageHandlerName].postMessage?.bind(original);
+ this.globals.capturedWebkitHandlers[webkitMessageHandlerName] = bound;
+ delete handlers[webkitMessageHandlerName].postMessage;
+ }
+ }
+ }
+ /**
+ * @param {import('../index.js').Subscription} msg
+ * @param {(value: unknown) => void} callback
+ */
+ subscribe(msg, callback) {
+ if (msg.subscriptionName in this.globals.window) {
+ throw new this.globals.Error(`A subscription with the name ${msg.subscriptionName} already exists`);
+ }
+ this.globals.ObjectDefineProperty(this.globals.window, msg.subscriptionName, {
+ enumerable: false,
+ configurable: true,
+ writable: false,
+ value: (data) => {
+ if (data && isSubscriptionEventFor(msg, data)) {
+ callback(data.params);
+ } else {
+ console.warn("Received a message that did not match the subscription", data);
+ }
+ }
+ });
+ return () => {
+ this.globals.ReflectDeleteProperty(this.globals.window, msg.subscriptionName);
+ };
+ }
+ };
+ var WebkitMessagingConfig = class {
+ /**
+ * @param {object} params
+ * @param {boolean} params.hasModernWebkitAPI
+ * @param {string[]} params.webkitMessageHandlerNames
+ * @param {string} params.secret
+ * @internal
+ */
+ constructor(params) {
+ this.hasModernWebkitAPI = params.hasModernWebkitAPI;
+ this.webkitMessageHandlerNames = params.webkitMessageHandlerNames;
+ this.secret = params.secret;
+ }
+ };
+ var SecureMessagingParams = class {
+ /**
+ * @param {object} params
+ * @param {string} params.methodName
+ * @param {string} params.secret
+ * @param {number[]} params.key
+ * @param {number[]} params.iv
+ */
+ constructor(params) {
+ this.methodName = params.methodName;
+ this.secret = params.secret;
+ this.key = params.key;
+ this.iv = params.iv;
+ }
+ };
+ function captureGlobals() {
+ const globals = {
+ window,
+ getRandomValues: window.crypto.getRandomValues.bind(window.crypto),
+ TextEncoder,
+ TextDecoder,
+ Uint8Array,
+ Uint16Array,
+ Uint32Array,
+ JSONstringify: window.JSON.stringify,
+ JSONparse: window.JSON.parse,
+ Arrayfrom: window.Array.from,
+ Promise: window.Promise,
+ Error: window.Error,
+ ReflectDeleteProperty: window.Reflect.deleteProperty.bind(window.Reflect),
+ ObjectDefineProperty: window.Object.defineProperty,
+ addEventListener: window.addEventListener.bind(window),
+ /** @type {Record} */
+ capturedWebkitHandlers: {}
+ };
+ if (isSecureContext) {
+ globals.generateKey = window.crypto.subtle.generateKey.bind(window.crypto.subtle);
+ globals.exportKey = window.crypto.subtle.exportKey.bind(window.crypto.subtle);
+ globals.importKey = window.crypto.subtle.importKey.bind(window.crypto.subtle);
+ globals.encrypt = window.crypto.subtle.encrypt.bind(window.crypto.subtle);
+ globals.decrypt = window.crypto.subtle.decrypt.bind(window.crypto.subtle);
+ }
+ return globals;
+ }
+
+ // ../messaging/lib/android.js
+ var AndroidMessagingTransport = class {
+ /**
+ * @param {AndroidMessagingConfig} config
+ * @param {MessagingContext} messagingContext
+ * @internal
+ */
+ constructor(config, messagingContext) {
+ this.messagingContext = messagingContext;
+ this.config = config;
+ }
+ /**
+ * @param {NotificationMessage} msg
+ */
+ notify(msg) {
+ try {
+ this.config.sendMessageThrows?.(JSON.stringify(msg));
+ } catch (e3) {
+ console.error(".notify failed", e3);
+ }
+ }
+ /**
+ * @param {RequestMessage} msg
+ * @return {Promise}
+ */
+ request(msg) {
+ return new Promise((resolve, reject) => {
+ const unsub = this.config.subscribe(msg.id, handler);
+ try {
+ this.config.sendMessageThrows?.(JSON.stringify(msg));
+ } catch (e3) {
+ unsub();
+ reject(new Error("request failed to send: " + e3.message || "unknown error"));
+ }
+ function handler(data) {
+ if (isResponseFor(msg, data)) {
+ if (data.result) {
+ resolve(data.result || {});
+ return unsub();
+ }
+ if (data.error) {
+ reject(new Error(data.error.message));
+ return unsub();
+ }
+ unsub();
+ throw new Error("unreachable: must have `result` or `error` key by this point");
+ }
+ }
+ });
+ }
+ /**
+ * @param {Subscription} msg
+ * @param {(value: unknown | undefined) => void} callback
+ */
+ subscribe(msg, callback) {
+ const unsub = this.config.subscribe(msg.subscriptionName, (data) => {
+ if (isSubscriptionEventFor(msg, data)) {
+ callback(data.params || {});
+ }
+ });
+ return () => {
+ unsub();
+ };
+ }
+ };
+ var AndroidMessagingConfig = class {
+ /** @type {(json: string, secret: string) => void} */
+ _capturedHandler;
+ /**
+ * @param {object} params
+ * @param {Record} params.target
+ * @param {boolean} params.debug
+ * @param {string} params.messageSecret - a secret to ensure that messages are only
+ * processed by the correct handler
+ * @param {string} params.javascriptInterface - the name of the javascript interface
+ * registered on the native side
+ * @param {string} params.messageCallback - the name of the callback that the native
+ * side will use to send messages back to the javascript side
+ */
+ constructor(params) {
+ this.target = params.target;
+ this.debug = params.debug;
+ this.javascriptInterface = params.javascriptInterface;
+ this.messageSecret = params.messageSecret;
+ this.messageCallback = params.messageCallback;
+ this.listeners = new globalThis.Map();
+ this._captureGlobalHandler();
+ this._assignHandlerMethod();
+ }
+ /**
+ * The transport can call this to transmit a JSON payload along with a secret
+ * to the native Android handler.
+ *
+ * Note: This can throw - it's up to the transport to handle the error.
+ *
+ * @type {(json: string) => void}
+ * @throws
+ * @internal
+ */
+ sendMessageThrows(json) {
+ this._capturedHandler(json, this.messageSecret);
+ }
+ /**
+ * A subscription on Android is just a named listener. All messages from
+ * android -> are delivered through a single function, and this mapping is used
+ * to route the messages to the correct listener.
+ *
+ * Note: Use this to implement request->response by unsubscribing after the first
+ * response.
+ *
+ * @param {string} id
+ * @param {(msg: MessageResponse | SubscriptionEvent) => void} callback
+ * @returns {() => void}
+ * @internal
+ */
+ subscribe(id, callback) {
+ this.listeners.set(id, callback);
+ return () => {
+ this.listeners.delete(id);
+ };
+ }
+ /**
+ * Accept incoming messages and try to deliver it to a registered listener.
+ *
+ * This code is defensive to prevent any single handler from affecting another if
+ * it throws (producer interference).
+ *
+ * @param {MessageResponse | SubscriptionEvent} payload
+ * @internal
+ */
+ _dispatch(payload) {
+ if (!payload)
+ return this._log("no response");
+ if ("id" in payload) {
+ if (this.listeners.has(payload.id)) {
+ this._tryCatch(() => this.listeners.get(payload.id)?.(payload));
+ } else {
+ this._log("no listeners for ", payload);
+ }
+ }
+ if ("subscriptionName" in payload) {
+ if (this.listeners.has(payload.subscriptionName)) {
+ this._tryCatch(() => this.listeners.get(payload.subscriptionName)?.(payload));
+ } else {
+ this._log("no subscription listeners for ", payload);
+ }
+ }
+ }
+ /**
+ *
+ * @param {(...args: any[]) => any} fn
+ * @param {string} [context]
+ */
+ _tryCatch(fn, context = "none") {
+ try {
+ return fn();
+ } catch (e3) {
+ if (this.debug) {
+ console.error("AndroidMessagingConfig error:", context);
+ console.error(e3);
+ }
+ }
+ }
+ /**
+ * @param {...any} args
+ */
+ _log(...args) {
+ if (this.debug) {
+ console.log("AndroidMessagingConfig", ...args);
+ }
+ }
+ /**
+ * Capture the global handler and remove it from the global object.
+ */
+ _captureGlobalHandler() {
+ const { target, javascriptInterface } = this;
+ if (Object.prototype.hasOwnProperty.call(target, javascriptInterface)) {
+ this._capturedHandler = target[javascriptInterface].process.bind(target[javascriptInterface]);
+ delete target[javascriptInterface];
+ } else {
+ this._capturedHandler = () => {
+ this._log("Android messaging interface not available", javascriptInterface);
+ };
+ }
+ }
+ /**
+ * Assign the incoming handler method to the global object.
+ * This is the method that Android will call to deliver messages.
+ */
+ _assignHandlerMethod() {
+ const responseHandler = (providedSecret, response) => {
+ if (providedSecret === this.messageSecret) {
+ this._dispatch(response);
+ }
+ };
+ Object.defineProperty(this.target, this.messageCallback, {
+ value: responseHandler
+ });
+ }
+ };
+
+ // ../messaging/lib/typed-messages.js
+ function createTypedMessages(base, messaging2) {
+ const asAny = (
+ /** @type {any} */
+ messaging2
+ );
+ return (
+ /** @type {BaseClass} */
+ asAny
+ );
+ }
+
+ // ../messaging/index.js
+ var MessagingContext2 = class {
+ /**
+ * @param {object} params
+ * @param {string} params.context
+ * @param {string} params.featureName
+ * @param {"production" | "development"} params.env
+ * @internal
+ */
+ constructor(params) {
+ this.context = params.context;
+ this.featureName = params.featureName;
+ this.env = params.env;
+ }
+ };
+ var Messaging = class {
+ /**
+ * @param {MessagingContext} messagingContext
+ * @param {MessagingConfig} config
+ */
+ constructor(messagingContext, config) {
+ this.messagingContext = messagingContext;
+ this.transport = getTransport(config, this.messagingContext);
+ }
+ /**
+ * Send a 'fire-and-forget' message.
+ * @throws {MissingHandler}
+ *
+ * @example
+ *
+ * ```ts
+ * const messaging = new Messaging(config)
+ * messaging.notify("foo", {bar: "baz"})
+ * ```
+ * @param {string} name
+ * @param {Record} [data]
+ */
+ notify(name, data = {}) {
+ const message = new NotificationMessage({
+ context: this.messagingContext.context,
+ featureName: this.messagingContext.featureName,
+ method: name,
+ params: data
+ });
+ this.transport.notify(message);
+ }
+ /**
+ * Send a request, and wait for a response
+ * @throws {MissingHandler}
+ *
+ * @example
+ * ```
+ * const messaging = new Messaging(config)
+ * const response = await messaging.request("foo", {bar: "baz"})
+ * ```
+ *
+ * @param {string} name
+ * @param {Record} [data]
+ * @return {Promise}
+ */
+ request(name, data = {}) {
+ const id = globalThis?.crypto?.randomUUID?.() || name + ".response";
+ const message = new RequestMessage({
+ context: this.messagingContext.context,
+ featureName: this.messagingContext.featureName,
+ method: name,
+ params: data,
+ id
+ });
+ return this.transport.request(message);
+ }
+ /**
+ * @param {string} name
+ * @param {(value: unknown) => void} callback
+ * @return {() => void}
+ */
+ subscribe(name, callback) {
+ const msg = new Subscription({
+ context: this.messagingContext.context,
+ featureName: this.messagingContext.featureName,
+ subscriptionName: name
+ });
+ return this.transport.subscribe(msg, callback);
+ }
+ };
+ var TestTransportConfig = class {
+ /**
+ * @param {MessagingTransport} impl
+ */
+ constructor(impl) {
+ this.impl = impl;
+ }
+ };
+ var TestTransport = class {
+ /**
+ * @param {TestTransportConfig} config
+ * @param {MessagingContext} messagingContext
+ */
+ constructor(config, messagingContext) {
+ this.config = config;
+ this.messagingContext = messagingContext;
+ }
+ notify(msg) {
+ return this.config.impl.notify(msg);
+ }
+ request(msg) {
+ return this.config.impl.request(msg);
+ }
+ subscribe(msg, callback) {
+ return this.config.impl.subscribe(msg, callback);
+ }
+ };
+ function getTransport(config, messagingContext) {
+ if (config instanceof WebkitMessagingConfig) {
+ return new WebkitMessagingTransport(config, messagingContext);
+ }
+ if (config instanceof WindowsMessagingConfig) {
+ return new WindowsMessagingTransport(config, messagingContext);
+ }
+ if (config instanceof AndroidMessagingConfig) {
+ return new AndroidMessagingTransport(config, messagingContext);
+ }
+ if (config instanceof TestTransportConfig) {
+ return new TestTransport(config, messagingContext);
+ }
+ throw new Error("unreachable");
+ }
+ var MissingHandler = class extends Error {
+ /**
+ * @param {string} message
+ * @param {string} handlerName
+ */
+ constructor(message, handlerName) {
+ super(message);
+ this.handlerName = handlerName;
+ }
+ };
+
+ // shared/create-special-page-messaging.js
+ function createSpecialPageMessaging(opts) {
+ const messageContext = new MessagingContext2({
+ context: "specialPages",
+ featureName: opts.pageName,
+ env: opts.env
+ });
+ try {
+ if (opts.injectName === "windows") {
+ const opts2 = new WindowsMessagingConfig({
+ methods: {
+ // @ts-expect-error - not in @types/chrome
+ postMessage: window.chrome.webview.postMessage,
+ // @ts-expect-error - not in @types/chrome
+ addEventListener: window.chrome.webview.addEventListener,
+ // @ts-expect-error - not in @types/chrome
+ removeEventListener: window.chrome.webview.removeEventListener
+ }
+ });
+ return new Messaging(messageContext, opts2);
+ } else if (opts.injectName === "apple") {
+ const opts2 = new WebkitMessagingConfig({
+ hasModernWebkitAPI: true,
+ secret: "",
+ webkitMessageHandlerNames: ["specialPages"]
+ });
+ return new Messaging(messageContext, opts2);
+ } else if (opts.injectName === "android") {
+ const opts2 = new AndroidMessagingConfig({
+ messageSecret: "duckduckgo-android-messaging-secret",
+ messageCallback: "messageCallback",
+ javascriptInterface: messageContext.context,
+ target: globalThis,
+ debug: true
+ });
+ return new Messaging(messageContext, opts2);
+ }
+ } catch (e3) {
+ console.error("could not access handlers for %s, falling back to mock interface", opts.injectName);
+ }
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
+ /**
+ * @param {import('@duckduckgo/messaging').NotificationMessage} msg
+ */
+ notify(msg) {
+ console.log(msg);
+ },
+ /**
+ * @param {import('@duckduckgo/messaging').RequestMessage} msg
+ */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ request: (msg) => {
+ console.log(msg);
+ if (msg.method === "initialSetup") {
+ return Promise.resolve({
+ locale: "en",
+ env: opts.env
+ });
+ }
+ return Promise.resolve(null);
+ },
+ /**
+ * @param {import('@duckduckgo/messaging').SubscriptionEvent} msg
+ */
+ subscribe(msg) {
+ console.log(msg);
+ return () => {
+ console.log("teardown");
+ };
+ }
+ });
+ return new Messaging(messageContext, fallback);
+ }
+
+ // shared/environment.js
+ var Environment = class _Environment {
+ /**
+ * @param {object} params
+ * @param {'app' | 'components'} [params.display] - whether to show the application or component list
+ * @param {'production' | 'development'} [params.env] - application environment
+ * @param {URLSearchParams} [params.urlParams] - URL params passed into the page
+ * @param {ImportMeta['injectName']} [params.injectName] - application platform
+ * @param {boolean} [params.willThrow] - whether the application will simulate an error
+ * @param {boolean} [params.debugState] - whether to show debugging UI
+ * @param {string} [params.locale] - for applications strings
+ * @param {number} [params.textLength] - what ratio of text should be used. Set a number higher than 1 to have longer strings for testing
+ */
+ constructor({
+ env = "production",
+ urlParams = new URLSearchParams(location.search),
+ injectName = "windows",
+ willThrow = urlParams.get("willThrow") === "true",
+ debugState = urlParams.has("debugState"),
+ display = "app",
+ locale = "en",
+ textLength = 1
+ } = {}) {
+ this.display = display;
+ this.urlParams = urlParams;
+ this.injectName = injectName;
+ this.willThrow = willThrow;
+ this.debugState = debugState;
+ this.env = env;
+ this.locale = locale;
+ this.textLength = textLength;
+ }
+ /**
+ * @param {string|null|undefined} injectName
+ * @returns {Environment}
+ */
+ withInjectName(injectName) {
+ if (!injectName)
+ return this;
+ if (!isInjectName(injectName))
+ return this;
+ return new _Environment({
+ ...this,
+ injectName
+ });
+ }
+ /**
+ * @param {string|null|undefined} env
+ * @returns {Environment}
+ */
+ withEnv(env) {
+ if (!env)
+ return this;
+ if (env !== "production" && env !== "development")
+ return this;
+ return new _Environment({
+ ...this,
+ env
+ });
+ }
+ /**
+ * @param {string|null|undefined} display
+ * @returns {Environment}
+ */
+ withDisplay(display) {
+ if (!display)
+ return this;
+ if (display !== "app" && display !== "components")
+ return this;
+ return new _Environment({
+ ...this,
+ display
+ });
+ }
+ /**
+ * @param {string|null|undefined} locale
+ * @returns {Environment}
+ */
+ withLocale(locale) {
+ if (!locale)
+ return this;
+ if (typeof locale !== "string")
+ return this;
+ if (locale.length !== 2)
+ return this;
+ return new _Environment({
+ ...this,
+ locale
+ });
+ }
+ /**
+ * @param {string|number|null|undefined} length
+ * @returns {Environment}
+ */
+ withTextLength(length) {
+ if (!length)
+ return this;
+ const num = Number(length);
+ if (num >= 1 && num <= 2) {
+ return new _Environment({
+ ...this,
+ textLength: num
+ });
+ }
+ return this;
+ }
+ };
+ function isInjectName(input) {
+ const allowed = ["windows", "apple", "integration", "android"];
+ return allowed.includes(input);
+ }
+
+ // pages/new-tab/src/js/index.js
+ var NewTabPage = class {
+ /**
+ * @param {import("@duckduckgo/messaging").Messaging} messaging
+ * @param {ImportMeta['injectName']} injectName
+ */
+ constructor(messaging2, injectName) {
+ this.messaging = createTypedMessages(this, messaging2);
+ this.injectName = injectName;
+ }
+ /**
+ * @return {Promise}
+ */
+ init() {
+ return this.messaging.request("initialSetup");
+ }
+ /**
+ * @param {string} message
+ */
+ reportInitException(message) {
+ this.messaging.notify("reportInitException", { message });
+ }
+ /**
+ * This will be sent if the application has loaded, but a client-side error
+ * has occurred that cannot be recovered from
+ * @param {{message: string}} params
+ */
+ reportPageException(params) {
+ this.messaging.notify("reportPageException", params);
+ }
+ };
+ var baseEnvironment = new Environment().withInjectName("windows").withEnv("production");
+ var messaging = createSpecialPageMessaging({
+ injectName: "windows",
+ env: "production",
+ pageName: "newTabPage",
+ mockTransport: () => {
+ if (baseEnvironment.injectName !== "integration")
+ return null;
+ if (window.__playwright_01)
+ return null;
+ let mock = null;
+ return mock;
+ }
+ });
+ var newTabMessaging = new NewTabPage(messaging, "windows");
+ init(newTabMessaging, baseEnvironment).catch((e3) => {
+ console.error(e3);
+ const msg = typeof e3?.message === "string" ? e3.message : "unknown init error";
+ newTabMessaging.reportInitException(msg);
+ });
+})();
diff --git a/build/windows/pages/new-tab/js/inline.js b/build/windows/pages/new-tab/js/inline.js
new file mode 100644
index 000000000..5a71d2623
--- /dev/null
+++ b/build/windows/pages/new-tab/js/inline.js
@@ -0,0 +1,3 @@
+"use strict";
+(() => {
+})();
diff --git a/build/windows/pages/new-tab/js/mock-transport.js b/build/windows/pages/new-tab/js/mock-transport.js
new file mode 100644
index 000000000..ca6b115d2
--- /dev/null
+++ b/build/windows/pages/new-tab/js/mock-transport.js
@@ -0,0 +1,103 @@
+import { TestTransportConfig } from '@duckduckgo/messaging'
+
+export function mockTransport () {
+ const channel = new BroadcastChannel('ntp')
+
+ function broadcast () {
+ setTimeout(() => {
+ channel.postMessage({
+ change: 'ntp.widgetConfig'
+ })
+ }, 100)
+ }
+
+ /**
+ * @param {string} name
+ * @return {Record|null}
+ */
+ function read (name) {
+ try {
+ const item = localStorage.getItem(name)
+ if (!item) return null
+ console.log('did read from LS', item)
+ return JSON.parse(item)
+ } catch (e) {
+ console.error('Failed to parse initialSetup from localStorage', e)
+ return null
+ }
+ }
+
+ /**
+ * @param {string} name
+ * @param {Record} value
+ */
+ function write (name, value) {
+ try {
+ localStorage.setItem(name, JSON.stringify(value))
+ console.log('✅ did write')
+ } catch (e) {
+ console.error('Failed to write', e)
+ }
+ }
+
+ return new TestTransportConfig({
+ notify (msg) {
+ switch (msg.method) {
+ case 'setWidgetConfig': {
+ if (!msg.params) throw new Error('unreachable')
+ write('ntp.widgetConfig', msg.params)
+ broadcast()
+ return
+ }
+ default: {
+ console.warn('unhandled notification', msg)
+ }
+ }
+ },
+ subscribe (sub, cb) {
+ switch (sub.subscriptionName) {
+ case 'onWidgetConfigUpdated': {
+ const controller = new AbortController()
+ // console.log('sub?', sub, cb);
+ channel.addEventListener('message', () => {
+ const values = read('ntp.widgetConfig')
+ if (values) {
+ cb(values)
+ }
+ }, { signal: controller.signal })
+ return () => controller.abort()
+ }
+ }
+ return () => {}
+ },
+ // eslint-ignore-next-line require-await
+ request (msg) {
+ switch (msg.method) {
+ case 'initialSetup': {
+ const widgetsFromStorage = read('ntp.widgets') || {
+ widgets: [
+ { id: 'favorites' },
+ { id: 'privacyStats' }
+ ]
+ }
+ const widgetConfigFromStorage = read('ntp.widgetConfig') || {
+ widgetConfig: [
+ { id: 'favorites', visibility: 'visible' },
+ { id: 'privacyStats', visibility: 'visible' }
+ ]
+ }
+ return Promise.resolve({
+ widgets: widgetsFromStorage.widgets,
+ widgetConfig: widgetConfigFromStorage.widgetConfig,
+ platform: { name: 'integration' },
+ env: 'development',
+ locale: 'en'
+ })
+ }
+ default: {
+ return Promise.reject(new Error('unhandled request'))
+ }
+ }
+ }
+ })
+}
diff --git a/build/windows/pages/new-tab/locales/en/newtab.json b/build/windows/pages/new-tab/locales/en/newtab.json
new file mode 100644
index 000000000..6d61912fc
--- /dev/null
+++ b/build/windows/pages/new-tab/locales/en/newtab.json
@@ -0,0 +1,16 @@
+{
+ "smartling": {
+ "string_format": "icu",
+ "translate_paths": [
+ {
+ "path": "*/title",
+ "key": "{*}/title",
+ "instruction": "*/note"
+ }
+ ]
+ },
+ "helloWorld": {
+ "title": "Hello world!",
+ "note": "here!"
+ }
+}
diff --git a/build/windows/pages/onboarding/js/index.js b/build/windows/pages/onboarding/js/index.js
index d77743314..998401264 100644
--- a/build/windows/pages/onboarding/js/index.js
+++ b/build/windows/pages/onboarding/js/index.js
@@ -8440,7 +8440,7 @@
} catch (e3) {
console.error("could not access handlers for %s, falling back to mock interface", opts.injectName);
}
- const fallback = new TestTransportConfig({
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
/**
* @param {import('@duckduckgo/messaging').NotificationMessage} msg
*/
diff --git a/packages/special-pages/index.mjs b/packages/special-pages/index.mjs
index aaeaf484d..960c3cddd 100644
--- a/packages/special-pages/index.mjs
+++ b/packages/special-pages/index.mjs
@@ -56,6 +56,11 @@ export const support = {
'integration': ['copy', 'build-js'],
'apple': ['copy', 'build-js', 'inline-html'],
},
+ /** @type {Partial>} */
+ 'new-tab': {
+ 'integration': ['copy', 'build-js'],
+ 'windows': ['copy', 'build-js'],
+ },
}
/** @type {{src: string, dest: string, injectName: string}[]} */
@@ -157,7 +162,8 @@ for (const buildJob of buildJobs) {
'import.meta.env': JSON.stringify(NODE_ENV),
'import.meta.injectName': JSON.stringify(buildJob.injectName),
'import.meta.pageName': JSON.stringify(buildJob.pageName),
- }
+ },
+ dropLabels: buildJob.injectName === "integration" ? [] : ["$INTEGRATION"]
})
}
}
diff --git a/packages/special-pages/messages/new-tab/initialSetup.request.json b/packages/special-pages/messages/new-tab/initialSetup.request.json
new file mode 100644
index 000000000..0af74a319
--- /dev/null
+++ b/packages/special-pages/messages/new-tab/initialSetup.request.json
@@ -0,0 +1,3 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
diff --git a/packages/special-pages/messages/new-tab/initialSetup.response.json b/packages/special-pages/messages/new-tab/initialSetup.response.json
new file mode 100644
index 000000000..2a8f3abe7
--- /dev/null
+++ b/packages/special-pages/messages/new-tab/initialSetup.response.json
@@ -0,0 +1,30 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "required": ["widgets", "widgetConfig", "locale", "env", "platform"],
+ "properties": {
+ "widgets": {
+ "$ref": "./types/widget-list.json"
+ },
+ "widgetConfig": {
+ "$ref": "./types/widget-config.json"
+ },
+ "locale": {
+ "type": "string"
+ },
+ "env": {
+ "type": "string",
+ "enum": ["development", "production"]
+ },
+ "platform": {
+ "type": "object",
+ "required": ["name"],
+ "properties": {
+ "name": {
+ "type": "string",
+ "enum": ["macos", "windows", "android", "ios"]
+ }
+ }
+ }
+ }
+}
diff --git a/packages/special-pages/messages/new-tab/onWidgetConfigUpdated.subscribe.json b/packages/special-pages/messages/new-tab/onWidgetConfigUpdated.subscribe.json
new file mode 100644
index 000000000..8d0f33369
--- /dev/null
+++ b/packages/special-pages/messages/new-tab/onWidgetConfigUpdated.subscribe.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "required": ["widgetConfig"],
+ "properties": {
+ "widgetConfig": {
+ "$ref": "./types/widget-config.json"
+ }
+ }
+}
diff --git a/packages/special-pages/messages/new-tab/reportInitException.notify.json b/packages/special-pages/messages/new-tab/reportInitException.notify.json
new file mode 100644
index 000000000..afd7d6bde
--- /dev/null
+++ b/packages/special-pages/messages/new-tab/reportInitException.notify.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "required": ["message"],
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ }
+}
diff --git a/packages/special-pages/messages/new-tab/reportPageException.notify.json b/packages/special-pages/messages/new-tab/reportPageException.notify.json
new file mode 100644
index 000000000..afd7d6bde
--- /dev/null
+++ b/packages/special-pages/messages/new-tab/reportPageException.notify.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "required": ["message"],
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ }
+}
diff --git a/packages/special-pages/messages/new-tab/setWidgetConfig.notify.json b/packages/special-pages/messages/new-tab/setWidgetConfig.notify.json
new file mode 100644
index 000000000..8d0f33369
--- /dev/null
+++ b/packages/special-pages/messages/new-tab/setWidgetConfig.notify.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "required": ["widgetConfig"],
+ "properties": {
+ "widgetConfig": {
+ "$ref": "./types/widget-config.json"
+ }
+ }
+}
diff --git a/packages/special-pages/messages/new-tab/types/widget-config.json b/packages/special-pages/messages/new-tab/types/widget-config.json
new file mode 100644
index 000000000..ca20ed012
--- /dev/null
+++ b/packages/special-pages/messages/new-tab/types/widget-config.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "array",
+ "title": "Widget Config",
+ "description": "Configuration settings for widgets",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "visibility"
+ ],
+ "title": "Widget Config Item",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "A unique identifier for the widget."
+ },
+ "visibility": {
+ "title": "Widget Visibility",
+ "description": "The visibility state of the widget, as configured by the user",
+ "type": "string",
+ "enum": [
+ "visible",
+ "hidden"
+ ]
+ }
+ }
+ }
+}
diff --git a/packages/special-pages/messages/new-tab/types/widget-list.json b/packages/special-pages/messages/new-tab/types/widget-list.json
new file mode 100644
index 000000000..2a57c5925
--- /dev/null
+++ b/packages/special-pages/messages/new-tab/types/widget-list.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "array",
+ "title": "Widget List",
+ "description": "An ordered list of supported Widgets. Use this to communicate what's supported",
+ "items": {
+ "type": "object",
+ "required": ["id"],
+ "title": "Widget List Item",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "A unique identifier for the widget."
+ }
+ }
+ }
+}
diff --git a/packages/special-pages/pages/duckplayer/app/index.js b/packages/special-pages/pages/duckplayer/app/index.js
index ee978acc9..f4ff62d3c 100644
--- a/packages/special-pages/pages/duckplayer/app/index.js
+++ b/packages/special-pages/pages/duckplayer/app/index.js
@@ -10,7 +10,7 @@ import { Settings } from './settings.js'
import { SettingsProvider } from './providers/SettingsProvider.jsx'
import { MessagingContext } from './types.js'
import { UserValuesProvider } from './providers/UserValuesProvider.jsx'
-import { Fallback } from './components/Fallback.jsx'
+import { Fallback } from '../../../shared/components/Fallback/Fallback.jsx'
import { Components } from './components/Components.jsx'
import { MobileApp } from './components/MobileApp.jsx'
import { DesktopApp } from './components/DesktopApp.jsx'
diff --git a/packages/special-pages/pages/new-tab/app/components/App.js b/packages/special-pages/pages/new-tab/app/components/App.js
new file mode 100644
index 000000000..55ad8ff65
--- /dev/null
+++ b/packages/special-pages/pages/new-tab/app/components/App.js
@@ -0,0 +1,10 @@
+import { h } from 'preact'
+import styles from './App.module.css'
+
+export function App ({ children }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/packages/special-pages/pages/new-tab/app/components/App.module.css b/packages/special-pages/pages/new-tab/app/components/App.module.css
new file mode 100644
index 000000000..658209802
--- /dev/null
+++ b/packages/special-pages/pages/new-tab/app/components/App.module.css
@@ -0,0 +1,7 @@
+.layout {
+ padding-top: var(--sp-16);
+ padding-bottom: var(--sp-16);
+ max-width: 504px;
+ margin-left: auto;
+ margin-right: auto;
+}
diff --git a/packages/special-pages/pages/new-tab/app/index.js b/packages/special-pages/pages/new-tab/app/index.js
new file mode 100644
index 000000000..6a86b8390
--- /dev/null
+++ b/packages/special-pages/pages/new-tab/app/index.js
@@ -0,0 +1,89 @@
+import { render, h } from 'preact'
+import './styles/base.css' // global styles
+import { App } from './components/App.js'
+import { EnvironmentProvider, UpdateEnvironment } from '../../../shared/components/EnvironmentProvider.js'
+import { Fallback } from '../../../shared/components/Fallback/Fallback.jsx'
+import { ErrorBoundary } from '../../../shared/components/ErrorBoundary.js'
+import { SettingsProvider } from './settings.provider.js'
+import { MessagingContext } from './types'
+import { TranslationProvider } from '../../../shared/components/TranslationsProvider.js'
+import { WidgetConfigAPI } from './widget-list/widget-config.js'
+import enStrings from '../src/locales/en/newtab.json'
+import { WidgetConfigProvider } from './widget-list/widget-config.provider.js'
+import { WidgetList } from './widget-list/WidgetList.js'
+import { Settings } from './settings.js'
+
+/**
+ * @param {import("../src/js").NewTabPage} messaging
+ * @param {import("../../../shared/environment").Environment} baseEnvironment
+ */
+export async function init (messaging, baseEnvironment) {
+ const init = await messaging.init()
+
+ if (!Array.isArray(init.widgets)) {
+ throw new Error('missing critical initialSetup.widgets array')
+ }
+ if (!Array.isArray(init.widgetConfig)) {
+ throw new Error('missing critical initialSetup.widgetConfig array')
+ }
+
+ // Create an instance of the global widget api
+ const widgetConfigAPI = new WidgetConfigAPI(messaging, init.widgetConfig)
+
+ // update the 'env' in case it was changed by native sides
+ const environment = baseEnvironment
+ .withEnv(init.env)
+ .withLocale(init.locale)
+ .withLocale(baseEnvironment.urlParams.get('locale'))
+ .withTextLength(baseEnvironment.urlParams.get('textLength'))
+ .withDisplay(baseEnvironment.urlParams.get('display'))
+
+ console.log('environment:', environment)
+ console.log('locale:', environment.locale)
+
+ const strings = environment.locale === 'en'
+ ? enStrings
+ : await fetch(`./locales/${environment.locale}/new-tab.json`)
+ .then(x => x.json())
+ .catch(e => {
+ console.error('Could not load locale', environment.locale, e)
+ return enStrings
+ })
+
+ const settings = new Settings({})
+ .withPlatformName(baseEnvironment.injectName)
+ .withPlatformName(init.platform?.name)
+ .withPlatformName(baseEnvironment.urlParams.get('platform'))
+
+ const didCatch = (error) => {
+ const message = error?.message || error?.error || 'unknown'
+ messaging.reportPageException({ message })
+ }
+
+ const root = document.querySelector('#app')
+ if (!root) throw new Error('could not render, root element missing')
+
+ render(
+
+ }>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ root
+ )
+}
diff --git a/packages/special-pages/pages/new-tab/app/settings.js b/packages/special-pages/pages/new-tab/app/settings.js
new file mode 100644
index 000000000..42fa73fe2
--- /dev/null
+++ b/packages/special-pages/pages/new-tab/app/settings.js
@@ -0,0 +1,23 @@
+export class Settings {
+ /**
+ * @param {object} params
+ * @param {{name: ImportMeta['platform']}} [params.platform]
+ */
+ constructor ({
+ platform = { name: 'windows' }
+ }) {
+ this.platform = platform
+ }
+
+ withPlatformName (name) {
+ /** @type {ImportMeta['platform'][]} */
+ const valid = ['windows', 'macos', 'ios', 'android']
+ if (valid.includes(/** @type {any} */(name))) {
+ return new Settings({
+ ...this,
+ platform: { name }
+ })
+ }
+ return this
+ }
+}
diff --git a/packages/special-pages/pages/new-tab/app/settings.provider.js b/packages/special-pages/pages/new-tab/app/settings.provider.js
new file mode 100644
index 000000000..3f71e1eb8
--- /dev/null
+++ b/packages/special-pages/pages/new-tab/app/settings.provider.js
@@ -0,0 +1,16 @@
+import { h, createContext } from 'preact'
+
+const SettingsContext = createContext(/** @type {{settings: import("./settings.js").Settings}} */({}))
+
+/**
+ * @param {object} params
+ * @param {import("./settings.js").Settings} params.settings
+ * @param {import("preact").ComponentChild} params.children
+ */
+export function SettingsProvider ({ settings, children }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/packages/special-pages/pages/new-tab/app/styles/base.css b/packages/special-pages/pages/new-tab/app/styles/base.css
new file mode 100644
index 000000000..74f4d01ab
--- /dev/null
+++ b/packages/special-pages/pages/new-tab/app/styles/base.css
@@ -0,0 +1,45 @@
+*, *:after, *:before {
+ box-sizing: border-box
+}
+html[data-reduced-motion=true] * {
+ animation: none!important;
+ transition: none!important;
+}
+/* Base styles */
+body {
+ font-family: system-ui;
+ margin: 0;
+
+ height: 100vh;
+ width: 100%;
+ overflow-x: hidden;
+
+ /* Make it feel more like something native */
+ user-select: none;
+ -webkit-user-select: none;
+ cursor: default;
+ color: var(--theme-txt-color);
+ background: var(--theme-page-bg);
+}
+body > main {
+ width: 100%;
+}
+h1,
+h2,
+h3,
+h4 {
+ margin: 0;
+}
+button {
+ font-family: system-ui, sans-serif;
+}
+ul {
+ margin: 0;
+ padding: 0;
+}
+
+li {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/packages/special-pages/pages/new-tab/app/types.js b/packages/special-pages/pages/new-tab/app/types.js
new file mode 100644
index 000000000..32d7e6b03
--- /dev/null
+++ b/packages/special-pages/pages/new-tab/app/types.js
@@ -0,0 +1,18 @@
+import { useContext } from 'preact/hooks'
+import { TranslationContext } from '../../../shared/components/TranslationsProvider.js'
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import json from '../src/locales/en/newtab.json'
+import { createContext } from 'preact'
+
+/**
+ * This is a wrapper to only allow keys from the default translation file
+ * @type {() => { t: (key: keyof json, replacements?: Record) => string }}
+ */
+export function useTypedTranslation () {
+ return {
+ t: useContext(TranslationContext).t
+ }
+}
+
+export const MessagingContext = createContext(/** @type {import("../src/js/index.js").NewTabPage} */({}))
+export const useMessaging = () => useContext(MessagingContext)
diff --git a/packages/special-pages/pages/new-tab/app/widget-list/WidgetList.js b/packages/special-pages/pages/new-tab/app/widget-list/WidgetList.js
new file mode 100644
index 000000000..7846ab023
--- /dev/null
+++ b/packages/special-pages/pages/new-tab/app/widget-list/WidgetList.js
@@ -0,0 +1,56 @@
+import { h, Fragment } from 'preact'
+import { useVisibility, WidgetConfigContext, WidgetVisibilityProvider } from './widget-config.provider.js'
+import { useContext } from 'preact/hooks'
+
+const widgetMap = {
+ favorites: () => ,
+ privacyStats: () =>
+}
+
+export function WidgetList () {
+ const { widgets, widgetConfig } = useContext(WidgetConfigContext)
+
+ return (
+
+ {widgets.map((widget) => {
+ const matchingConfig = widgetConfig.find(item => item.id === widget.id)
+ if (!matchingConfig) {
+ console.warn('missing config for widget: ', widget)
+ return null
+ }
+ return (
+
+
+ {widgetMap[widget.id]?.()}
+
+
+ )
+ })}
+
+ )
+}
+
+function Favorites () {
+ const { visibility, id, toggle } = useVisibility()
+ return (
+
+
Favorites Component
+
{id} visibility: {visibility}
{' '}
+
+
+ )
+}
+
+function PrivacyStats () {
+ const { visibility, id, toggle } = useVisibility()
+ return (
+
+
Privacy Stats Component
+
{id} visibility: {visibility}
{' '}
+
+
+ )
+}
diff --git a/packages/special-pages/pages/new-tab/app/widget-list/widget-config.js b/packages/special-pages/pages/new-tab/app/widget-list/widget-config.js
new file mode 100644
index 000000000..330347b87
--- /dev/null
+++ b/packages/special-pages/pages/new-tab/app/widget-list/widget-config.js
@@ -0,0 +1,144 @@
+/**
+ * @typedef {import("../../../../types/new-tab.js").WidgetConfig} WidgetConfig
+ */
+
+/**
+ * The public API. Use this to subscribe to updates
+ */
+export class WidgetConfigAPI {
+ /**
+ * @param {import("../../src/js/index.js").NewTabPage} ntp - The internal data feed, expected to have a `subscribe` method.
+ * @param {WidgetConfig} initialData - Initial widget configuration.
+ */
+ constructor (ntp, initialData) {
+ this.ntp = ntp
+ this.manager = new WidgetConfigManager(ntp, initialData)
+ }
+
+ /**
+ * @param {(data: WidgetConfig) => void} cb
+ * @return {() => void} - call this returned method to dispose of the subscription
+ */
+ onUpdate (cb) {
+ const controller = new AbortController()
+ this.manager.eventTarget.addEventListener(this.manager.DATA_CHANGE_EVT, (/** @type {CustomEvent} */evt) => {
+ cb(evt.detail)
+ }, { signal: controller.signal })
+ return () => controller.abort()
+ }
+
+ /**
+ * @param {string} id
+ */
+ show (id) {
+ const next = this.manager.inMemoryData.map(w => {
+ if (w.id === id) return { ...w, visibility: /** @type {const} */('visible') }
+ return w
+ })
+ this.manager.update(next)
+ }
+
+ /**
+ * @param {string} id
+ */
+ hide (id) {
+ const next = this.manager.inMemoryData.map(w => {
+ if (w.id === id) return { ...w, visibility: /** @type {const} */('hidden') }
+ return w
+ })
+ this.manager.update(next)
+ }
+}
+
+class WidgetConfigManager {
+ debounceTimer = null
+ eventTarget = new EventTarget()
+ DATA_CHANGE_EVT = 'dataChanged'
+ DEBOUNCE_TIME_MS = 200
+ /**
+ * @param {import("../../src/js/index.js").NewTabPage} ntp - The internal data feed, expected to have a `subscribe` method.
+ * @param {WidgetConfig} initialData - Initial widget configuration.
+ */
+ constructor (ntp, initialData) {
+ this.ntp = ntp
+ this.inMemoryData = initialData
+
+ // Set up the subscription data feed as a source
+ this.setupSubscriptionStream()
+ }
+
+ /**
+ * Sets up the subscription stream from the data feed, which behaves like an input source
+ * that updates in-memory data but does not trigger persistence.
+ */
+ setupSubscriptionStream () {
+ // this subscription lives for the lifespan of the page, so doesn't need cleanup logic
+ this.ntp.messaging.subscribe('onWidgetConfigUpdated', (newData) => {
+ this.updateInMemoryData(newData.widgetConfig, 'subscription')
+ })
+ }
+
+ /**
+ * Manually trigger an update, for example, from a UI element.
+ * @param {WidgetConfig} newData - The new widget configuration to update.
+ */
+ update (newData) {
+ this.updateInMemoryData(newData, 'manual')
+ }
+
+ /**
+ * Updates the in-memory data and triggers persistence if the source is a manual update.
+ * This method centralizes all state updates.
+ * @param {WidgetConfig} newData - The new widget configuration to update in memory.
+ * @param {'manual' | 'subscription'} source - The source of the update. Either 'subscription' or 'manual'.
+ */
+ updateInMemoryData (newData, source) {
+ this.log(`Updating in-memory data from '${source}:'`, newData)
+ this.inMemoryData = structuredClone(newData) // Create new immutable state
+ this.broadcastChange()
+
+ // If the source is 'manual', debounce the save operation
+ if (source === 'manual') {
+ this.clearDebounceTimer()
+ this.debounceTimer = /** @type {any} */(setTimeout(() => {
+ this.persist()
+ }, this.DEBOUNCE_TIME_MS))
+ }
+ }
+
+ /**
+ * Clears the debounce timer if it exists, simulating the switchMap behavior.
+ */
+ clearDebounceTimer () {
+ if (this.debounceTimer) {
+ this.log('Clearing previous debounce timer.')
+ clearTimeout(this.debounceTimer)
+ this.debounceTimer = null
+ }
+ }
+
+ /**
+ * Broadcasts the current state to external subscribers using EventTarget.
+ */
+ broadcastChange () {
+ this.log('Broadcasting change to external listeners:', this.inMemoryData)
+ this.eventTarget.dispatchEvent(new CustomEvent(this.DATA_CHANGE_EVT, { detail: this.inMemoryData }))
+ }
+
+ /**
+ * Persists the current in-memory widget configuration state to the internal data feed.
+ */
+ persist () {
+ this.log('will persist data to backend:', this.inMemoryData)
+ this.ntp.messaging.notify('setWidgetConfig', { widgetConfig: this.inMemoryData })
+ }
+
+ /**
+ * Logs messages to the console for tracing internal state updates.
+ * @param {string} message - The message to log.
+ * @param {any} data - The associated data for the message.
+ */
+ log (message, data = null) {
+ console.log(`[WidgetConfigManager] ${message}`, data)
+ }
+}
diff --git a/packages/special-pages/pages/new-tab/app/widget-list/widget-config.provider.js b/packages/special-pages/pages/new-tab/app/widget-list/widget-config.provider.js
new file mode 100644
index 000000000..24b132115
--- /dev/null
+++ b/packages/special-pages/pages/new-tab/app/widget-list/widget-config.provider.js
@@ -0,0 +1,115 @@
+import { createContext, h } from 'preact'
+import { useCallback, useContext, useEffect, useState } from 'preact/hooks'
+
+/**
+ * @typedef {import('../../../../types/new-tab.js').WidgetConfig} WidgetConfig
+ * @typedef {import('../../../../types/new-tab.js').WidgetList} WidgetList
+ * @typedef {import("../../../../types/new-tab.js").WidgetConfigItem} WidgetConfigItem
+ * @typedef {import("./widget-config.js").WidgetConfigAPI} WidgetConfigAPI
+ */
+
+export const WidgetConfigContext = createContext({
+ /** @type {WidgetList} */
+ widgets: [],
+
+ /** @type {WidgetConfig} */
+ widgetConfig: [],
+
+ /** @type {(name:string) => void} */
+ hide: () => {
+
+ },
+
+ /** @type {(name:string) => void} */
+ show: () => {
+
+ }
+})
+
+export const WidgetConfigDispatchContext = createContext({
+ dispatch: null
+})
+
+/**
+ * @param {object} props
+ * @param {import("preact").ComponentChild} props.children
+ * @param {WidgetConfig} props.widgetConfig - the initial config data
+ * @param {WidgetList} props.widgets - the initial widget list
+ * @param {WidgetConfigAPI} props.api - the stateful API manager
+ */
+export function WidgetConfigProvider (props) {
+ const [data, setData] = useState(props.widgetConfig)
+
+ useEffect(() => {
+ const unsub = props.api.onUpdate((widgetConfig) => {
+ setData(widgetConfig)
+ })
+ return () => unsub()
+ }, [props.api])
+
+ /**
+ * @param {string} name
+ */
+ function hide (name) {
+ console.log('will hide', name)
+ props.api.hide(name)
+ }
+
+ /**
+ * @param {string} name
+ */
+ function show (name) {
+ console.log('will show', name)
+ props.api.show(name)
+ }
+
+ return (
+
+ {props.children}
+
+ )
+}
+
+const WidgetVisibilityContext = createContext({
+ visibility: /** @type {WidgetConfigItem['visibility']} */('visible'),
+ id: /** @type {WidgetConfigItem['id']} */(''),
+ toggle: () => {
+
+ }
+})
+
+export function useVisibility () {
+ return useContext(WidgetVisibilityContext)
+}
+
+/**
+ * @param {object} props
+ * @param {WidgetConfigItem['id']} props.id - the current id key used for storage
+ * @param {WidgetConfigItem['visibility']} props.visibility - the current id key used for storage
+ * @param {import("preact").ComponentChild} props.children
+ */
+export function WidgetVisibilityProvider (props) {
+ const { widgetConfig, show, hide } = useContext(WidgetConfigContext)
+
+ const toggle = useCallback(() => {
+ const matching = widgetConfig.find(x => x.id === props.id)
+ if (matching?.visibility === 'visible') {
+ hide(props.id)
+ } else {
+ show(props.id)
+ }
+ }, [props.id, widgetConfig])
+
+ return {props.children}
+}
diff --git a/packages/special-pages/pages/new-tab/src/index.html b/packages/special-pages/pages/new-tab/src/index.html
new file mode 100644
index 000000000..6d0102db4
--- /dev/null
+++ b/packages/special-pages/pages/new-tab/src/index.html
@@ -0,0 +1,14 @@
+
+
+
+ New Tab Page
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/special-pages/pages/new-tab/src/js/index.js b/packages/special-pages/pages/new-tab/src/js/index.js
new file mode 100644
index 000000000..55c80a776
--- /dev/null
+++ b/packages/special-pages/pages/new-tab/src/js/index.js
@@ -0,0 +1,79 @@
+import 'preact/devtools'
+/**
+ * @module New Tab Page
+ * @category Special Pages
+ *
+ * New Tab Page
+ *
+ */
+import { init } from '../../app/index.js'
+import {
+ createTypedMessages
+} from '@duckduckgo/messaging'
+import { createSpecialPageMessaging } from '../../../../shared/create-special-page-messaging'
+import { Environment } from '../../../../shared/environment.js'
+import { mockTransport } from './mock-transport.js'
+
+export class NewTabPage {
+ /**
+ * @param {import("@duckduckgo/messaging").Messaging} messaging
+ * @param {ImportMeta['injectName']} injectName
+ */
+ constructor (messaging, injectName) {
+ /**
+ * @internal
+ */
+ this.messaging = createTypedMessages(this, messaging)
+ this.injectName = injectName
+ }
+
+ /**
+ * @return {Promise}
+ */
+ init () {
+ return this.messaging.request('initialSetup')
+ }
+
+ /**
+ * @param {string} message
+ */
+ reportInitException (message) {
+ this.messaging.notify('reportInitException', { message })
+ }
+
+ /**
+ * This will be sent if the application has loaded, but a client-side error
+ * has occurred that cannot be recovered from
+ * @param {{message: string}} params
+ */
+ reportPageException (params) {
+ this.messaging.notify('reportPageException', params)
+ }
+}
+
+const baseEnvironment = new Environment()
+ .withInjectName(import.meta.injectName)
+ .withEnv(import.meta.env)
+
+const messaging = createSpecialPageMessaging({
+ injectName: import.meta.injectName,
+ env: import.meta.env,
+ pageName: 'newTabPage',
+ mockTransport: () => {
+ // only in integration environments
+ if (baseEnvironment.injectName !== 'integration') return null
+ // never in playwright environments
+ if (window.__playwright_01) return null
+ let mock = null
+ // eslint-disable-next-line no-labels
+ $INTEGRATION: mock = mockTransport()
+ return mock
+ }
+})
+
+const newTabMessaging = new NewTabPage(messaging, import.meta.injectName)
+init(newTabMessaging, baseEnvironment).catch(e => {
+ console.error(e)
+ const msg = typeof e?.message === 'string' ? e.message : 'unknown init error'
+ newTabMessaging.reportInitException(msg)
+})
diff --git a/packages/special-pages/pages/new-tab/src/js/inline.js b/packages/special-pages/pages/new-tab/src/js/inline.js
new file mode 100644
index 000000000..6d912bfc5
--- /dev/null
+++ b/packages/special-pages/pages/new-tab/src/js/inline.js
@@ -0,0 +1 @@
+// remove
diff --git a/packages/special-pages/pages/new-tab/src/js/mock-transport.js b/packages/special-pages/pages/new-tab/src/js/mock-transport.js
new file mode 100644
index 000000000..ca6b115d2
--- /dev/null
+++ b/packages/special-pages/pages/new-tab/src/js/mock-transport.js
@@ -0,0 +1,103 @@
+import { TestTransportConfig } from '@duckduckgo/messaging'
+
+export function mockTransport () {
+ const channel = new BroadcastChannel('ntp')
+
+ function broadcast () {
+ setTimeout(() => {
+ channel.postMessage({
+ change: 'ntp.widgetConfig'
+ })
+ }, 100)
+ }
+
+ /**
+ * @param {string} name
+ * @return {Record|null}
+ */
+ function read (name) {
+ try {
+ const item = localStorage.getItem(name)
+ if (!item) return null
+ console.log('did read from LS', item)
+ return JSON.parse(item)
+ } catch (e) {
+ console.error('Failed to parse initialSetup from localStorage', e)
+ return null
+ }
+ }
+
+ /**
+ * @param {string} name
+ * @param {Record} value
+ */
+ function write (name, value) {
+ try {
+ localStorage.setItem(name, JSON.stringify(value))
+ console.log('✅ did write')
+ } catch (e) {
+ console.error('Failed to write', e)
+ }
+ }
+
+ return new TestTransportConfig({
+ notify (msg) {
+ switch (msg.method) {
+ case 'setWidgetConfig': {
+ if (!msg.params) throw new Error('unreachable')
+ write('ntp.widgetConfig', msg.params)
+ broadcast()
+ return
+ }
+ default: {
+ console.warn('unhandled notification', msg)
+ }
+ }
+ },
+ subscribe (sub, cb) {
+ switch (sub.subscriptionName) {
+ case 'onWidgetConfigUpdated': {
+ const controller = new AbortController()
+ // console.log('sub?', sub, cb);
+ channel.addEventListener('message', () => {
+ const values = read('ntp.widgetConfig')
+ if (values) {
+ cb(values)
+ }
+ }, { signal: controller.signal })
+ return () => controller.abort()
+ }
+ }
+ return () => {}
+ },
+ // eslint-ignore-next-line require-await
+ request (msg) {
+ switch (msg.method) {
+ case 'initialSetup': {
+ const widgetsFromStorage = read('ntp.widgets') || {
+ widgets: [
+ { id: 'favorites' },
+ { id: 'privacyStats' }
+ ]
+ }
+ const widgetConfigFromStorage = read('ntp.widgetConfig') || {
+ widgetConfig: [
+ { id: 'favorites', visibility: 'visible' },
+ { id: 'privacyStats', visibility: 'visible' }
+ ]
+ }
+ return Promise.resolve({
+ widgets: widgetsFromStorage.widgets,
+ widgetConfig: widgetConfigFromStorage.widgetConfig,
+ platform: { name: 'integration' },
+ env: 'development',
+ locale: 'en'
+ })
+ }
+ default: {
+ return Promise.reject(new Error('unhandled request'))
+ }
+ }
+ }
+ })
+}
diff --git a/packages/special-pages/pages/new-tab/src/locales/en/newtab.json b/packages/special-pages/pages/new-tab/src/locales/en/newtab.json
new file mode 100644
index 000000000..6d61912fc
--- /dev/null
+++ b/packages/special-pages/pages/new-tab/src/locales/en/newtab.json
@@ -0,0 +1,16 @@
+{
+ "smartling": {
+ "string_format": "icu",
+ "translate_paths": [
+ {
+ "path": "*/title",
+ "key": "{*}/title",
+ "instruction": "*/note"
+ }
+ ]
+ },
+ "helloWorld": {
+ "title": "Hello world!",
+ "note": "here!"
+ }
+}
diff --git a/packages/special-pages/playwright.config.js b/packages/special-pages/playwright.config.js
index 4b10c9c9d..9b3fcb6c5 100644
--- a/packages/special-pages/playwright.config.js
+++ b/packages/special-pages/playwright.config.js
@@ -7,7 +7,8 @@ export default defineConfig({
testMatch: [
'duckplayer.spec.js',
'duckplayer-screenshots.spec.js',
- 'onboarding.spec.js'
+ 'onboarding.spec.js',
+ 'new-tab.spec.js'
],
use: {
...devices['Desktop Edge'],
diff --git a/packages/special-pages/shared/components/Fallback/Fallback.jsx b/packages/special-pages/shared/components/Fallback/Fallback.jsx
new file mode 100644
index 000000000..5555dbdcc
--- /dev/null
+++ b/packages/special-pages/shared/components/Fallback/Fallback.jsx
@@ -0,0 +1,19 @@
+import { h } from "preact";
+import styles from "./Fallback.module.css";
+
+/**
+ * @param {object} props
+ * @param {boolean} props.showDetails
+ */
+export function Fallback({showDetails}) {
+ return (
+
+
+
Something went wrong!
+ {showDetails && (
+
Please check logs for a message called reportPageException
+ )}
+
+
+ )
+}
diff --git a/packages/special-pages/shared/components/Fallback/Fallback.module.css b/packages/special-pages/shared/components/Fallback/Fallback.module.css
new file mode 100644
index 000000000..6e689c17f
--- /dev/null
+++ b/packages/special-pages/shared/components/Fallback/Fallback.module.css
@@ -0,0 +1,4 @@
+.fallback {
+ height: 100%;
+ width: 100%;
+}
diff --git a/packages/special-pages/shared/create-special-page-messaging.js b/packages/special-pages/shared/create-special-page-messaging.js
index 9cdf24d37..7ad91a695 100644
--- a/packages/special-pages/shared/create-special-page-messaging.js
+++ b/packages/special-pages/shared/create-special-page-messaging.js
@@ -12,6 +12,7 @@ import {
* @param {ImportMeta['env']} opts.env
* @param {ImportMeta['injectName']} opts.injectName
* @param {string} opts.pageName
+ * @param {(() => TestTransportConfig|null) | null | undefined} [opts.mockTransport]
* @internal
*/
export function createSpecialPageMessaging (opts) {
@@ -55,7 +56,7 @@ export function createSpecialPageMessaging (opts) {
}
// this fallback allows for the 'integration' target to run without errors
- const fallback = new TestTransportConfig({
+ const fallback = opts.mockTransport?.() || new TestTransportConfig({
/**
* @param {import('@duckduckgo/messaging').NotificationMessage} msg
*/
diff --git a/packages/special-pages/tests/new-tab.spec.js b/packages/special-pages/tests/new-tab.spec.js
new file mode 100644
index 000000000..3ab426a71
--- /dev/null
+++ b/packages/special-pages/tests/new-tab.spec.js
@@ -0,0 +1,65 @@
+import { test, expect } from '@playwright/test'
+import { NewtabPage } from './page-objects/newtab'
+
+test.describe('newtab widgets', () => {
+ test('widget config single click', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo)
+ await ntp.reducedMotion()
+ await ntp.openPage()
+
+ // hide
+ await page.getByRole('button', { name: 'Toggle Privacy Stats' }).click()
+ await expect(page.locator('#app')).toContainText('privacyStats visibility: hidden')
+
+ // debounced
+ await page.waitForTimeout(500)
+
+ // verify the single sync call, where one is hidden
+ const outgoing = await ntp.mocks.outgoing({ names: ['setWidgetConfig'] })
+ expect(outgoing).toStrictEqual([{
+ payload: {
+ context: 'specialPages',
+ featureName: 'newTabPage',
+ params: {
+ widgetConfig: [
+ { id: 'favorites', visibility: 'visible' },
+ { id: 'privacyStats', visibility: 'hidden' }
+ ]
+ },
+ method: 'setWidgetConfig'
+ }
+ }])
+ })
+ test('widget config double click', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo)
+ await ntp.reducedMotion()
+ await ntp.openPage()
+
+ // hide
+ await page.getByRole('button', { name: 'Toggle Privacy Stats' }).click()
+ await expect(page.locator('#app')).toContainText('privacyStats visibility: hidden')
+
+ // show
+ await page.getByRole('button', { name: 'Toggle Privacy Stats' }).click()
+ await expect(page.locator('#app')).toContainText('privacyStats visibility: visible')
+
+ // debounced
+ await page.waitForTimeout(500)
+
+ // verify the single sync call, where both are visible.
+ const outgoing = await ntp.mocks.outgoing({ names: ['setWidgetConfig'] })
+ expect(outgoing).toStrictEqual([{
+ payload: {
+ context: 'specialPages',
+ featureName: 'newTabPage',
+ params: {
+ widgetConfig: [
+ { id: 'favorites', visibility: 'visible' },
+ { id: 'privacyStats', visibility: 'visible' }
+ ]
+ },
+ method: 'setWidgetConfig'
+ }
+ }])
+ })
+})
diff --git a/packages/special-pages/tests/page-objects/newtab.js b/packages/special-pages/tests/page-objects/newtab.js
new file mode 100644
index 000000000..93134560b
--- /dev/null
+++ b/packages/special-pages/tests/page-objects/newtab.js
@@ -0,0 +1,103 @@
+import { Mocks } from './mocks.js'
+import { perPlatform } from '../../../../integration-test/playwright/type-helpers.mjs'
+import { join } from 'node:path'
+
+/**
+ * @typedef {import('../../../../integration-test/playwright/type-helpers.mjs').Build} Build
+ * @typedef {import('../../../../integration-test/playwright/type-helpers.mjs').PlatformInfo} PlatformInfo
+ */
+
+export class NewtabPage {
+ /**
+ * @param {import("@playwright/test").Page} page
+ * @param {Build} build
+ * @param {PlatformInfo} platform
+ */
+ constructor (page, build, platform) {
+ this.page = page
+ this.build = build
+ this.platform = platform
+ this.mocks = new Mocks(page, build, platform, {
+ context: 'specialPages',
+ featureName: 'newTabPage',
+ env: 'development'
+ })
+ this.page.on('console', console.log)
+ // default mocks - just enough to render the first page without error
+ this.mocks.defaultResponses({
+ requestSetAsDefault: {},
+ requestImport: {},
+ /** @type {import('../../types/new-tab.js').InitialSetupResponse} */
+ initialSetup: {
+ widgets: [
+ { id: 'favorites' },
+ { id: 'privacyStats' }
+ ],
+ widgetConfig: [
+ { id: 'favorites', visibility: 'visible' },
+ { id: 'privacyStats', visibility: 'visible' }
+ ],
+ env: 'development',
+ locale: 'en',
+ platform: {
+ name: this.platform.name || 'windows'
+ }
+ }
+ })
+ }
+
+ /**
+ * Opens a page with optional parameters.
+ * This method ensures that mocks are installed and routes are set up before navigating to the page.
+ *
+ * @param {Object} [params] - Optional parameters for opening the page.
+ * @param {'debug' | 'production'} [params.mode] - Optional parameters for opening the page.
+ * @param {boolean} [params.willThrow] - Optional flag to simulate an exception
+ */
+ async openPage ({ mode = 'debug', willThrow = false } = { }) {
+ await this.mocks.install()
+ await this.page.route('/**', (route, req) => {
+ const url = new URL(req.url())
+ // try to serve assets, but change `/` to 'index'
+ let filepath = url.pathname
+ if (filepath === '/') filepath = 'index.html'
+
+ return route.fulfill({
+ status: 200,
+ path: join(this.basePath, filepath)
+ })
+ })
+ const searchParams = new URLSearchParams({ mode, willThrow: String(willThrow) })
+ await this.page.goto('/' + '?' + searchParams.toString())
+ }
+
+ /**
+ * We test the fully built artifacts, so for each test run we need to
+ * select the correct HTML file.
+ * @return {string}
+ */
+ get basePath () {
+ return this.build.switch({
+ windows: () => '../../build/windows/pages/new-tab',
+ integration: () => '../../build/integration/pages/new-tab'
+ })
+ }
+
+ /**
+ * @param {import("@playwright/test").Page} page
+ * @param {import("@playwright/test").TestInfo} testInfo
+ */
+ static create (page, testInfo) {
+ // Read the configuration object to determine which platform we're testing against
+ const { platformInfo, build } = perPlatform(testInfo.project.use)
+ return new NewtabPage(page, build, platformInfo)
+ }
+
+ async reducedMotion () {
+ await this.page.emulateMedia({ reducedMotion: 'reduce' })
+ }
+
+ async darkMode () {
+ await this.page.emulateMedia({ colorScheme: 'dark' })
+ }
+}
diff --git a/packages/special-pages/types/new-tab.ts b/packages/special-pages/types/new-tab.ts
new file mode 100644
index 000000000..48d376a3d
--- /dev/null
+++ b/packages/special-pages/types/new-tab.ts
@@ -0,0 +1,107 @@
+/**
+ * @module NewTab Messages
+ * @description
+ *
+ * These types are auto-generated from schema files.
+ * scripts/build-types.mjs is responsible for type generation.
+ * **DO NOT** edit this file directly as your changes will be lost.
+ */
+
+/**
+ * The visibility state of the widget, as configured by the user
+ */
+export type WidgetVisibility = "visible" | "hidden";
+/**
+ * Configuration settings for widgets
+ */
+export type WidgetConfig = WidgetConfigItem[];
+/**
+ * An ordered list of supported Widgets. Use this to communicate what's supported
+ */
+export type WidgetList = WidgetListItem[];
+
+/**
+ * Requests, Notifications and Subscriptions from the NewTab feature
+ */
+export interface NewTabMessages {
+ notifications: ReportInitExceptionNotification | ReportPageExceptionNotification | SetWidgetConfigNotification;
+ requests: InitialSetupRequest;
+ subscriptions: OnWidgetConfigUpdatedSubscription;
+}
+/**
+ * Generated from @see "../messages/new-tab/reportInitException.notify.json"
+ */
+export interface ReportInitExceptionNotification {
+ method: "reportInitException";
+ params: ReportInitExceptionNotify;
+}
+export interface ReportInitExceptionNotify {
+ message: string;
+}
+/**
+ * Generated from @see "../messages/new-tab/reportPageException.notify.json"
+ */
+export interface ReportPageExceptionNotification {
+ method: "reportPageException";
+ params: ReportPageExceptionNotify;
+}
+export interface ReportPageExceptionNotify {
+ message: string;
+}
+/**
+ * Generated from @see "../messages/new-tab/setWidgetConfig.notify.json"
+ */
+export interface SetWidgetConfigNotification {
+ method: "setWidgetConfig";
+ params: SetWidgetConfigNotify;
+}
+export interface SetWidgetConfigNotify {
+ widgetConfig: WidgetConfig;
+}
+export interface WidgetConfigItem {
+ /**
+ * A unique identifier for the widget.
+ */
+ id: string;
+ visibility: WidgetVisibility;
+}
+/**
+ * Generated from @see "../messages/new-tab/initialSetup.request.json"
+ */
+export interface InitialSetupRequest {
+ method: "initialSetup";
+ result: InitialSetupResponse;
+}
+export interface InitialSetupResponse {
+ widgets: WidgetList;
+ widgetConfig: WidgetConfig;
+ locale: string;
+ env: "development" | "production";
+ platform: {
+ name: "macos" | "windows" | "android" | "ios";
+ };
+}
+export interface WidgetListItem {
+ /**
+ * A unique identifier for the widget.
+ */
+ id: string;
+}
+/**
+ * Generated from @see "../messages/new-tab/onWidgetConfigUpdated.subscribe.json"
+ */
+export interface OnWidgetConfigUpdatedSubscription {
+ subscriptionEvent: "onWidgetConfigUpdated";
+ params: OnWidgetConfigUpdatedSubscribe;
+}
+export interface OnWidgetConfigUpdatedSubscribe {
+ widgetConfig: WidgetConfig;
+}
+
+declare module "../pages/new-tab/src/js/index.js" {
+ export interface NewTabPage {
+ notify: import("@duckduckgo/messaging/lib/shared-types").MessagingBase['notify'],
+ request: import("@duckduckgo/messaging/lib/shared-types").MessagingBase['request'],
+ subscribe: import("@duckduckgo/messaging/lib/shared-types").MessagingBase['subscribe']
+ }
+}
\ No newline at end of file