@prosopo/client-bundle-example 2.10.24 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +5 -4
  2. package/.turbo/turbo-build$colon$tsc.log +11 -11
  3. package/.turbo/turbo-build.log +6 -5
  4. package/CHANGELOG.md +16 -0
  5. package/dist/cjs/index.cjs +13 -41
  6. package/dist/cjs/plugins/pages.cjs +113 -0
  7. package/dist/index.js +2 -42
  8. package/dist/plugins/pages.js +103 -0
  9. package/package.json +2 -2
  10. package/src/frictionless-explicit-web3.html +10 -23
  11. package/src/frictionless-explicit.html +10 -22
  12. package/src/frictionless-implicit.html +60 -86
  13. package/src/frictionless-manual-start.html +13 -33
  14. package/src/image-explicit-web3.html +8 -24
  15. package/src/image-explicit.html +8 -23
  16. package/src/image-implicit.html +192 -0
  17. package/src/index.html +11 -26
  18. package/src/index.ts +1 -62
  19. package/src/invisible-frictionless-explicit.html +8 -17
  20. package/src/invisible-frictionless-implicit.html +8 -25
  21. package/src/invisible-image-explicit.html +8 -23
  22. package/src/invisible-image-implicit.html +8 -24
  23. package/src/invisible-pow-explicit.html +8 -23
  24. package/src/invisible-pow-implicit.html +9 -21
  25. package/src/invisible-puzzle-explicit.html +8 -23
  26. package/src/invisible-puzzle-implicit.html +9 -21
  27. package/src/plugins/code-snippet-injector.ts +179 -0
  28. package/src/plugins/form-filler-injector.ts +36 -121
  29. package/src/plugins/layout-injector.ts +238 -0
  30. package/src/plugins/logo.ts +15 -0
  31. package/src/plugins/pages.ts +219 -0
  32. package/src/plugins/placement-injector.ts +25 -64
  33. package/src/plugins/slots.ts +32 -0
  34. package/src/plugins/status-log-injector.ts +35 -114
  35. package/src/pow-explicit-web3.html +8 -24
  36. package/src/pow-explicit.html +8 -23
  37. package/src/pow-implicit-sessionid.html +10 -24
  38. package/src/pow-implicit.html +9 -24
  39. package/src/puzzle-bind-explicit.html +9 -24
  40. package/src/puzzle-explicit.html +8 -23
  41. package/src/puzzle-implicit.html +9 -24
  42. package/src/styles/demo.css +803 -0
  43. package/src/tests/plugins.unit.test.ts +285 -118
  44. package/tsconfig.tsbuildinfo +1 -1
  45. package/vite.config.ts +5 -5
  46. package/src/plugins/explanation-injector.ts +0 -294
  47. package/src/plugins/navigation-injector.ts +0 -606
  48. package/src/styles/captcha.css +0 -78
  49. package/src/styles/field.css +0 -12
  50. package/src/tests/floatLabel.unit.test.ts +0 -195
  51. package/src/tests/floatLabelReady.unit.test.ts +0 -36
@@ -0,0 +1,238 @@
1
+ // Copyright 2021-2026 Prosopo (UK) Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ // Wraps every demo page in the playground shell: the prosopo.io header, a
16
+ // sidebar that links between the demo pages, and a panel for the event log,
17
+ // the code for the current setup and the sign-up call to action.
18
+ import type { IndexHtmlTransformContext, Plugin } from "vite";
19
+ import { prosopoWordmark } from "./logo.js";
20
+ import {
21
+ type DemoPage,
22
+ type DemoSetup,
23
+ captchaTypeOptions,
24
+ defaultSetup,
25
+ findPageByPath,
26
+ labelOf,
27
+ modeOptions,
28
+ pagePathFromFilename,
29
+ relativePrefix,
30
+ renderingOptions,
31
+ renderingsFor,
32
+ resolvePage,
33
+ } from "./pages.js";
34
+ import { slotMarkup } from "./slots.js";
35
+
36
+ export const SIGN_UP_URL = "https://prosopo.io/pricing/";
37
+ const LOGIN_URL = "https://portal.prosopo.io/";
38
+ const DOCS_URL = "https://docs.prosopo.io/";
39
+
40
+ const ariaCurrent = (isCurrent: boolean): string =>
41
+ isCurrent ? ' aria-current="page"' : "";
42
+
43
+ const headTags = (prefix: string): string => `
44
+ <meta charset="utf-8">
45
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
46
+ <link rel="preconnect" href="https://fonts.googleapis.com">
47
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
48
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700;800&display=swap">
49
+ <link rel="stylesheet" href="${prefix}styles/demo.css">`;
50
+
51
+ const header = `
52
+ <header class="demo-header">
53
+ <a class="demo-header__logo" href="https://prosopo.io/">${prosopoWordmark}</a>
54
+ <nav class="demo-header__nav" aria-label="prosopo.io">
55
+ <a href="https://prosopo.io/products/">Platform</a>
56
+ <a href="${SIGN_UP_URL}">Pricing</a>
57
+ <a href="${DOCS_URL}">Docs</a>
58
+ <a href="https://prosopo.io/blog/">Blog</a>
59
+ </nav>
60
+ <div class="demo-header__actions">
61
+ <a class="demo-link-button demo-link-button--muted" href="${LOGIN_URL}">Login</a>
62
+ <a class="demo-link-button demo-link-button--primary" href="${SIGN_UP_URL}">Get started →</a>
63
+ </div>
64
+ </header>`;
65
+
66
+ const sidebar = (page: DemoPage | undefined, prefix: string): string => {
67
+ const setup: DemoSetup = page ?? defaultSetup;
68
+ const href = (target: DemoSetup): string =>
69
+ `${prefix}${resolvePage(target).path}`;
70
+
71
+ const choice = (
72
+ target: DemoSetup,
73
+ isCurrent: boolean,
74
+ name: string,
75
+ description: string,
76
+ ): string => `
77
+ <a class="demo-choice" data-demo-nav href="${href(target)}"${ariaCurrent(isCurrent)}>
78
+ <span class="demo-choice__radio"></span>
79
+ <span class="demo-choice__body">
80
+ <span class="demo-choice__name">${name}</span>
81
+ <span class="demo-choice__description">${description}</span>
82
+ </span>
83
+ </a>`;
84
+
85
+ const types = captchaTypeOptions
86
+ .map(({ value, label, description }) =>
87
+ choice(
88
+ { ...setup, captchaType: value },
89
+ page?.captchaType === value,
90
+ value === defaultSetup.captchaType
91
+ ? `${label}<span class="demo-badge">Default</span>`
92
+ : label,
93
+ description,
94
+ ),
95
+ )
96
+ .join("");
97
+
98
+ const modes = modeOptions
99
+ .map(
100
+ ({ value, label }) =>
101
+ `<a data-demo-nav href="${href({ ...setup, mode: value })}"${ariaCurrent(page?.mode === value)}>${label}</a>`,
102
+ )
103
+ .join("");
104
+
105
+ const renderings = renderingsFor(setup.captchaType, setup.mode)
106
+ .map(({ value, label, description }) =>
107
+ choice(
108
+ { ...setup, rendering: value },
109
+ page?.rendering === value,
110
+ label,
111
+ description,
112
+ ),
113
+ )
114
+ .join("");
115
+
116
+ // Rendering is an integration detail, so it stays folded away unless the
117
+ // visitor is already on a non-default rendering.
118
+ const advancedOpen =
119
+ page && page.rendering !== defaultSetup.rendering ? " open" : "";
120
+
121
+ return `
122
+ <aside class="demo-sidebar">
123
+ <div class="demo-intro-block">
124
+ <div class="demo-eyebrow">Live demo</div>
125
+ <h1 class="demo-title">Procaptcha playground</h1>
126
+ <p class="demo-intro">Pick a setup, fill in the form, see what happens.</p>
127
+ </div>
128
+ <nav class="demo-group" aria-label="Captcha type">
129
+ <div class="demo-group__label">Captcha type</div>
130
+ <div class="demo-types">${types}
131
+ </div>
132
+ </nav>
133
+ <div class="demo-group">
134
+ <div class="demo-group__label">Mode</div>
135
+ <nav class="demo-segmented" aria-label="Mode">${modes}</nav>
136
+ </div>
137
+ ${slotMarkup("placement")}
138
+ <details class="demo-advanced"${advancedOpen}>
139
+ <summary>Advanced</summary>
140
+ <div class="demo-group">
141
+ <div class="demo-group__label">Rendering</div>
142
+ <p class="demo-group__hint">How the widget gets onto your page. Most sites use implicit.</p>
143
+ <nav class="demo-renderings" aria-label="Rendering">${renderings}
144
+ </nav>
145
+ </div>
146
+ </details>
147
+ </aside>`;
148
+ };
149
+
150
+ const chips = (page: DemoPage | undefined): string =>
151
+ page
152
+ ? [
153
+ labelOf(captchaTypeOptions, page.captchaType),
154
+ labelOf(modeOptions, page.mode),
155
+ ...(page.rendering === defaultSetup.rendering
156
+ ? []
157
+ : [labelOf(renderingOptions, page.rendering)]),
158
+ ]
159
+ .map((label) => `<span class="demo-chip">${label}</span>`)
160
+ .join("")
161
+ : "";
162
+
163
+ const layoutOpen = (page: DemoPage | undefined, prefix: string): string => `
164
+ <div class="demo-layout">
165
+ ${sidebar(page, prefix)}
166
+ <main class="demo-main">
167
+ <div class="demo-toolbar">${chips(page)}${slotMarkup("toolbar")}</div>
168
+ `;
169
+
170
+ // Pages outside the registry (web3, session id) have no snippet to show.
171
+ const codeSection = (page: DemoPage | undefined): string =>
172
+ page
173
+ ? `
174
+ <section class="demo-panel__section">
175
+ <h2 class="demo-panel__title">Code for this setup</h2>
176
+ ${slotMarkup("code")}
177
+ </section>`
178
+ : "";
179
+
180
+ const layoutClose = (page: DemoPage | undefined): string => `
181
+ <p class="demo-cta-line">Want this on your site? <a href="${SIGN_UP_URL}">Start free, no credit card →</a></p>
182
+ </main>
183
+ <aside class="demo-panel" aria-label="What happened">
184
+ <section class="demo-panel__section">
185
+ <h2 class="demo-panel__title">Events</h2>
186
+ ${slotMarkup("events")}
187
+ </section>${codeSection(page)}
188
+ <div class="demo-panel__cta">
189
+ <h2>Get your own site key</h2>
190
+ <p>Free plan. No credit card required.</p>
191
+ <a class="demo-link-button demo-link-button--pink" href="${SIGN_UP_URL}">Get started →</a>
192
+ </div>
193
+ </aside>
194
+ </div>`;
195
+
196
+ // Placement is a query parameter, so carry it across when moving between pages.
197
+ const keepPlacementScript = `
198
+ <script>
199
+ (function () {
200
+ var placement = new URLSearchParams(window.location.search).get("placement");
201
+ if (!placement) return;
202
+ var links = document.querySelectorAll("a[data-demo-nav]");
203
+ for (var i = 0; i < links.length; i++) {
204
+ var url = new URL(links[i].getAttribute("href"), window.location.href);
205
+ url.searchParams.set("placement", placement);
206
+ links[i].setAttribute("href", url.toString());
207
+ }
208
+ })();
209
+ </script>`;
210
+
211
+ export default function layoutInjector(): Plugin {
212
+ return {
213
+ name: "layout-injector",
214
+ transformIndexHtml: {
215
+ order: "pre",
216
+ handler(html: string, ctx: IndexHtmlTransformContext): string {
217
+ if (!html.includes("<body>") || !html.includes("</body>")) {
218
+ return html;
219
+ }
220
+
221
+ const pagePath = pagePathFromFilename(ctx.filename);
222
+ const page = findPageByPath(pagePath);
223
+ const prefix = relativePrefix(pagePath);
224
+
225
+ return html
226
+ .replace("<head>", () => `<head>${headTags(prefix)}`)
227
+ .replace(
228
+ "<body>",
229
+ () => `<body class="demo">${header}${layoutOpen(page, prefix)}`,
230
+ )
231
+ .replace(
232
+ "</body>",
233
+ () => `${layoutClose(page)}${keepPlacementScript}\n</body>`,
234
+ );
235
+ },
236
+ },
237
+ };
238
+ }
@@ -0,0 +1,15 @@
1
+ // Copyright 2021-2026 Prosopo (UK) Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ export const prosopoWordmark = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2062.63 468.67" role="img" aria-label="Prosopo"><g fill="currentColor" transform="translate(-215.73 -1774.69)"><path d="M335.55,1825.19A147.75,147.75,0,0,1,483.3,1972.94h50.5c0-109.49-88.76-198.25-198.25-198.25v50.5Z"/><path d="M269.36,1891.39A147.74,147.74,0,0,1,417.1,2039.13h50.5c0-109.49-88.75-198.24-198.24-198.24v50.5Z"/><path d="M414,2157.17a147.75,147.75,0,0,1-147.74-147.74h-50.5c0,109.49,88.75,198.24,198.24,198.24v-50.5Z"/><path d="M480.17,2091a147.74,147.74,0,0,1-147.74-147.75H281.92c0,109.49,88.76,198.25,198.25,198.25V2091Z"/><path d="M862.8,2017.5q-27.39,22.86-78.25,22.86h-65v112.19H654.82v-312h134q46.32,0,73.86,24.13t27.55,74.72Q890.2,1994.64,862.8,2017.5ZM813,1905.1q-12.37-10.36-34.7-10.38H719.59v91.87h58.75q22.32,0,34.7-11.22t12.39-35.56Q825.43,1915.48,813,1905.1Z"/><path d="M1045.69,1916.42c.78.08,2.51.19,5.19.32v61.81c-3.81-.42-7.2-.71-10.16-.85s-5.36-.21-7.2-.21q-36.4,0-48.89,23.71-7,13.33-7,41.06v110.29H916.89V1921.82h57.58V1962q14-23.07,24.34-31.54,16.94-14.18,44-14.18C1044,1916.32,1044.92,1916.35,1045.69,1916.42Z"/><path d="M1265.64,2124.32q-29.21,36.06-88.69,36.06t-88.69-36.06Q1059,2088.26,1059,2037.5q0-49.9,29.22-86.5t88.69-36.59q59.47,0,88.69,36.59t29.21,86.5Q1294.85,2088.26,1265.64,2124.32ZM1217.38,2091q14.17-18.81,14.18-53.48t-14.18-53.37q-14.19-18.7-40.64-18.71T1136,1984.13q-14.29,18.72-14.29,53.37T1136,2091q14.28,18.81,40.75,18.81T1217.38,2091Z"/><path d="M1371.81,2078.88q1.92,16.1,8.29,22.87,11.28,12.06,41.7,12.06,17.85,0,28.39-5.29t10.53-15.88a17.12,17.12,0,0,0-8.48-15.45q-8.49-5.28-63.12-18.2-39.33-9.73-55.41-24.35-16.08-14.39-16.09-41.49,0-32,25.14-54.93t70.75-23q43.26,0,70.53,17.25t31.29,59.59H1455q-1.27-11.64-6.58-18.42-10-12.27-34-12.28-19.74,0-28.13,6.14t-8.38,14.4c0,6.91,3,11.93,8.92,15q8.89,4.89,63,16.73,36,8.46,54.05,25.61,17.77,17.35,17.78,43.39,0,34.3-25.56,56t-79,21.7q-54.51,0-80.49-23t-26-58.53Z"/><path d="M1745.54,2124.32q-29.22,36.06-88.7,36.06t-88.69-36.06q-29.2-36.06-29.21-86.82,0-49.9,29.21-86.5t88.69-36.59q59.49,0,88.7,36.59t29.21,86.5Q1774.75,2088.26,1745.54,2124.32ZM1697.27,2091q14.19-18.81,14.19-53.48t-14.19-53.37q-14.18-18.7-40.64-18.71t-40.75,18.71q-14.28,18.72-14.28,53.37t14.28,53.48q14.3,18.81,40.75,18.81T1697.27,2091Z"/><path d="M1992.75,1946.59q28.24,29.84,28.23,87.63,0,61-27.58,92.93t-71.06,32q-27.69,0-46-13.76-10-7.62-19.6-22.23v120.24H1797V1921.82h57.79v34.08q9.79-15,20.88-23.71,20.23-15.43,48.15-15.45Q1964.53,1916.74,1992.75,1946.59Zm-46.3,43.39q-12.3-20.52-39.88-20.53-33.15,0-45.54,31.11-6.43,16.51-6.42,41.92,0,40.21,21.58,56.51,12.82,9.53,30.37,9.53,25.45,0,38.83-19.48t13.36-51.86Q1958.75,2010.51,1946.45,1990Z"/><path d="M2249.14,2124.32q-29.2,36.06-88.69,36.06t-88.69-36.06q-29.22-36.06-29.21-86.82,0-49.9,29.21-86.5t88.69-36.59q59.49,0,88.69,36.59t29.22,86.5Q2278.36,2088.26,2249.14,2124.32ZM2200.88,2091q14.19-18.81,14.18-53.48t-14.18-53.37q-14.18-18.7-40.64-18.71t-40.75,18.71q-14.28,18.72-14.29,53.37t14.29,53.48q14.3,18.81,40.75,18.81T2200.88,2091Z"/></g></svg>`;
@@ -0,0 +1,219 @@
1
+ // Copyright 2021-2026 Prosopo (UK) Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import { CaptchaType } from "@prosopo/types";
16
+
17
+ export type DemoCaptchaType =
18
+ | CaptchaType.frictionless
19
+ | CaptchaType.pow
20
+ | CaptchaType.puzzle
21
+ | CaptchaType.image;
22
+
23
+ export type DemoMode = "standard" | "invisible";
24
+
25
+ export type DemoRendering = "implicit" | "explicit" | "manual" | "bound";
26
+
27
+ export interface DemoSetup {
28
+ captchaType: DemoCaptchaType;
29
+ mode: DemoMode;
30
+ rendering: DemoRendering;
31
+ }
32
+
33
+ export interface DemoPage extends DemoSetup {
34
+ path: string;
35
+ }
36
+
37
+ export interface DemoOption<T> {
38
+ value: T;
39
+ label: string;
40
+ }
41
+
42
+ export interface CaptchaTypeOption extends DemoOption<DemoCaptchaType> {
43
+ description: string;
44
+ }
45
+
46
+ export const captchaTypeOptions: CaptchaTypeOption[] = [
47
+ {
48
+ value: CaptchaType.frictionless,
49
+ label: "Frictionless",
50
+ description: "Humans pass without a challenge. Bots get one.",
51
+ },
52
+ {
53
+ value: CaptchaType.pow,
54
+ label: "Proof of Work",
55
+ description: "The browser does a quick computation.",
56
+ },
57
+ {
58
+ value: CaptchaType.puzzle,
59
+ label: "Puzzle",
60
+ description: "Drag a piece into place.",
61
+ },
62
+ {
63
+ value: CaptchaType.image,
64
+ label: "Image",
65
+ description: "Pick the images that match.",
66
+ },
67
+ ];
68
+
69
+ export const modeOptions: DemoOption<DemoMode>[] = [
70
+ { value: "standard", label: "Standard" },
71
+ { value: "invisible", label: "Invisible" },
72
+ ];
73
+
74
+ export interface RenderingOption extends DemoOption<DemoRendering> {
75
+ description: string;
76
+ }
77
+
78
+ export const renderingOptions: RenderingOption[] = [
79
+ {
80
+ value: "implicit",
81
+ label: "Implicit",
82
+ description:
83
+ "Add a div with your site key and the script renders the widget.",
84
+ },
85
+ {
86
+ value: "explicit",
87
+ label: "Explicit",
88
+ description: "Your own code calls render() when it is ready.",
89
+ },
90
+ {
91
+ value: "manual",
92
+ label: "Manual start",
93
+ description:
94
+ "The widget shows straight away, but checks only start when you say.",
95
+ },
96
+ {
97
+ value: "bound",
98
+ label: "Bound button",
99
+ description: "Your own submit button opens the challenge.",
100
+ },
101
+ ];
102
+
103
+ const page = (
104
+ path: string,
105
+ captchaType: DemoCaptchaType,
106
+ mode: DemoMode,
107
+ rendering: DemoRendering,
108
+ ): DemoPage => ({ path, captchaType, mode, rendering });
109
+
110
+ const indexPage = page(
111
+ "index.html",
112
+ CaptchaType.frictionless,
113
+ "standard",
114
+ "implicit",
115
+ );
116
+
117
+ export const defaultSetup: DemoSetup = indexPage;
118
+
119
+ export const demoPages: DemoPage[] = [
120
+ indexPage,
121
+ page(
122
+ "frictionless-explicit.html",
123
+ CaptchaType.frictionless,
124
+ "standard",
125
+ "explicit",
126
+ ),
127
+ page(
128
+ "frictionless-manual-start.html",
129
+ CaptchaType.frictionless,
130
+ "standard",
131
+ "manual",
132
+ ),
133
+ page(
134
+ "invisible-frictionless-implicit.html",
135
+ CaptchaType.frictionless,
136
+ "invisible",
137
+ "implicit",
138
+ ),
139
+ page(
140
+ "invisible-frictionless-explicit.html",
141
+ CaptchaType.frictionless,
142
+ "invisible",
143
+ "explicit",
144
+ ),
145
+ page("pow-implicit.html", CaptchaType.pow, "standard", "implicit"),
146
+ page("pow-explicit.html", CaptchaType.pow, "standard", "explicit"),
147
+ page("invisible-pow-implicit.html", CaptchaType.pow, "invisible", "implicit"),
148
+ page("invisible-pow-explicit.html", CaptchaType.pow, "invisible", "explicit"),
149
+ page("puzzle-implicit.html", CaptchaType.puzzle, "standard", "implicit"),
150
+ page("puzzle-explicit.html", CaptchaType.puzzle, "standard", "explicit"),
151
+ page("puzzle-bind-explicit.html", CaptchaType.puzzle, "standard", "bound"),
152
+ page(
153
+ "invisible-puzzle-implicit.html",
154
+ CaptchaType.puzzle,
155
+ "invisible",
156
+ "implicit",
157
+ ),
158
+ page(
159
+ "invisible-puzzle-explicit.html",
160
+ CaptchaType.puzzle,
161
+ "invisible",
162
+ "explicit",
163
+ ),
164
+ page("image-implicit.html", CaptchaType.image, "standard", "implicit"),
165
+ page("image-explicit.html", CaptchaType.image, "standard", "explicit"),
166
+ page(
167
+ "invisible-image-implicit.html",
168
+ CaptchaType.image,
169
+ "invisible",
170
+ "implicit",
171
+ ),
172
+ page(
173
+ "invisible-image-explicit.html",
174
+ CaptchaType.image,
175
+ "invisible",
176
+ "explicit",
177
+ ),
178
+ ];
179
+
180
+ const isSetup = (candidate: DemoSetup, setup: DemoSetup): boolean =>
181
+ candidate.captchaType === setup.captchaType &&
182
+ candidate.mode === setup.mode &&
183
+ candidate.rendering === setup.rendering;
184
+
185
+ export const findPageByPath = (path: string): DemoPage | undefined =>
186
+ demoPages.find((demoPage) => demoPage.path === path);
187
+
188
+ // Not every combination has a page (there is no invisible manual start, for
189
+ // example), so fall back to implicit rendering, then to the standard mode.
190
+ export const resolvePage = (setup: DemoSetup): DemoPage =>
191
+ demoPages.find((demoPage) => isSetup(demoPage, setup)) ??
192
+ demoPages.find((demoPage) =>
193
+ isSetup(demoPage, { ...setup, rendering: "implicit" }),
194
+ ) ??
195
+ demoPages.find((demoPage) =>
196
+ isSetup(demoPage, { ...setup, mode: "standard", rendering: "implicit" }),
197
+ ) ??
198
+ indexPage;
199
+
200
+ export const renderingsFor = (
201
+ captchaType: DemoCaptchaType,
202
+ mode: DemoMode,
203
+ ): RenderingOption[] =>
204
+ renderingOptions.filter(({ value }) =>
205
+ demoPages.some((demoPage) =>
206
+ isSetup(demoPage, { captchaType, mode, rendering: value }),
207
+ ),
208
+ );
209
+
210
+ export const labelOf = <T>(options: DemoOption<T>[], value: T): string =>
211
+ options.find((option) => option.value === value)?.label ?? String(value);
212
+
213
+ export const pagePathFromFilename = (filename: string | undefined): string => {
214
+ const normalized = (filename ?? "").replace(/\\/g, "/");
215
+ return normalized.match(/^(?:.*\/)?(?:src|dist)\/(.+)$/)?.[1] ?? "";
216
+ };
217
+
218
+ export const relativePrefix = (pagePath: string): string =>
219
+ "../".repeat((pagePath.match(/\//g) ?? []).length);
@@ -11,6 +11,7 @@
11
11
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
+
14
15
  // Vite plugin to inject a popup/float placement switcher into HTML files.
15
16
  //
16
17
  // Placement is resolved once, when the widget renders, so switching it means
@@ -20,57 +21,17 @@
20
21
  // script of their own, so the injected script stamps `data-placement` onto
21
22
  // their containers instead, which exercises the attribute path.
22
23
  import type { IndexHtmlTransformContext, Plugin } from "vite";
24
+ import { fillSlot } from "./slots.js";
23
25
 
24
26
  export default function placementInjector(): Plugin {
25
- const placementCss = `
26
- <style>
27
- .placement-switcher {
28
- margin: 20px 0;
29
- padding: 12px 15px;
30
- background-color: #f0f8ff;
31
- border: 2px solid #2196F3;
32
- border-radius: 5px;
33
- font-family: monospace;
34
- font-size: 14px;
35
- }
36
- .placement-switcher-title {
37
- font-weight: bold;
38
- margin-bottom: 10px;
39
- border-bottom: 1px solid #2196F3;
40
- padding-bottom: 5px;
41
- }
42
- .placement-option {
43
- display: inline-block;
44
- text-decoration: none;
45
- margin-right: 8px;
46
- padding: 6px 14px;
47
- border: 1px solid #2196F3;
48
- border-radius: 4px;
49
- background-color: white;
50
- color: #2196F3;
51
- cursor: pointer;
52
- font-family: inherit;
53
- font-size: inherit;
54
- }
55
- .placement-option[aria-current="page"] {
56
- background-color: #2196F3;
57
- color: white;
58
- font-weight: bold;
59
- }
60
- .placement-note {
61
- margin-top: 10px;
62
- color: #555;
63
- }
64
- </style>
65
- `;
66
-
67
27
  const placementHtml = `
68
- <!-- Challenge placement switcher -->
69
- <div id="placement-switcher" class="placement-switcher">
70
- <div class="placement-switcher-title">Challenge placement</div>
71
- <a class="placement-option" data-placement-option="popup" href="?placement=popup">popup</a>
72
- <a class="placement-option" data-placement-option="float" href="?placement=float">float</a>
73
- <div id="placement-note" class="placement-note"></div>
28
+ <div id="placement-switcher" class="demo-group">
29
+ <div class="demo-group__label">Challenge placement</div>
30
+ <nav class="demo-segmented" aria-label="Challenge placement">
31
+ <a data-placement-option="popup" href="?placement=popup">Popup</a>
32
+ <a data-placement-option="float" href="?placement=float">Float</a>
33
+ </nav>
34
+ <p id="placement-note" class="demo-placement-note"></p>
74
35
  </div>
75
36
  `;
76
37
 
@@ -96,10 +57,10 @@ export default function placementInjector(): Plugin {
96
57
  var note = document.getElementById("placement-note");
97
58
  if (note) {
98
59
  note.textContent = placement === "float" && invisible
99
- ? "float requested, but an invisible widget has nothing to anchor to, so the challenge opens as a popup."
60
+ ? "An invisible widget has nothing to anchor to, so the challenge opens as a popup."
100
61
  : placement === "float"
101
- ? "The challenge opens directly above the widget and stays pinned there while you scroll. The page stays usable behind it."
102
- : "The challenge opens centred over the page. This is the default.";
62
+ ? "The challenge opens next to the widget and stays pinned while you scroll."
63
+ : "The challenge opens in the middle of the page.";
103
64
  }
104
65
 
105
66
  // Links rather than buttons, and not only because navigation is what
@@ -124,7 +85,7 @@ export default function placementInjector(): Plugin {
124
85
  return {
125
86
  name: "placement-injector",
126
87
  transformIndexHtml: {
127
- order: "post", // after the nav bar and status log are in place
88
+ order: "post",
128
89
  handler(html: string, _ctx: IndexHtmlTransformContext): string {
129
90
  if (!html.includes("<body") || !html.includes("</body>")) {
130
91
  return html;
@@ -134,19 +95,19 @@ export default function placementInjector(): Plugin {
134
95
  return html;
135
96
  }
136
97
 
137
- const withCss = html.replace("</head>", `${placementCss}</head>`);
138
-
139
- // The switcher goes above the status log so the widget and its
140
- // controls stay together, and the script goes last so every
141
- // element it touches has been parsed.
142
- const withSwitcher = withCss.includes('<div id="captcha-status"')
143
- ? withCss.replace(
144
- '<div id="captcha-status"',
145
- `${placementHtml}<div id="captcha-status"`,
146
- )
147
- : withCss.replace("</body>", `${placementHtml}</body>`);
98
+ // Without the layout, the switcher goes above the status log so the
99
+ // widget and its controls stay together.
100
+ const withSwitcher =
101
+ fillSlot(html, "placement", placementHtml) ??
102
+ (html.includes('<div id="captcha-status"')
103
+ ? html.replace(
104
+ '<div id="captcha-status"',
105
+ () => `${placementHtml}<div id="captcha-status"`,
106
+ )
107
+ : html.replace("</body>", () => `${placementHtml}</body>`));
148
108
 
149
- return withSwitcher.replace("</body>", `${placementJs}</body>`);
109
+ // The script goes last so every element it touches has been parsed.
110
+ return withSwitcher.replace("</body>", () => `${placementJs}</body>`);
150
111
  },
151
112
  },
152
113
  };
@@ -0,0 +1,32 @@
1
+ // Copyright 2021-2026 Prosopo (UK) Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ // The layout plugin leaves empty placeholders that the other plugins fill, so
16
+ // each plugin still works on its own when the layout is not there.
17
+
18
+ export type DemoSlot = "placement" | "toolbar" | "events" | "code";
19
+
20
+ export const slotMarkup = (slot: DemoSlot): string =>
21
+ `<div data-demo-slot="${slot}"></div>`;
22
+
23
+ export const fillSlot = (
24
+ html: string,
25
+ slot: DemoSlot,
26
+ content: string,
27
+ ): string | undefined => {
28
+ const marker = slotMarkup(slot);
29
+ return html.includes(marker)
30
+ ? html.replace(marker, () => content)
31
+ : undefined;
32
+ };