From df7a7876411ab529ff45b2acf65c3c98610a3e70 Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Thu, 8 Jan 2026 11:04:05 -0800 Subject: [PATCH 01/18] npm dependencies --- package-lock.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index a5905e9..7b5161c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,7 +77,6 @@ "integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -119,7 +118,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -143,7 +141,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -381,7 +378,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -871,7 +867,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2207,7 +2202,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2257,7 +2251,6 @@ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" From e0b7f11d881223716f72ecb23e18474e9aa35a92 Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Thu, 8 Jan 2026 11:04:49 -0800 Subject: [PATCH 02/18] Service files ignored --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f87d27e..888828d 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ docs/TAuth .env.* qodana.yaml bin/ +PLAN.md From 8cf857052d7eb2d294bca687b280c10f7b1cc8f8 Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Thu, 8 Jan 2026 11:10:45 -0800 Subject: [PATCH 03/18] refactor(schema): move subscribeEnabled into subscribe object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed top-level `subscribeEnabled` field from project schema - Added `enabled` property inside `subscribe` object - Updated script.js to read from `subscribe.enabled` - Updated test typedef and filter logic - Fixed JSON syntax error (curly quotes in Poodle description) Schema change: Before: { subscribeEnabled: true, subscribe: { script: "..." } } After: { subscribe: { enabled: true, script: "..." } } πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- data/projects.json | 19 +++---------------- script.js | 4 ++-- tests/hero.spec.js | 4 ++-- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/data/projects.json b/data/projects.json index da7ce50..ea6df95 100644 --- a/data/projects.json +++ b/data/projects.json @@ -11,7 +11,6 @@ "docs": "https://github.com/MarcoPoloResearchLab/marcopolo.github.io/blob/main/ISSUES.md", "launchEnabled": false, "docsEnabled": false, - "subscribeEnabled": false, "icon": "assets/projects/issues-md/icon.png" }, { @@ -25,7 +24,6 @@ "docs": null, "launchEnabled": false, "docsEnabled": false, - "subscribeEnabled": false, "icon": "assets/projects/photolab/icon.svg" }, { @@ -39,7 +37,6 @@ "docs": "https://github.com/tyemirov/ctx#readme", "launchEnabled": true, "docsEnabled": true, - "subscribeEnabled": false, "icon": "assets/projects/ctx/icon.png" }, { @@ -53,7 +50,6 @@ "docs": "https://github.com/tyemirov/gix#readme", "launchEnabled": true, "docsEnabled": true, - "subscribeEnabled": false, "icon": "assets/projects/gix/icon.png" }, { @@ -67,7 +63,6 @@ "docs": "https://github.com/temirov/ghttp#readme", "launchEnabled": true, "docsEnabled": true, - "subscribeEnabled": false, "icon": "assets/projects/ghttp/icon.png" }, { @@ -81,9 +76,9 @@ "docs": "https://github.com/tyemirov/loopaware#readme", "launchEnabled": true, "docsEnabled": true, - "subscribeEnabled": true, "icon": "assets/projects/loopaware/icon.svg", "subscribe": { + "enabled": true, "script": "https://loopaware.mprlab.com/subscribe.js?site_id=a3222433-92ec-473a-9255-0797226c2273&mode=inline&accent=%23ffd369&cta=Subscribe&success=Thanks%20for%20subscribing&name_field=false", "title": "Get LoopAware release updates", "copy": "Drop your email to hear when LoopAware ships fresh drops, integrations, and subscriber tooling." @@ -100,7 +95,6 @@ "docs": "https://github.com/temirov/pinguin#readme", "launchEnabled": true, "docsEnabled": true, - "subscribeEnabled": false, "icon": "assets/projects/pinguin/icon.png" }, { @@ -114,7 +108,6 @@ "docs": "https://github.com/tyemirov/ETS#readme", "launchEnabled": true, "docsEnabled": true, - "subscribeEnabled": false, "icon": "assets/projects/ets/icon.svg" }, { @@ -128,7 +121,6 @@ "docs": "https://github.com/tyemirov/TAuth#readme", "launchEnabled": true, "docsEnabled": true, - "subscribeEnabled": false, "icon": "assets/projects/tauth/icon.svg" }, { @@ -142,13 +134,12 @@ "docs": "https://github.com/tyemirov/ledger#readme", "launchEnabled": true, "docsEnabled": true, - "subscribeEnabled": false, "icon": "assets/projects/ledger/icon.png" }, { "id": "product-scanner", "name": "Poodle Scanner", - "description": "AI-assisted storefront auditor nicknamed β€œPoodle” that sniffs out PDP gaps, evaluates results against configurable rule packs, and reports issues through a CLI and authenticated dashboard.", + "description": "AI-assisted storefront auditor nicknamed 'Poodle' that sniffs out PDP gaps, evaluates results against configurable rule packs, and reports issues through a CLI and authenticated dashboard.", "status": "Beta", "category": "Products", "repo": "github.com/MarcoPoloResearchLab/ProductScanner", @@ -156,7 +147,6 @@ "docs": "https://github.com/MarcoPoloResearchLab/ProductScanner#readme", "launchEnabled": true, "docsEnabled": true, - "subscribeEnabled": false, "icon": "assets/projects/product-scanner/icon.png" }, { @@ -170,7 +160,6 @@ "docs": "https://github.com/MarcoPoloResearchLab/Sheet2Tube#readme", "launchEnabled": true, "docsEnabled": true, - "subscribeEnabled": false, "icon": "assets/projects/sheet2tube/icon.svg" }, { @@ -184,9 +173,9 @@ "docs": "https://github.com/MarcoPoloResearchLab/gravity#readme", "launchEnabled": true, "docsEnabled": true, - "subscribeEnabled": true, "icon": "assets/projects/gravity-notes/icon.png", "subscribe": { + "enabled": true, "script": "https://loopaware.mprlab.com/subscribe.js?site_id=d8c3d1c8-7968-43d0-8026-ee827ada7666&mode=inline&accent=%23ffd369&cta=Subscribe&success=Thanks%20for%20subscribing&name_field=false", "title": "Get Gravity Notes release updates", "copy": "Drop your email to hear when Gravity Notes ships fresh features, AI integrations, and new plugins.", @@ -204,7 +193,6 @@ "docs": "https://github.com/MarcoPoloResearchLab/RSVP#readme", "launchEnabled": true, "docsEnabled": true, - "subscribeEnabled": false, "icon": "assets/projects/rsvp/icon.png" }, { @@ -218,7 +206,6 @@ "docs": "https://github.com/MarcoPoloResearchLab/prompts#readme", "launchEnabled": true, "docsEnabled": true, - "subscribeEnabled": false, "icon": "assets/projects/prompt-bubbles/icon.svg" } ] diff --git a/script.js b/script.js index bd730c9..eafd1d5 100644 --- a/script.js +++ b/script.js @@ -15,13 +15,13 @@ * @property {string | null | undefined} [repo] * @property {boolean | undefined} [launchEnabled] * @property {boolean | undefined} [docsEnabled] - * @property {boolean | undefined} [subscribeEnabled] * @property {string | null | undefined} [icon] * @property {ProjectSubscribeConfig | null | undefined} [subscribe] */ /** * @typedef {Object} ProjectSubscribeConfig + * @property {boolean | undefined} [enabled] * @property {string} script * @property {number | undefined} [height] * @property {string | undefined} [title] @@ -114,7 +114,7 @@ function buildProjectCard(project) { const subscribeConfig = project.subscribe && project.subscribe.script ? project.subscribe : null; const subscribeEnabled = - Boolean(subscribeConfig) && (project.subscribeEnabled !== false); + Boolean(subscribeConfig) && (subscribeConfig.enabled !== false); const hasSubscribeWidget = subscribeEnabled; const isFlippable = hasSubscribeWidget || FLIPPABLE_STATUSES.includes(project.status); if (isFlippable) { diff --git a/tests/hero.spec.js b/tests/hero.spec.js index 17db2d4..cd11627 100644 --- a/tests/hero.spec.js +++ b/tests/hero.spec.js @@ -2,7 +2,7 @@ const {test, expect} = require("@playwright/test"); -/** @type {{projects: Array<{name: string, status: string, category: string, description: string, app?: string|null, launchEnabled?: boolean}>}} */ +/** @type {{projects: Array<{name: string, status: string, category: string, description: string, app?: string|null, launchEnabled?: boolean, subscribe?: {enabled?: boolean, script?: string}}>}} */ const catalog = require("../data/projects.json"); test.describe("Marco Polo Research Lab landing page", () => { @@ -186,7 +186,7 @@ test.describe("Marco Polo Research Lab landing page", () => { project => project.subscribe && project.subscribe.script && - project.subscribeEnabled !== false, + project.subscribe.enabled !== false, ); await page.goto("/index.html"); From bee77a9e10baa71a719d9f557b0a75f9cb76962f Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Thu, 8 Jan 2026 11:16:25 -0800 Subject: [PATCH 04/18] refactor: add explicit subscribe.enabled to all projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added `subscribe: { enabled: false }` to all 13 projects without active subscriptions - Updated test logic to check `subscribe.enabled !== false` combined with `subscribe.script` for determining if subscribe is active - Maintains backward compatibility while making schema explicit πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- data/projects.json | 39 ++++++++++++++++++++++++++------------- tests/hero.spec.js | 6 +++++- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/data/projects.json b/data/projects.json index ea6df95..759eeae 100644 --- a/data/projects.json +++ b/data/projects.json @@ -11,7 +11,8 @@ "docs": "https://github.com/MarcoPoloResearchLab/marcopolo.github.io/blob/main/ISSUES.md", "launchEnabled": false, "docsEnabled": false, - "icon": "assets/projects/issues-md/icon.png" + "icon": "assets/projects/issues-md/icon.png", + "subscribe": { "enabled": false } }, { "id": "photolab", @@ -24,7 +25,8 @@ "docs": null, "launchEnabled": false, "docsEnabled": false, - "icon": "assets/projects/photolab/icon.svg" + "icon": "assets/projects/photolab/icon.svg", + "subscribe": { "enabled": false } }, { "id": "ctx", @@ -37,7 +39,8 @@ "docs": "https://github.com/tyemirov/ctx#readme", "launchEnabled": true, "docsEnabled": true, - "icon": "assets/projects/ctx/icon.png" + "icon": "assets/projects/ctx/icon.png", + "subscribe": { "enabled": false } }, { "id": "gix", @@ -50,7 +53,8 @@ "docs": "https://github.com/tyemirov/gix#readme", "launchEnabled": true, "docsEnabled": true, - "icon": "assets/projects/gix/icon.png" + "icon": "assets/projects/gix/icon.png", + "subscribe": { "enabled": false } }, { "id": "ghttp", @@ -63,7 +67,8 @@ "docs": "https://github.com/temirov/ghttp#readme", "launchEnabled": true, "docsEnabled": true, - "icon": "assets/projects/ghttp/icon.png" + "icon": "assets/projects/ghttp/icon.png", + "subscribe": { "enabled": false } }, { "id": "loopaware", @@ -95,7 +100,8 @@ "docs": "https://github.com/temirov/pinguin#readme", "launchEnabled": true, "docsEnabled": true, - "icon": "assets/projects/pinguin/icon.png" + "icon": "assets/projects/pinguin/icon.png", + "subscribe": { "enabled": false } }, { "id": "ets", @@ -108,7 +114,8 @@ "docs": "https://github.com/tyemirov/ETS#readme", "launchEnabled": true, "docsEnabled": true, - "icon": "assets/projects/ets/icon.svg" + "icon": "assets/projects/ets/icon.svg", + "subscribe": { "enabled": false } }, { "id": "tauth", @@ -121,7 +128,8 @@ "docs": "https://github.com/tyemirov/TAuth#readme", "launchEnabled": true, "docsEnabled": true, - "icon": "assets/projects/tauth/icon.svg" + "icon": "assets/projects/tauth/icon.svg", + "subscribe": { "enabled": false } }, { "id": "ledger", @@ -134,7 +142,8 @@ "docs": "https://github.com/tyemirov/ledger#readme", "launchEnabled": true, "docsEnabled": true, - "icon": "assets/projects/ledger/icon.png" + "icon": "assets/projects/ledger/icon.png", + "subscribe": { "enabled": false } }, { "id": "product-scanner", @@ -147,7 +156,8 @@ "docs": "https://github.com/MarcoPoloResearchLab/ProductScanner#readme", "launchEnabled": true, "docsEnabled": true, - "icon": "assets/projects/product-scanner/icon.png" + "icon": "assets/projects/product-scanner/icon.png", + "subscribe": { "enabled": false } }, { "id": "sheet2tube", @@ -160,7 +170,8 @@ "docs": "https://github.com/MarcoPoloResearchLab/Sheet2Tube#readme", "launchEnabled": true, "docsEnabled": true, - "icon": "assets/projects/sheet2tube/icon.svg" + "icon": "assets/projects/sheet2tube/icon.svg", + "subscribe": { "enabled": false } }, { "id": "gravity-notes", @@ -193,7 +204,8 @@ "docs": "https://github.com/MarcoPoloResearchLab/RSVP#readme", "launchEnabled": true, "docsEnabled": true, - "icon": "assets/projects/rsvp/icon.png" + "icon": "assets/projects/rsvp/icon.png", + "subscribe": { "enabled": false } }, { "id": "prompt-bubbles", @@ -206,7 +218,8 @@ "docs": "https://github.com/MarcoPoloResearchLab/prompts#readme", "launchEnabled": true, "docsEnabled": true, - "icon": "assets/projects/prompt-bubbles/icon.svg" + "icon": "assets/projects/prompt-bubbles/icon.svg", + "subscribe": { "enabled": false } } ] } diff --git a/tests/hero.spec.js b/tests/hero.spec.js index cd11627..e57c03e 100644 --- a/tests/hero.spec.js +++ b/tests/hero.spec.js @@ -73,10 +73,14 @@ test.describe("Marco Polo Research Lab landing page", () => { const classesAfterClick = await card.getAttribute("class"); + const hasActiveSubscribe = + project.subscribe && + project.subscribe.enabled !== false && + project.subscribe.script; const shouldFlip = project.status === "Beta" || project.status === "WIP" || - Boolean(project.subscribe); + hasActiveSubscribe; if (shouldFlip) { expect(classesAfterClick || "").toMatch(/is-flipped/); From 0f4e464a169d375fbd1b42189a848a1bf49b6600 Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Thu, 8 Jan 2026 11:19:47 -0800 Subject: [PATCH 05/18] refactor: consolidate launch and docs into configuration objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replaced `app`/`launchEnabled` with `launch: { enabled, url }` - Replaced `docs`/`docsEnabled` with `docs: { enabled, url }` - Normalized schema: all config objects (launch, docs, subscribe) follow the same `{ enabled: boolean, ...config }` pattern - Updated script.js typedefs and card rendering logic - Updated tests to use new schema accessors πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- data/projects.json | 174 +++++++++++++++++++++++++++++---------------- script.js | 43 ++++++----- tests/hero.spec.js | 20 +++--- 3 files changed, 149 insertions(+), 88 deletions(-) diff --git a/data/projects.json b/data/projects.json index 759eeae..1e30506 100644 --- a/data/projects.json +++ b/data/projects.json @@ -7,11 +7,15 @@ "status": "WIP", "category": "Research", "repo": "github.com/MarcoPoloResearchLab/marcopolo.github.io", - "app": "https://github.com/MarcoPoloResearchLab/marcopolo.github.io/blob/main/ISSUES.md", - "docs": "https://github.com/MarcoPoloResearchLab/marcopolo.github.io/blob/main/ISSUES.md", - "launchEnabled": false, - "docsEnabled": false, "icon": "assets/projects/issues-md/icon.png", + "launch": { + "enabled": false, + "url": "https://github.com/MarcoPoloResearchLab/marcopolo.github.io/blob/main/ISSUES.md" + }, + "docs": { + "enabled": false, + "url": "https://github.com/MarcoPoloResearchLab/marcopolo.github.io/blob/main/ISSUES.md" + }, "subscribe": { "enabled": false } }, { @@ -21,11 +25,9 @@ "status": "WIP", "category": "Research", "repo": null, - "app": null, - "docs": null, - "launchEnabled": false, - "docsEnabled": false, "icon": "assets/projects/photolab/icon.svg", + "launch": { "enabled": false }, + "docs": { "enabled": false }, "subscribe": { "enabled": false } }, { @@ -35,11 +37,15 @@ "status": "Production", "category": "Tools", "repo": "github.com/tyemirov/ctx", - "app": "https://ctx.mprlab.com", - "docs": "https://github.com/tyemirov/ctx#readme", - "launchEnabled": true, - "docsEnabled": true, "icon": "assets/projects/ctx/icon.png", + "launch": { + "enabled": true, + "url": "https://ctx.mprlab.com" + }, + "docs": { + "enabled": true, + "url": "https://github.com/tyemirov/ctx#readme" + }, "subscribe": { "enabled": false } }, { @@ -49,11 +55,15 @@ "status": "Production", "category": "Tools", "repo": "github.com/tyemirov/gix", - "app": "https://gix.mprlab.com", - "docs": "https://github.com/tyemirov/gix#readme", - "launchEnabled": true, - "docsEnabled": true, "icon": "assets/projects/gix/icon.png", + "launch": { + "enabled": true, + "url": "https://gix.mprlab.com" + }, + "docs": { + "enabled": true, + "url": "https://github.com/tyemirov/gix#readme" + }, "subscribe": { "enabled": false } }, { @@ -63,11 +73,15 @@ "status": "Production", "category": "Tools", "repo": "github.com/temirov/ghttp", - "app": "https://github.com/temirov/ghttp", - "docs": "https://github.com/temirov/ghttp#readme", - "launchEnabled": true, - "docsEnabled": true, "icon": "assets/projects/ghttp/icon.png", + "launch": { + "enabled": true, + "url": "https://github.com/temirov/ghttp" + }, + "docs": { + "enabled": true, + "url": "https://github.com/temirov/ghttp#readme" + }, "subscribe": { "enabled": false } }, { @@ -77,11 +91,15 @@ "status": "Production", "category": "Platform", "repo": "github.com/tyemirov/loopaware", - "app": "https://loopaware.mprlab.com", - "docs": "https://github.com/tyemirov/loopaware#readme", - "launchEnabled": true, - "docsEnabled": true, "icon": "assets/projects/loopaware/icon.svg", + "launch": { + "enabled": true, + "url": "https://loopaware.mprlab.com" + }, + "docs": { + "enabled": true, + "url": "https://github.com/tyemirov/loopaware#readme" + }, "subscribe": { "enabled": true, "script": "https://loopaware.mprlab.com/subscribe.js?site_id=a3222433-92ec-473a-9255-0797226c2273&mode=inline&accent=%23ffd369&cta=Subscribe&success=Thanks%20for%20subscribing&name_field=false", @@ -96,11 +114,15 @@ "status": "Production", "category": "Platform", "repo": "github.com/temirov/pinguin", - "app": "https://github.com/temirov/pinguin", - "docs": "https://github.com/temirov/pinguin#readme", - "launchEnabled": true, - "docsEnabled": true, "icon": "assets/projects/pinguin/icon.png", + "launch": { + "enabled": true, + "url": "https://github.com/temirov/pinguin" + }, + "docs": { + "enabled": true, + "url": "https://github.com/temirov/pinguin#readme" + }, "subscribe": { "enabled": false } }, { @@ -110,11 +132,15 @@ "status": "Beta", "category": "Platform", "repo": "github.com/tyemirov/ETS", - "app": "https://ets.mprlab.com", - "docs": "https://github.com/tyemirov/ETS#readme", - "launchEnabled": true, - "docsEnabled": true, "icon": "assets/projects/ets/icon.svg", + "launch": { + "enabled": true, + "url": "https://ets.mprlab.com" + }, + "docs": { + "enabled": true, + "url": "https://github.com/tyemirov/ETS#readme" + }, "subscribe": { "enabled": false } }, { @@ -124,11 +150,15 @@ "status": "Production", "category": "Platform", "repo": "github.com/tyemirov/TAuth", - "app": "https://tauth.mprlab.com", - "docs": "https://github.com/tyemirov/TAuth#readme", - "launchEnabled": true, - "docsEnabled": true, "icon": "assets/projects/tauth/icon.svg", + "launch": { + "enabled": true, + "url": "https://tauth.mprlab.com" + }, + "docs": { + "enabled": true, + "url": "https://github.com/tyemirov/TAuth#readme" + }, "subscribe": { "enabled": false } }, { @@ -138,11 +168,15 @@ "status": "Beta", "category": "Platform", "repo": "github.com/tyemirov/ledger", - "app": "https://github.com/tyemirov/ledger", - "docs": "https://github.com/tyemirov/ledger#readme", - "launchEnabled": true, - "docsEnabled": true, "icon": "assets/projects/ledger/icon.png", + "launch": { + "enabled": true, + "url": "https://github.com/tyemirov/ledger" + }, + "docs": { + "enabled": true, + "url": "https://github.com/tyemirov/ledger#readme" + }, "subscribe": { "enabled": false } }, { @@ -152,11 +186,15 @@ "status": "Beta", "category": "Products", "repo": "github.com/MarcoPoloResearchLab/ProductScanner", - "app": "https://ps.mprlab.com", - "docs": "https://github.com/MarcoPoloResearchLab/ProductScanner#readme", - "launchEnabled": true, - "docsEnabled": true, "icon": "assets/projects/product-scanner/icon.png", + "launch": { + "enabled": true, + "url": "https://ps.mprlab.com" + }, + "docs": { + "enabled": true, + "url": "https://github.com/MarcoPoloResearchLab/ProductScanner#readme" + }, "subscribe": { "enabled": false } }, { @@ -166,11 +204,15 @@ "status": "Beta", "category": "Products", "repo": "github.com/MarcoPoloResearchLab/Sheet2Tube", - "app": "https://sheet2tube.mprlab.com", - "docs": "https://github.com/MarcoPoloResearchLab/Sheet2Tube#readme", - "launchEnabled": true, - "docsEnabled": true, "icon": "assets/projects/sheet2tube/icon.svg", + "launch": { + "enabled": true, + "url": "https://sheet2tube.mprlab.com" + }, + "docs": { + "enabled": true, + "url": "https://github.com/MarcoPoloResearchLab/Sheet2Tube#readme" + }, "subscribe": { "enabled": false } }, { @@ -180,11 +222,15 @@ "status": "Production", "category": "Products", "repo": "github.com/MarcoPoloResearchLab/gravity", - "app": "https://gravity.mprlab.com", - "docs": "https://github.com/MarcoPoloResearchLab/gravity#readme", - "launchEnabled": true, - "docsEnabled": true, "icon": "assets/projects/gravity-notes/icon.png", + "launch": { + "enabled": true, + "url": "https://gravity.mprlab.com" + }, + "docs": { + "enabled": true, + "url": "https://github.com/MarcoPoloResearchLab/gravity#readme" + }, "subscribe": { "enabled": true, "script": "https://loopaware.mprlab.com/subscribe.js?site_id=d8c3d1c8-7968-43d0-8026-ee827ada7666&mode=inline&accent=%23ffd369&cta=Subscribe&success=Thanks%20for%20subscribing&name_field=false", @@ -200,11 +246,15 @@ "status": "Production", "category": "Products", "repo": "github.com/MarcoPoloResearchLab/RSVP", - "app": "https://rsvp.mprlab.com", - "docs": "https://github.com/MarcoPoloResearchLab/RSVP#readme", - "launchEnabled": true, - "docsEnabled": true, "icon": "assets/projects/rsvp/icon.png", + "launch": { + "enabled": true, + "url": "https://rsvp.mprlab.com" + }, + "docs": { + "enabled": true, + "url": "https://github.com/MarcoPoloResearchLab/RSVP#readme" + }, "subscribe": { "enabled": false } }, { @@ -214,11 +264,15 @@ "status": "Production", "category": "Products", "repo": "github.com/MarcoPoloResearchLab/prompts", - "app": "https://prompts.mprlab.com", - "docs": "https://github.com/MarcoPoloResearchLab/prompts#readme", - "launchEnabled": true, - "docsEnabled": true, "icon": "assets/projects/prompt-bubbles/icon.svg", + "launch": { + "enabled": true, + "url": "https://prompts.mprlab.com" + }, + "docs": { + "enabled": true, + "url": "https://github.com/MarcoPoloResearchLab/prompts#readme" + }, "subscribe": { "enabled": false } } ] diff --git a/script.js b/script.js index eafd1d5..9488ab2 100644 --- a/script.js +++ b/script.js @@ -10,19 +10,29 @@ * @property {string} description * @property {ProjectStatus} status * @property {ProjectCategory} category - * @property {string | null} app - * @property {string | null | undefined} [docs] * @property {string | null | undefined} [repo] - * @property {boolean | undefined} [launchEnabled] - * @property {boolean | undefined} [docsEnabled] * @property {string | null | undefined} [icon] - * @property {ProjectSubscribeConfig | null | undefined} [subscribe] + * @property {ProjectLaunchConfig} launch + * @property {ProjectDocsConfig} docs + * @property {ProjectSubscribeConfig} subscribe + */ + +/** + * @typedef {Object} ProjectLaunchConfig + * @property {boolean} enabled + * @property {string | undefined} [url] + */ + +/** + * @typedef {Object} ProjectDocsConfig + * @property {boolean} enabled + * @property {string | undefined} [url] */ /** * @typedef {Object} ProjectSubscribeConfig - * @property {boolean | undefined} [enabled] - * @property {string} script + * @property {boolean} enabled + * @property {string | undefined} [script] * @property {number | undefined} [height] * @property {string | undefined} [title] * @property {string | undefined} [copy] @@ -112,9 +122,8 @@ function buildProjectCard(project) { const inner = document.createElement("div"); inner.className = "project-card-inner"; - const subscribeConfig = project.subscribe && project.subscribe.script ? project.subscribe : null; - const subscribeEnabled = - Boolean(subscribeConfig) && (subscribeConfig.enabled !== false); + const subscribeConfig = project.subscribe; + const subscribeEnabled = subscribeConfig.enabled && Boolean(subscribeConfig.script); const hasSubscribeWidget = subscribeEnabled; const isFlippable = hasSubscribeWidget || FLIPPABLE_STATUSES.includes(project.status); if (isFlippable) { @@ -157,17 +166,17 @@ function buildProjectCard(project) { const shouldShowLaunch = project.status !== "WIP" && - Boolean(project.app) && - (project.launchEnabled !== false); + project.launch.enabled && + Boolean(project.launch.url); - const shouldShowDocs = Boolean(project.docs) && (project.docsEnabled !== false); + const shouldShowDocs = project.docs.enabled && Boolean(project.docs.url); const actionsRow = document.createElement("div"); actionsRow.className = "card-actions"; - if (shouldShowLaunch && project.app) { + if (shouldShowLaunch && project.launch.url) { const link = document.createElement("a"); - link.href = project.app; + link.href = project.launch.url; link.className = "card-action"; link.target = "_blank"; link.rel = "noreferrer noopener"; @@ -175,9 +184,9 @@ function buildProjectCard(project) { actionsRow.append(link); } - if (shouldShowDocs && project.docs) { + if (shouldShowDocs && project.docs.url) { const docsLink = document.createElement("a"); - docsLink.href = project.docs; + docsLink.href = project.docs.url; docsLink.className = "card-action"; docsLink.target = "_blank"; docsLink.rel = "noreferrer noopener"; diff --git a/tests/hero.spec.js b/tests/hero.spec.js index e57c03e..beffb1e 100644 --- a/tests/hero.spec.js +++ b/tests/hero.spec.js @@ -2,7 +2,7 @@ const {test, expect} = require("@playwright/test"); -/** @type {{projects: Array<{name: string, status: string, category: string, description: string, app?: string|null, launchEnabled?: boolean, subscribe?: {enabled?: boolean, script?: string}}>}} */ +/** @type {{projects: Array<{name: string, status: string, category: string, description: string, launch: {enabled: boolean, url?: string}, docs: {enabled: boolean, url?: string}, subscribe: {enabled: boolean, script?: string}}>}} */ const catalog = require("../data/projects.json"); test.describe("Marco Polo Research Lab landing page", () => { @@ -45,12 +45,12 @@ test.describe("Marco Polo Research Lab landing page", () => { const action = card.locator("a.card-action").first(); const expectLaunchVisible = project.status !== "WIP" && - Boolean(project.app) && - (project.launchEnabled !== false); + project.launch.enabled && + Boolean(project.launch.url); await expect(action).toHaveCount(expectLaunchVisible ? 1 : 0); - if (expectLaunchVisible) { - await expect(action).toHaveAttribute("href", project.app); + if (expectLaunchVisible && project.launch.url) { + await expect(action).toHaveAttribute("href", project.launch.url); } } }); @@ -74,9 +74,8 @@ test.describe("Marco Polo Research Lab landing page", () => { const classesAfterClick = await card.getAttribute("class"); const hasActiveSubscribe = - project.subscribe && - project.subscribe.enabled !== false && - project.subscribe.script; + project.subscribe.enabled && + Boolean(project.subscribe.script); const shouldFlip = project.status === "Beta" || project.status === "WIP" || @@ -188,9 +187,8 @@ test.describe("Marco Polo Research Lab landing page", () => { test("subscribe-enabled cards render LoopAware forms after flipping", async ({page}) => { const subscribeProjects = catalog.projects.filter( project => - project.subscribe && - project.subscribe.script && - project.subscribe.enabled !== false, + project.subscribe.enabled && + Boolean(project.subscribe.script), ); await page.goto("/index.html"); From 04864a5b742e4a5ade06a2c8f7cf6c1701f9deea Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Thu, 8 Jan 2026 13:21:44 -0800 Subject: [PATCH 06/18] fix: remove duplicate mpr-band custom element registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mpr-ui CDN already defines the mpr-band custom element. Loading the local mpr-band.js caused Safari to throw: "NotSupportedError: Cannot define multiple custom elements with the same tag name" Removed the redundant local script and deleted the unused mpr-band.js. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- index.html | 1 - mpr-band.js | 25 ------------------------- 2 files changed, 26 deletions(-) delete mode 100644 mpr-band.js diff --git a/index.html b/index.html index 5201dd5..d42ecf2 100644 --- a/index.html +++ b/index.html @@ -192,7 +192,6 @@

Products

> - diff --git a/mpr-band.js b/mpr-band.js deleted file mode 100644 index 91d3c22..0000000 --- a/mpr-band.js +++ /dev/null @@ -1,25 +0,0 @@ -// @ts-check - -class MprBand extends HTMLElement { - constructor() { - super(); - const shadowRoot = this.attachShadow({mode: "open"}); - shadowRoot.innerHTML = ` - - - `; - } -} - -customElements.define("mpr-band", MprBand); From 536af31bbdce972df34994384c57579f4831ac75 Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Thu, 8 Jan 2026 13:29:47 -0800 Subject: [PATCH 07/18] local development runs with SSL certs --- docker-compose.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 27b8f59..174a514 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,8 +11,14 @@ services: - "${GHTTP_BIND_ADDRESS}" - "--directory" - "${GHTTP_SERVE_DIRECTORY}" + - "--tls-cert" + - ${GHTTP_TLS_CERT_CONTAINER_PATH} + - "--tls-key" + - "${GHTTP_TLS_KEY_CONTAINER_PATH}" - "${CONTAINER_HTTP_PORT}" ports: - "${HOST_HTTP_PORT}:${CONTAINER_HTTP_PORT}" volumes: - ./:${GHTTP_SERVE_DIRECTORY}:ro + - ${GHTTP_TLS_CERT_HOST_PATH}:${GHTTP_TLS_CERT_CONTAINER_PATH}:ro + - ${GHTTP_TLS_KEY_HOST_PATH}:${GHTTP_TLS_KEY_CONTAINER_PATH}:ro \ No newline at end of file From 991930dc160b2c781bc49e3b36af029af7580f35 Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Thu, 8 Jan 2026 22:44:09 -0800 Subject: [PATCH 08/18] fix: resolve Safari iframe positioning in flipped cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fetch and inline LoopAware script content to bypass cross-origin restrictions in Safari's srcdoc iframe handling - Remove rotateY(180deg) transform from subscribe overlay that caused Safari to miscalculate iframe position (~640px off-screen) - Add CSS overrides for subscribe form to match dark theme - Remove inner border/card styling for cleaner appearance - Document MP-302 bug and fix in ISSUES.md πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- ISSUES.md | 10 ++++++++ script.js | 73 +++++++++++++++++++++++++++++++++++++++++++++++++----- styles.css | 21 ++++++---------- 3 files changed, 85 insertions(+), 19 deletions(-) diff --git a/ISSUES.md b/ISSUES.md index be5e186..75fff61 100644 --- a/ISSUES.md +++ b/ISSUES.md @@ -107,6 +107,16 @@ Each issue is formatted as `- [ ] [-]`. When resolved it becomes -` - Root cause: The mpr-ui CDN stylesheet likely sets `position: relative` on the `mpr-footer` custom element, and our local `styles.css:536-541` does not explicitly override it to `static`. - Fix: Add `position: static;` to the `mpr-footer { ... }` rule block in `styles.css` at line 536 to ensure the host element is not positioned, regardless of CDN defaults. - Verify fix passes: `npx playwright test --grep "footer respects non-sticky"`. +- [ ] [MP-302] Subscribe iframe positioned off-screen in Safari due to nested 3D transform issues + - Symptom: LoopAware subscribe form renders inside iframe (verified via `contentDocument.body.innerHTML`) but is visually invisible when card is flipped. + - Debug findings: + - Overlay `getBoundingClientRect()`: `y: 61.9` (correct, visible) + - Iframe `getBoundingClientRect()`: `y: -580` (incorrect, ~640px above viewport) + - Iframe dimensions: `436x180` (correct) + - Iframe content: form with `#mp-subscribe-form` renders correctly + - Root cause: Safari miscalculates iframe position within nested 3D-transformed containers. The `.project-card-subscribe-overlay` has `transform: rotateY(180deg)` to counter the card flip, but Safari's compositor places the iframe at pre-transform coordinates. + - Additional issue: Cross-origin script loading from `srcdoc` iframe was blocked by Safari; fixed by fetching script content and inlining it. + - Fix: Flatten 3D context for subscribe widget content using `transform-style: flat` on the overlay, ensuring child elements use 2D positioning within the already-transformed container. ## Maintenance (400–499) diff --git a/script.js b/script.js index 9488ab2..b9aebbc 100644 --- a/script.js +++ b/script.js @@ -63,11 +63,10 @@ const FLIPPABLE_STATUSES = ["Beta", "WIP"]; /** * Generates inline HTML used inside the subscribe iframe so the LoopAware script * can render its real widget without leaking global styles into the page. - * @param {string} scriptUrl + * @param {string} scriptContent * @returns {string} */ -function buildSubscribeFrameDocument(scriptUrl) { - const safeUrl = String(scriptUrl).replace(/"/g, """); +function buildSubscribeFrameDocument(scriptContent) { return ` @@ -83,14 +82,73 @@ function buildSubscribeFrameDocument(scriptUrl) { background: transparent; font-family: "Space Grotesk", "Roboto", sans-serif; } + + #mp-subscribe-form { + background: transparent !important; + border: none !important; + box-shadow: none !important; + padding: 0 !important; + max-width: 100% !important; + } + + #mp-subscribe-form > div:first-child { + display: none !important; + } + + #mp-subscribe-form input { + background: rgba(255, 255, 255, 0.08) !important; + border: 1px solid rgba(255, 255, 255, 0.2) !important; + color: #fff !important; + } + + #mp-subscribe-form input::placeholder { + color: rgba(255, 255, 255, 0.4) !important; + } + + #mp-subscribe-form button { + background: #ffd369 !important; + color: #0a1a1f !important; + } + + #mp-subscribe-form #mp-subscribe-status { + color: rgba(255, 255, 255, 0.7) !important; + } - + `; } +/** @type {Map>} */ +const scriptCache = new Map(); + +/** + * Fetches and caches the LoopAware subscribe script content. + * @param {string} scriptUrl + * @returns {Promise} + */ +async function fetchSubscribeScript(scriptUrl) { + if (scriptCache.has(scriptUrl)) { + return scriptCache.get(scriptUrl); + } + const promise = fetch(scriptUrl) + .then(response => { + if (!response.ok) { + throw new Error(`Failed to fetch subscribe script: ${response.status}`); + } + return response.text(); + }) + .catch(error => { + scriptCache.delete(scriptUrl); + console.error("Subscribe script fetch error:", error); + return ""; + }); + scriptCache.set(scriptUrl, promise); + return promise; +} + /** * Fetches the JSON catalog for the landing page. * @returns {Promise} @@ -267,7 +325,7 @@ function buildProjectCard(project) { let subscribeFrameLoaded = false; - loadSubscribeWidget = () => { + loadSubscribeWidget = async () => { if (subscribeFrameLoaded) return; subscribeFrameLoaded = true; const overlayRect = subscribeOverlay.getBoundingClientRect(); @@ -279,7 +337,10 @@ function buildProjectCard(project) { subscribeFrame.addEventListener("load", () => { subscribeOverlay.dataset.subscribeLoaded = "true"; }, {once: true}); - subscribeFrame.srcdoc = buildSubscribeFrameDocument(subscribeConfig.script); + const scriptContent = await fetchSubscribeScript(subscribeConfig.script); + if (scriptContent) { + subscribeFrame.srcdoc = buildSubscribeFrameDocument(scriptContent); + } }; } back.append(backHeader, backBody); diff --git a/styles.css b/styles.css index 53bb5f9..df5b9c6 100644 --- a/styles.css +++ b/styles.css @@ -470,10 +470,6 @@ a:focus { .subscribe-widget { padding: 1rem; - border-radius: 18px; - border: 1px solid var(--accent-outline); - background: rgba(0, 40, 46, 0.6); - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02); overflow: hidden; height: 100%; display: flex; @@ -503,33 +499,32 @@ a:focus { height: 240px; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); overflow: hidden; + transform: translateZ(0); + backface-visibility: hidden; } .project-card-subscribe-overlay { position: absolute; - inset: 24px; + inset: 0; display: flex; flex-direction: column; gap: 0.75rem; opacity: 0; pointer-events: none; - transform: rotateY(180deg) translateY(12px); + transform: translateY(12px); transition: opacity 0.3s ease, transform 0.3s ease; z-index: 2; - backface-visibility: hidden; overflow: hidden; - padding: 0; - border-radius: 20px; - border: 1px solid rgba(255, 211, 105, 0.15); - background: radial-gradient(circle at top, rgba(3, 38, 46, 0.9), rgba(2, 20, 25, 0.85)); - box-shadow: 0 25px 60px rgba(0, 0, 0, 0.6); + padding: 1.5rem; + border-radius: 24px; + background: rgba(2, 20, 25, 0.95); backdrop-filter: blur(18px); } .project-card-flippable.is-flipped .project-card-subscribe-overlay { opacity: 1; pointer-events: auto; - transform: rotateY(180deg) translateY(0); + transform: translateY(0); } /* ---------- Footer ---------- */ From 20e717b7eaaf4623625f5a5902ea0303e27f744c Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Thu, 8 Jan 2026 23:07:34 -0800 Subject: [PATCH 09/18] fix: preserve LoopAware URL parameters and API endpoint in srcdoc iframe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The LoopAware subscribe.js reads configuration from its script tag's src attribute and derives API endpoint from location. In srcdoc iframes: - scriptTag.src is empty (no URL params) - location is "about:srcdoc" (wrong API endpoint) Patch the inlined script to: 1. Use original URL for config parsing (name_field, accent, etc.) 2. Use correct LoopAware host for API calls instead of location πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- script.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/script.js b/script.js index b9aebbc..43c926d 100644 --- a/script.js +++ b/script.js @@ -64,9 +64,27 @@ const FLIPPABLE_STATUSES = ["Beta", "WIP"]; * Generates inline HTML used inside the subscribe iframe so the LoopAware script * can render its real widget without leaking global styles into the page. * @param {string} scriptContent + * @param {string} scriptUrl - Original URL with query parameters for config parsing * @returns {string} */ -function buildSubscribeFrameDocument(scriptContent) { +function buildSubscribeFrameDocument(scriptContent, scriptUrl) { + // Extract base URL for API endpoint (e.g., https://loopaware.mprlab.com) + const urlObj = new URL(scriptUrl); + const baseUrl = `${urlObj.protocol}//${urlObj.host}`; + + // Patch the script to work inside srcdoc iframe: + // 1. Replace scriptTag.src fallback with original URL (for config parsing) + // 2. Replace location-based endpoint with correct LoopAware URL + // (srcdoc iframes have origin "about:srcdoc" which can't make cross-origin requests) + const patchedScript = scriptContent + .replace( + /scriptTag\.src\s*\|\|\s*""/g, + JSON.stringify(scriptUrl) + ) + .replace( + /location\.protocol\s*\+\s*"\/\/"\s*\+\s*location\.host/g, + JSON.stringify(baseUrl) + ); return ` @@ -116,7 +134,7 @@ function buildSubscribeFrameDocument(scriptContent) { - + `; } @@ -339,7 +357,7 @@ function buildProjectCard(project) { }, {once: true}); const scriptContent = await fetchSubscribeScript(subscribeConfig.script); if (scriptContent) { - subscribeFrame.srcdoc = buildSubscribeFrameDocument(scriptContent); + subscribeFrame.srcdoc = buildSubscribeFrameDocument(scriptContent, subscribeConfig.script); } }; } From 8789794ab0f888d3819410a570bf549dbb33cf25 Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Fri, 9 Jan 2026 13:46:03 -0800 Subject: [PATCH 10/18] refactor: replace iframe with direct script embedding for subscribe widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove iframe/srcdoc approach that caused CORS issues in Safari (srcdoc iframes have Origin: null which cannot make cross-origin requests). Instead, load LoopAware subscribe.js directly on the page and move the form into the card container after it renders. This is a workaround until LoopAware implements LA-113 (target parameter support). Changes: - Remove iframe creation and srcdoc document building - Load subscribe.js via script tag, move form after render - Update CSS for direct form styling (no iframe) - Update Playwright tests for non-iframe form assertions πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- script.js | 185 +++++++++++++-------------------------------- styles.css | 51 ++++++++++--- tests/hero.spec.js | 25 ++---- 3 files changed, 101 insertions(+), 160 deletions(-) diff --git a/script.js b/script.js index 43c926d..80fe725 100644 --- a/script.js +++ b/script.js @@ -60,111 +60,54 @@ const STATUS_CLASS = Object.freeze({ /** @type {ProjectStatus[]} */ const FLIPPABLE_STATUSES = ["Beta", "WIP"]; -/** - * Generates inline HTML used inside the subscribe iframe so the LoopAware script - * can render its real widget without leaking global styles into the page. - * @param {string} scriptContent - * @param {string} scriptUrl - Original URL with query parameters for config parsing - * @returns {string} - */ -function buildSubscribeFrameDocument(scriptContent, scriptUrl) { - // Extract base URL for API endpoint (e.g., https://loopaware.mprlab.com) - const urlObj = new URL(scriptUrl); - const baseUrl = `${urlObj.protocol}//${urlObj.host}`; - - // Patch the script to work inside srcdoc iframe: - // 1. Replace scriptTag.src fallback with original URL (for config parsing) - // 2. Replace location-based endpoint with correct LoopAware URL - // (srcdoc iframes have origin "about:srcdoc" which can't make cross-origin requests) - const patchedScript = scriptContent - .replace( - /scriptTag\.src\s*\|\|\s*""/g, - JSON.stringify(scriptUrl) - ) - .replace( - /location\.protocol\s*\+\s*"\/\/"\s*\+\s*location\.host/g, - JSON.stringify(baseUrl) - ); - return ` - - - - - - - - - -`; -} - -/** @type {Map>} */ -const scriptCache = new Map(); +/** @type {Set} */ +const loadedSubscribeScripts = new Set(); /** - * Fetches and caches the LoopAware subscribe script content. - * @param {string} scriptUrl - * @returns {Promise} + * Loads the LoopAware subscribe script and moves the form to the target container. + * Workaround until LoopAware implements LA-113 (target parameter support). + * @param {string} scriptUrl - Full URL with query parameters + * @param {HTMLElement} targetContainer - Container to move the form into + * @returns {Promise} */ -async function fetchSubscribeScript(scriptUrl) { - if (scriptCache.has(scriptUrl)) { - return scriptCache.get(scriptUrl); - } - const promise = fetch(scriptUrl) - .then(response => { - if (!response.ok) { - throw new Error(`Failed to fetch subscribe script: ${response.status}`); +function loadSubscribeScript(scriptUrl, targetContainer) { + return new Promise((resolve) => { + // Script already loaded - form exists, move it + if (loadedSubscribeScripts.has(scriptUrl)) { + const form = document.getElementById("mp-subscribe-form"); + if (form && !targetContainer.contains(form)) { + targetContainer.appendChild(form); } - return response.text(); - }) - .catch(error => { - scriptCache.delete(scriptUrl); - console.error("Subscribe script fetch error:", error); - return ""; - }); - scriptCache.set(scriptUrl, promise); - return promise; + resolve(); + return; + } + + const script = document.createElement("script"); + script.src = scriptUrl; + script.async = true; + + script.addEventListener("load", () => { + loadedSubscribeScripts.add(scriptUrl); + // Wait for form to be created, then move it to target + const moveForm = () => { + const form = document.getElementById("mp-subscribe-form"); + if (form) { + targetContainer.appendChild(form); + resolve(); + } else { + requestAnimationFrame(moveForm); + } + }; + moveForm(); + }, {once: true}); + + script.addEventListener("error", () => { + console.error("Failed to load subscribe script:", scriptUrl); + resolve(); + }, {once: true}); + + document.head.appendChild(script); + }); } /** @@ -327,38 +270,23 @@ function buildProjectCard(project) { subscribeConfig.copy || "Leave your email to hear when this project ships new features and announcements."; - const subscribeFrame = document.createElement("iframe"); - subscribeFrame.className = "subscribe-widget-frame"; - subscribeFrame.loading = "lazy"; - subscribeFrame.title = `${project.name} subscribe form`; - subscribeFrame.setAttribute("aria-label", `Subscribe for ${project.name} updates`); - subscribeFrame.setAttribute("tabindex", "-1"); - const idealFrameHeight = Math.max(240, Math.min(subscribeConfig.height || 320, 420)); - subscribeFrame.dataset.frameHeight = String(idealFrameHeight); - subscribeWidget.append(subscribeHeading, subscribeBlurb, subscribeFrame); + // Container for LoopAware subscribe form (rendered by subscribe.js) + const subscribeFormContainer = document.createElement("div"); + subscribeFormContainer.className = "subscribe-form-container"; + + subscribeWidget.append(subscribeHeading, subscribeBlurb, subscribeFormContainer); subscribeOverlay = document.createElement("div"); subscribeOverlay.className = "project-card-subscribe-overlay"; subscribeOverlay.dataset.subscribeLoaded = "false"; subscribeOverlay.append(subscribeWidget); - let subscribeFrameLoaded = false; + let subscribeScriptLoaded = false; loadSubscribeWidget = async () => { - if (subscribeFrameLoaded) return; - subscribeFrameLoaded = true; - const overlayRect = subscribeOverlay.getBoundingClientRect(); - const maxFrameHeight = Math.max(160, overlayRect.height - 32); - const ideal = Number(subscribeFrame.dataset.frameHeight) || 320; - const frameHeight = Math.min(ideal, maxFrameHeight); - subscribeFrame.style.minHeight = `${frameHeight}px`; - subscribeFrame.style.height = `${frameHeight}px`; - subscribeFrame.addEventListener("load", () => { - subscribeOverlay.dataset.subscribeLoaded = "true"; - }, {once: true}); - const scriptContent = await fetchSubscribeScript(subscribeConfig.script); - if (scriptContent) { - subscribeFrame.srcdoc = buildSubscribeFrameDocument(scriptContent, subscribeConfig.script); - } + if (subscribeScriptLoaded) return; + subscribeScriptLoaded = true; + await loadSubscribeScript(subscribeConfig.script, subscribeFormContainer); + subscribeOverlay.dataset.subscribeLoaded = "true"; }; } back.append(backHeader, backBody); @@ -381,11 +309,6 @@ function buildProjectCard(project) { if (nowFlipped && loadSubscribeWidget) { loadSubscribeWidget(); } - - const iframe = card.querySelector(".subscribe-widget-frame"); - if (iframe) { - iframe.setAttribute("tabindex", nowFlipped ? "0" : "-1"); - } }; card.addEventListener("click", toggleFlip); diff --git a/styles.css b/styles.css index df5b9c6..41321c1 100644 --- a/styles.css +++ b/styles.css @@ -490,17 +490,48 @@ a:focus { font-size: 0.95rem; } -.subscribe-widget-frame { +.subscribe-form-container { width: 100%; - border: 0; - border-radius: 16px; - background: transparent; - min-height: 240px; - height: 240px; - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); - overflow: hidden; - transform: translateZ(0); - backface-visibility: hidden; +} + +/* LoopAware subscribe form styling */ +#mp-subscribe-form { + background: transparent !important; + border: none !important; + box-shadow: none !important; + padding: 0 !important; + max-width: 100% !important; + font-family: "Space Grotesk", "Roboto", sans-serif !important; + color: var(--text-gold) !important; +} + +#mp-subscribe-form > div:first-child { + display: none !important; +} + +#mp-subscribe-form input { + background: rgba(255, 255, 255, 0.08) !important; + border: 1px solid rgba(255, 255, 255, 0.2) !important; + color: #fff !important; + font-family: inherit !important; +} + +#mp-subscribe-form input::placeholder { + color: rgba(255, 255, 255, 0.4) !important; +} + +#mp-subscribe-form button { + background: var(--accent-gold) !important; + color: var(--bg-body) !important; + font-family: inherit !important; +} + +#mp-subscribe-form button:hover { + filter: brightness(1.1); +} + +#mp-subscribe-form #mp-subscribe-status { + color: rgba(255, 255, 255, 0.7) !important; } .project-card-subscribe-overlay { diff --git a/tests/hero.spec.js b/tests/hero.spec.js index beffb1e..75f4228 100644 --- a/tests/hero.spec.js +++ b/tests/hero.spec.js @@ -199,36 +199,23 @@ test.describe("Marco Polo Research Lab landing page", () => { const badge = card.locator(".status-badge").first(); const overlay = card.locator(".project-card-subscribe-overlay"); - const iframeElement = card.locator(".subscribe-widget-frame"); + const formContainer = card.locator(".subscribe-form-container"); await expect(overlay).toHaveAttribute("data-subscribe-loaded", "false"); - await expect( - iframeElement, - `${project.name} iframe should be unfocusable before flip`, - ).toHaveAttribute("tabindex", "-1"); await badge.click(); - const cardBack = card.locator(".project-card-face.project-card-back"); - const frame = cardBack.frameLocator(".subscribe-widget-frame"); - const loopAwareForm = frame.locator("#mp-subscribe-form"); - await expect(loopAwareForm, `${project.name} LoopAware form should render inside iframe`).toBeVisible(); - await expect(frame.locator("input[type='email']")).toBeVisible(); + // Form is rendered directly in the page (no iframe) via LoopAware subscribe.js + const loopAwareForm = formContainer.locator("#mp-subscribe-form"); + await expect(loopAwareForm, `${project.name} LoopAware form should render`).toBeVisible(); + await expect(formContainer.locator("input[type='email']")).toBeVisible(); await expect( - frame.locator("button"), + formContainer.locator("button"), `${project.name} LoopAware widget should expose a CTA button`, ).toContainText(/subscribe|notify/i); await expect(overlay).toHaveAttribute("data-subscribe-loaded", "true"); - await expect( - iframeElement, - `${project.name} iframe should be focusable after flip`, - ).toHaveAttribute("tabindex", "0"); await badge.click(); - await expect( - iframeElement, - `${project.name} iframe should be unfocusable after unflip`, - ).toHaveAttribute("tabindex", "-1"); } }); }); From aa3b46583ab8eed710ee021094d926e66b5d80bc Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Sun, 11 Jan 2026 10:10:28 -0800 Subject: [PATCH 11/18] refactor: use LoopAware target parameter (LA-113) for subscribe forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that LoopAware supports the target parameter, simplify the code to pass target ID directly instead of moving the form after render. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- script.js | 65 ++++++++++++++----------------------------------------- 1 file changed, 16 insertions(+), 49 deletions(-) diff --git a/script.js b/script.js index 80fe725..b429ece 100644 --- a/script.js +++ b/script.js @@ -60,54 +60,19 @@ const STATUS_CLASS = Object.freeze({ /** @type {ProjectStatus[]} */ const FLIPPABLE_STATUSES = ["Beta", "WIP"]; -/** @type {Set} */ -const loadedSubscribeScripts = new Set(); - /** - * Loads the LoopAware subscribe script and moves the form to the target container. - * Workaround until LoopAware implements LA-113 (target parameter support). - * @param {string} scriptUrl - Full URL with query parameters - * @param {HTMLElement} targetContainer - Container to move the form into - * @returns {Promise} + * Loads the LoopAware subscribe script with target parameter (LA-113). + * @param {string} scriptUrl - Base URL with query parameters + * @param {string} targetId - ID of the element to render the form into */ -function loadSubscribeScript(scriptUrl, targetContainer) { - return new Promise((resolve) => { - // Script already loaded - form exists, move it - if (loadedSubscribeScripts.has(scriptUrl)) { - const form = document.getElementById("mp-subscribe-form"); - if (form && !targetContainer.contains(form)) { - targetContainer.appendChild(form); - } - resolve(); - return; - } - - const script = document.createElement("script"); - script.src = scriptUrl; - script.async = true; - - script.addEventListener("load", () => { - loadedSubscribeScripts.add(scriptUrl); - // Wait for form to be created, then move it to target - const moveForm = () => { - const form = document.getElementById("mp-subscribe-form"); - if (form) { - targetContainer.appendChild(form); - resolve(); - } else { - requestAnimationFrame(moveForm); - } - }; - moveForm(); - }, {once: true}); - - script.addEventListener("error", () => { - console.error("Failed to load subscribe script:", scriptUrl); - resolve(); - }, {once: true}); - - document.head.appendChild(script); - }); +function loadSubscribeScript(scriptUrl, targetId) { + const url = new URL(scriptUrl); + url.searchParams.set("target", targetId); + + const script = document.createElement("script"); + script.src = url.toString(); + script.async = true; + document.head.appendChild(script); } /** @@ -270,8 +235,10 @@ function buildProjectCard(project) { subscribeConfig.copy || "Leave your email to hear when this project ships new features and announcements."; - // Container for LoopAware subscribe form (rendered by subscribe.js) + // Container for LoopAware subscribe form (rendered by subscribe.js with target param) const subscribeFormContainer = document.createElement("div"); + const targetId = `subscribe-target-${project.id}`; + subscribeFormContainer.id = targetId; subscribeFormContainer.className = "subscribe-form-container"; subscribeWidget.append(subscribeHeading, subscribeBlurb, subscribeFormContainer); @@ -282,10 +249,10 @@ function buildProjectCard(project) { let subscribeScriptLoaded = false; - loadSubscribeWidget = async () => { + loadSubscribeWidget = () => { if (subscribeScriptLoaded) return; subscribeScriptLoaded = true; - await loadSubscribeScript(subscribeConfig.script, subscribeFormContainer); + loadSubscribeScript(subscribeConfig.script, targetId); subscribeOverlay.dataset.subscribeLoaded = "true"; }; } From 5210a418607eb31c579e1aa5a44e263669b73c77 Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Sun, 11 Jan 2026 10:14:17 -0800 Subject: [PATCH 12/18] refactor: use declarative target field in subscribe config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move target ID from programmatic generation to declarative config in projects.json. Each subscribe-enabled project now specifies its own target element ID. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- data/projects.json | 2 ++ script.js | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/data/projects.json b/data/projects.json index 1e30506..04f01b1 100644 --- a/data/projects.json +++ b/data/projects.json @@ -103,6 +103,7 @@ "subscribe": { "enabled": true, "script": "https://loopaware.mprlab.com/subscribe.js?site_id=a3222433-92ec-473a-9255-0797226c2273&mode=inline&accent=%23ffd369&cta=Subscribe&success=Thanks%20for%20subscribing&name_field=false", + "target": "subscribe-target-loopaware", "title": "Get LoopAware release updates", "copy": "Drop your email to hear when LoopAware ships fresh drops, integrations, and subscriber tooling." } @@ -234,6 +235,7 @@ "subscribe": { "enabled": true, "script": "https://loopaware.mprlab.com/subscribe.js?site_id=d8c3d1c8-7968-43d0-8026-ee827ada7666&mode=inline&accent=%23ffd369&cta=Subscribe&success=Thanks%20for%20subscribing&name_field=false", + "target": "subscribe-target-gravity-notes", "title": "Get Gravity Notes release updates", "copy": "Drop your email to hear when Gravity Notes ships fresh features, AI integrations, and new plugins.", "height": 320 diff --git a/script.js b/script.js index b429ece..843e3b0 100644 --- a/script.js +++ b/script.js @@ -237,8 +237,7 @@ function buildProjectCard(project) { // Container for LoopAware subscribe form (rendered by subscribe.js with target param) const subscribeFormContainer = document.createElement("div"); - const targetId = `subscribe-target-${project.id}`; - subscribeFormContainer.id = targetId; + subscribeFormContainer.id = subscribeConfig.target; subscribeFormContainer.className = "subscribe-form-container"; subscribeWidget.append(subscribeHeading, subscribeBlurb, subscribeFormContainer); @@ -252,7 +251,7 @@ function buildProjectCard(project) { loadSubscribeWidget = () => { if (subscribeScriptLoaded) return; subscribeScriptLoaded = true; - loadSubscribeScript(subscribeConfig.script, targetId); + loadSubscribeScript(subscribeConfig.script, subscribeConfig.target); subscribeOverlay.dataset.subscribeLoaded = "true"; }; } From 3f3c93d95cba5803be5c09969bbfc56e53841b04 Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Sun, 11 Jan 2026 10:28:29 -0800 Subject: [PATCH 13/18] fix: prevent card flip when interacting with subscribe form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exclude form elements (input, button, textarea, select, label) and the subscribe form itself from triggering card flip. This allows users to interact with the subscribe form without the card flipping. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- script.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/script.js b/script.js index 843e3b0..c555dc4 100644 --- a/script.js +++ b/script.js @@ -266,7 +266,8 @@ function buildProjectCard(project) { */ const toggleFlip = event => { const target = /** @type {HTMLElement} */ (event.target); - if (target.closest("a")) { + // Don't flip when interacting with links or form elements + if (target.closest("a, input, button, textarea, select, label, #mp-subscribe-form")) { return; } @@ -281,7 +282,8 @@ function buildProjectCard(project) { card.addEventListener("keydown", event => { if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") { const target = /** @type {HTMLElement} */ (event.target); - if (target.closest("a")) { + // Don't flip when interacting with links or form elements + if (target.closest("a, input, button, textarea, select, label, #mp-subscribe-form")) { return; } From 7129a8f48b8a273924bc641a2803ac831243d18f Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Sun, 11 Jan 2026 10:36:34 -0800 Subject: [PATCH 14/18] feat: flip card back after successful subscription MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Listen for loopaware:subscribe:success CustomEvent and flip the card back after a 2-second delay to show the success message. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- script.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/script.js b/script.js index c555dc4..d84bd90 100644 --- a/script.js +++ b/script.js @@ -254,6 +254,16 @@ function buildProjectCard(project) { loadSubscribeScript(subscribeConfig.script, subscribeConfig.target); subscribeOverlay.dataset.subscribeLoaded = "true"; }; + + // Listen for successful subscription and flip card back after delay + subscribeFormContainer.addEventListener("loopaware:subscribe:success", () => { + setTimeout(() => { + if (card.classList.contains("is-flipped")) { + card.classList.remove("is-flipped"); + card.setAttribute("aria-pressed", "false"); + } + }, 2000); // 2 second delay to show success message + }); } back.append(backHeader, backBody); if (subscribeOverlay) { From c0eab84237abc301c20838c30178f399afcc8800 Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Sun, 11 Jan 2026 11:04:10 -0800 Subject: [PATCH 15/18] refactor: simplify subscribe success UX and remove defensive code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hide form inputs after successful subscription, show only status message - Reset form state when card flips back (clear email, restore inputs) - Remove redundant null checks and duplicate variables - Document subscribe form configuration in README πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- README.md | 26 ++++++++++++++++++++++++ data/projects.json | 9 ++++++++- script.js | 49 ++++++++++++++++++++++++++++------------------ 3 files changed, 64 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 2d1706c..3bfa081 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,32 @@ Each entry in the project gallery is powered by `data/projects.json`: Vectorizing images or text to SVG can be done using the tools available in the [svg_tools](https://github.com/tyemirov/svg_tools) repository. +## Adding Subscribe Forms + +Projects can display a LoopAware-powered subscribe form on their card back. To enable: + +1. **Create a site in LoopAware** – register your project at [loopaware.mprlab.com](https://loopaware.mprlab.com) and note the `site_id`. + +2. **Add the subscribe config** to the project entry in `data/projects.json`: + + ```json + "subscribe": { + "enabled": true, + "script": "https://loopaware.mprlab.com/subscribe.js?site_id=YOUR_SITE_ID&mode=inline&accent=%23ffd369&cta=Subscribe&success=Thanks%20for%20subscribing&name_field=false", + "target": "subscribe-target-YOUR_PROJECT_ID", + "title": "Get PROJECT_NAME updates", + "copy": "Drop your email to hear when PROJECT_NAME ships new features." + } + ``` + +3. **Key fields**: + - `script` – LoopAware widget URL with your `site_id` and customization params + - `target` – unique DOM ID where the form renders (use pattern `subscribe-target-{project-id}`) + - `title` – heading shown above the form + - `copy` – description text below the heading + +The card will automatically become flippable and load the subscribe widget when the user flips it. + ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/data/projects.json b/data/projects.json index 04f01b1..7efa583 100644 --- a/data/projects.json +++ b/data/projects.json @@ -196,7 +196,14 @@ "enabled": true, "url": "https://github.com/MarcoPoloResearchLab/ProductScanner#readme" }, - "subscribe": { "enabled": false } + "subscribe": { + "enabled": true, + "script": "https://loopaware.mprlab.com/subscribe.js?site_id=cc91ba37-b47c-46bf-bd4a-d92611ca4b1a&mode=inline&accent=%23ffd369&cta=Subscribe&success=Thanks%20for%20subscribing&name_field=false", + "target": "subscribe-target-product-scanner", + "title": "Get Gravity Notes release updates", + "copy": "Drop your email to hear when Gravity Notes ships fresh features, AI integrations, and new plugins.", + "height": 320 + } }, { "id": "sheet2tube", diff --git a/script.js b/script.js index d84bd90..05061b2 100644 --- a/script.js +++ b/script.js @@ -107,8 +107,7 @@ function buildProjectCard(project) { inner.className = "project-card-inner"; const subscribeConfig = project.subscribe; - const subscribeEnabled = subscribeConfig.enabled && Boolean(subscribeConfig.script); - const hasSubscribeWidget = subscribeEnabled; + const hasSubscribeWidget = subscribeConfig.enabled && Boolean(subscribeConfig.script); const isFlippable = hasSubscribeWidget || FLIPPABLE_STATUSES.includes(project.status); if (isFlippable) { card.classList.add("project-card-flippable"); @@ -158,9 +157,9 @@ function buildProjectCard(project) { const actionsRow = document.createElement("div"); actionsRow.className = "card-actions"; - if (shouldShowLaunch && project.launch.url) { + if (shouldShowLaunch) { const link = document.createElement("a"); - link.href = project.launch.url; + link.href = /** @type {string} */ (project.launch.url); link.className = "card-action"; link.target = "_blank"; link.rel = "noreferrer noopener"; @@ -168,9 +167,9 @@ function buildProjectCard(project) { actionsRow.append(link); } - if (shouldShowDocs && project.docs.url) { + if (shouldShowDocs) { const docsLink = document.createElement("a"); - docsLink.href = project.docs.url; + docsLink.href = /** @type {string} */ (project.docs.url); docsLink.className = "card-action"; docsLink.target = "_blank"; docsLink.rel = "noreferrer noopener"; @@ -217,7 +216,7 @@ function buildProjectCard(project) { backBody.append(backCopy); let subscribeOverlay = null; - if (hasSubscribeWidget && subscribeConfig) { + if (hasSubscribeWidget) { card.classList.add("project-card-has-subscribe"); const subscribeWidget = document.createElement("div"); @@ -257,11 +256,28 @@ function buildProjectCard(project) { // Listen for successful subscription and flip card back after delay subscribeFormContainer.addEventListener("loopaware:subscribe:success", () => { + const emailInput = /** @type {HTMLInputElement} */ ( + subscribeFormContainer.querySelector("#mp-subscribe-email") + ); + const submitButton = /** @type {HTMLElement} */ ( + subscribeFormContainer.querySelector("#mp-subscribe-submit") + ); + const statusElement = /** @type {HTMLElement} */ ( + subscribeFormContainer.querySelector("#mp-subscribe-status") + ); + + // Hide form inputs, keep status message visible + emailInput.style.display = "none"; + submitButton.style.display = "none"; + setTimeout(() => { - if (card.classList.contains("is-flipped")) { - card.classList.remove("is-flipped"); - card.setAttribute("aria-pressed", "false"); - } + card.classList.remove("is-flipped"); + card.setAttribute("aria-pressed", "false"); + // Reset form state for next flip + emailInput.style.display = ""; + emailInput.value = ""; + submitButton.style.display = ""; + statusElement.textContent = ""; }, 2000); // 2 second delay to show success message }); } @@ -314,9 +330,7 @@ function buildProjectCard(project) { */ function buildStatusBadge(status) { const badge = document.createElement("span"); - badge.className = "status-badge"; - const modifier = STATUS_CLASS[status]; - if (modifier) badge.classList.add(modifier); + badge.className = `status-badge ${STATUS_CLASS[status]}`; badge.textContent = status; return badge; } @@ -433,7 +447,7 @@ function layoutBandRows(grid) { } function setupHeroAudioToggle() { - const video = document.getElementById("hero-video"); + const video = /** @type {HTMLVideoElement | null} */ (document.getElementById("hero-video")); const toggle = document.getElementById("hero-sound-toggle"); if (!video || !toggle) return; @@ -448,10 +462,7 @@ function setupHeroAudioToggle() { }; const ensurePlayback = () => { - const playPromise = video.play(); - if (playPromise && typeof playPromise.then === "function") { - playPromise.catch(() => {}); - } + video.play().catch(() => {}); }; toggle.addEventListener("click", () => { From e255229ec90172dd6bf2e07e914503a023ec0647 Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Fri, 16 Jan 2026 10:53:36 -0800 Subject: [PATCH 16/18] Future development --- ISSUES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ISSUES.md b/ISSUES.md index 75fff61..f1024a0 100644 --- a/ISSUES.md +++ b/ISSUES.md @@ -90,6 +90,7 @@ Each issue is formatted as `- [ ] [-]`. When resolved it becomes -` - Rationale for current code: Likely added to prevent focus from jumping into the iframe during card flip animation or while card is not flipped. - Fix: Conditionally set `tabindex="0"` on the iframe when the card enters the flipped state (`is-flipped` class added) and restore `tabindex="-1"` when unflipped. Update the `toggleFlip` handler in `script.js:207-225` to toggle the iframe's tabindex alongside the flip state. - Extend Playwright test `subscribe-enabled cards render LoopAware forms after flipping` to assert that the iframe is focusable (`tabindex="0"`) when the card is flipped. +- [ ] [MP-205] Add the four-way color theme switch and style all of the lements accordingly. Use theme-config in the footer to style all elements and choose theme-switcher="square". Read up @tools/mpr-ui/README.md and @tools/mpr-ui/docs/integration-guide.md ## BugFixes (300–399) From 1d85f1cf0bd815bd22cc5ce1eb61645807c3ac53 Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Fri, 16 Jan 2026 10:53:58 -0800 Subject: [PATCH 17/18] Development watch for the changed filed to reload docker --- docker-compose.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 174a514..e94d77b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,4 +21,15 @@ services: volumes: - ./:${GHTTP_SERVE_DIRECTORY}:ro - ${GHTTP_TLS_CERT_HOST_PATH}:${GHTTP_TLS_CERT_CONTAINER_PATH}:ro - - ${GHTTP_TLS_KEY_HOST_PATH}:${GHTTP_TLS_KEY_CONTAINER_PATH}:ro \ No newline at end of file + - ${GHTTP_TLS_KEY_HOST_PATH}:${GHTTP_TLS_KEY_CONTAINER_PATH}:ro + develop: + watch: + - action: sync + path: ./index.html + target: ${GHTTP_SERVE_DIRECTORY}/index.html + - action: sync + path: ./script.js + target: ${GHTTP_SERVE_DIRECTORY}/script.js + - action: sync + path: ./styles.css + target: ${GHTTP_SERVE_DIRECTORY}/styles.css From 793e31678274deb109c3e04018afdc074c9813d5 Mon Sep 17 00:00:00 2001 From: Vadym Tyemirov Date: Fri, 16 Jan 2026 10:54:36 -0800 Subject: [PATCH 18/18] Subscription sections --- data/projects.json | 47 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/data/projects.json b/data/projects.json index 7efa583..f6b9769 100644 --- a/data/projects.json +++ b/data/projects.json @@ -16,12 +16,19 @@ "enabled": false, "url": "https://github.com/MarcoPoloResearchLab/marcopolo.github.io/blob/main/ISSUES.md" }, - "subscribe": { "enabled": false } + "subscribe": { + "enabled": true, + "script": "https://loopaware.mprlab.com/subscribe.js?site_id=e771ca8b-51d9-4ea4-884a-eb769ca66550&mode=inline&accent=%23ffd369&cta=Subscribe&success=Thanks%20for%20subscribing&name_field=false", + "target": "subscribe-target-issues-md", + "title": "Get ISSUES.md release updates", + "copy": "Drop your email to hear when ISSUES.md ships fresh features and AI integrations.", + "height": 320 + } }, { "id": "photolab", "name": "Photolab", - "description": "Local photo library classifier and search UI that writes high-confidence labels into EXIF, indexes metadata into SQLite, and serves a minimal browser-based search grid.", + "description": "Local photo library classifier and search UI that writes high-confidence labels into EXIF, indexes metadata, and serves a minimal browser-based search grid.", "status": "WIP", "category": "Research", "repo": null, @@ -124,7 +131,14 @@ "enabled": true, "url": "https://github.com/temirov/pinguin#readme" }, - "subscribe": { "enabled": false } + "subscribe": { + "enabled": true, + "script": "https://loopaware.mprlab.com/subscribe.js?site_id=e8f235c8-ddf2-46b7-ae42-022885fe8a44&mode=inline&accent=%23ffd369&cta=Subscribe&success=Thanks%20for%20subscribing&name_field=false", + "target": "subscribe-target-pinguin", + "title": "Get Pinguin release updates", + "copy": "Drop your email to hear when Pinguin ships fresh features.", + "height": 320 + } }, { "id": "ets", @@ -160,7 +174,14 @@ "enabled": true, "url": "https://github.com/tyemirov/TAuth#readme" }, - "subscribe": { "enabled": false } + "subscribe": { + "enabled": true, + "script": "https://loopaware.mprlab.com/subscribe.js?site_id=0cdc4451-b928-4274-8db2-c8b9a23796bd&mode=inline&accent=%23ffd369&cta=Subscribe&success=Thanks%20for%20subscribing&name_field=false", + "target": "subscribe-target-tauth", + "title": "Get TAuth release updates", + "copy": "Drop your email to hear when TAuth ships fresh features, OAuth integrations, and supports new platforms.", + "height": 320 + } }, { "id": "ledger", @@ -178,7 +199,14 @@ "enabled": true, "url": "https://github.com/tyemirov/ledger#readme" }, - "subscribe": { "enabled": false } + "subscribe": { + "enabled": true, + "script": "https://loopaware.mprlab.com/subscribe.js?site_id=f5c0dfd1-2943-43d0-8f46-a6064d04b05e&mode=inline&accent=%23ffd369&cta=Subscribe&success=Thanks%20for%20subscribing&name_field=false", + "target": "subscribe-target-ledger", + "title": "Get Ledger's release updates", + "copy": "Drop your email to hear when Ledger ships fresh features and integrations.", + "height": 320 + } }, { "id": "product-scanner", @@ -282,7 +310,14 @@ "enabled": true, "url": "https://github.com/MarcoPoloResearchLab/prompts#readme" }, - "subscribe": { "enabled": false } + "subscribe": { + "enabled": true, + "script": "https://loopaware.mprlab.com/subscribe.js?site_id=efad9dff-1777-4710-b6b1-3218ee8c5cbb&mode=inline&accent=%23ffd369&cta=Subscribe&success=Thanks%20for%20subscribing&name_field=false", + "target": "subscribe-target-prompt-bubbles", + "title": "Get Prompt Bubbles release updates", + "copy": "Drop your email to hear when Prompt Bubbles ships fresh features, platform integrations, and design changes.", + "height": 320 + } } ] }