>,
- TError,
- void,
- TContext
-> => {
- const mutationOptions = getMtaInboundEmailCreateMutationOptions(options);
-
- return useMutation(mutationOptions, queryClient);
-};
diff --git a/src/frontend/src/features/auth/index.tsx b/src/frontend/src/features/auth/index.tsx
index b9d8f313..29e43bf0 100644
--- a/src/frontend/src/features/auth/index.tsx
+++ b/src/frontend/src/features/auth/index.tsx
@@ -3,11 +3,9 @@ import React, { PropsWithChildren, useEffect } from "react";
import { getRequestUrl } from "@/features/api/utils";
import { useUsersMeRetrieve } from "@/features/api/gen/users/users";
import { Spinner } from "@gouvfr-lasuite/ui-kit";
-import { posthog } from "posthog-js";
import { UserWithAbilities } from "../api/gen/models/user_with_abilities";
export const logout = () => {
- posthog.reset()
window.location.replace(getRequestUrl("/api/v1.0/logout/"));
};
@@ -37,16 +35,6 @@ export const Auth = ({
}
}, [query.isError, redirect]);
- useEffect(() => {
- if (query.data?.data) {
- const user = query.data.data;
- posthog.identify(user.id, {
- email: user.email || undefined,
- name: user.full_name || undefined,
- });
- }
- }, [query.data?.data]);
-
if (!query.isFetched) {
return (
{leftIcon}
- {
- isDesktop && (
-
- )
- }
{isDesktop && (
diff --git a/src/frontend/src/features/layouts/components/main/left-panel/index.tsx b/src/frontend/src/features/layouts/components/main/left-panel/index.tsx
index 778ffa5d..49f201de 100644
--- a/src/frontend/src/features/layouts/components/main/left-panel/index.tsx
+++ b/src/frontend/src/features/layouts/components/main/left-panel/index.tsx
@@ -1,16 +1,12 @@
import { useResponsive } from "@gouvfr-lasuite/ui-kit";
-import { usePostHog } from "posthog-js/react";
import { useAuth } from "@/features/auth";
import { HeaderRight } from "../header/authenticated";
-import { PostHogSurveyButton } from "@/features/ui/components/feedback-button";
-import { useConfig } from "@/features/providers/config";
+import { SurveyButton } from "@/features/ui/components/feedback-button";
import { MailboxPanel } from "../../mailbox-panel";
import { LanguagePicker } from "../language-picker";
export const LeftPanel = ({ hasNoMailbox = true }: { hasNoMailbox?: boolean }) => {
const { user } = useAuth();
- const posthog = usePostHog();
- const config = useConfig();
const { isTablet } = useResponsive();
if (!isTablet && hasNoMailbox) return null;
@@ -25,9 +21,9 @@ export const LeftPanel = ({ hasNoMailbox = true }: { hasNoMailbox?: boolean }) =
{user ? : }
}
- {posthog.__loaded && config.POSTHOG_SURVEY_ID && (
+ {process.env.NEXT_PUBLIC_FEEDBACK_WIDGET_CHANNEL && (
)}
diff --git a/src/frontend/src/features/providers/config.tsx b/src/frontend/src/features/providers/config.tsx
index fd4b81ac..817cfcfa 100644
--- a/src/frontend/src/features/providers/config.tsx
+++ b/src/frontend/src/features/providers/config.tsx
@@ -4,9 +4,6 @@ import { PropsWithChildren, createContext, useContext, useMemo } from "react";
const DEFAULT_CONFIG: ConfigRetrieve200 = {
ENVIRONMENT: "",
- POSTHOG_KEY: null,
- POSTHOG_HOST: null,
- POSTHOG_SURVEY_ID: null,
LANGUAGES: [],
LANGUAGE_CODE: "",
AI_ENABLED: false,
diff --git a/src/frontend/src/features/providers/posthog.tsx b/src/frontend/src/features/providers/posthog.tsx
deleted file mode 100644
index 6536bff7..00000000
--- a/src/frontend/src/features/providers/posthog.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { PostHogProvider as PostHogProviderBase } from "posthog-js/react";
-import { PropsWithChildren } from "react";
-import { useConfig } from './config';
-
-/**
- * A global provider in charge of initializing PostHog if the config has
- * the POSTHOG_KEY and POSTHOG_HOST set.
- */
-export const PostHogProvider = ({ children, }: PropsWithChildren) => {
- const config = useConfig();
-
- if (!config?.POSTHOG_KEY || !config?.POSTHOG_HOST) {
- return children;
- }
-
- return (
-
- {children}
-
- );
-};
diff --git a/src/frontend/src/features/ui/components/feedback-button/index.tsx b/src/frontend/src/features/ui/components/feedback-button/index.tsx
index fd6c3326..a9d043a9 100644
--- a/src/frontend/src/features/ui/components/feedback-button/index.tsx
+++ b/src/frontend/src/features/ui/components/feedback-button/index.tsx
@@ -1,33 +1,81 @@
-import { useConfig } from "@/features/providers/config"
import { Icon, IconType } from "@gouvfr-lasuite/ui-kit"
import { Button, ButtonProps } from "@openfun/cunningham-react"
-import { usePostHog } from "posthog-js/react"
import { useTranslation } from "react-i18next"
+import { useAuth } from "@/features/auth";
/**
- * A button that opens the PostHog survey modal.
- *
- * This button is only visible if PostHog is loaded. To work, a survey must be
- * created in PostHog with type Feedback button and as CSS Selector you must
- * use `#posthog-feedback-survey`.
- *
+ * A button that opens the feedback widget
*/
-export const PostHogSurveyButton = (props: ButtonProps) => {
+export const SurveyButton = (props: ButtonProps) => {
const { t } = useTranslation()
- const posthog = usePostHog()
- const config = useConfig()
+ const { user } = useAuth();
+
+ const apiUrl = process.env.NEXT_PUBLIC_FEEDBACK_WIDGET_API_URL;
+ const widgetPath = process.env.NEXT_PUBLIC_FEEDBACK_WIDGET_PATH;
+ const channel = process.env.NEXT_PUBLIC_FEEDBACK_WIDGET_CHANNEL;
+
+ if (!channel || !apiUrl || !widgetPath) return null;
+
+ const title: string = t("feedback_widget.title");
+ const placeholder: string = t("feedback_widget.placeholder");
+ const emailPlaceholder: string = t("feedback_widget.email_placeholder");
+ const submitText: string = t("feedback_widget.submit_text");
+ const successText: string = t("feedback_widget.success_text");
+ const successText2: string = t("feedback_widget.success_text2");
+
+
+ const showWidget = () => {
+ // Initialize the widget array if it doesn't exist
+ if (typeof window !== "undefined" && widgetPath) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (window as any)._stmsg_widget = (window as any)._stmsg_widget || [];
+
+ // Construct script URLs from the base path
+ const feedbackScript = `${widgetPath}feedback.js`;
+
+ // Push the widget configuration
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (window as any)._stmsg_widget.push([
+ "feedback",
+ "init",
+ {
+ title,
+ api: apiUrl,
+ channel,
+ placeholder,
+ emailPlaceholder,
+ submitText,
+ successText,
+ successText2,
+ // Add email parameter if user is logged in
+ ...(user?.email && { email: user.email }),
+ },
+ ]);
+
+ // Load the loader script if not already loaded
+ if (!document.querySelector(`script[src="${feedbackScript}"]`)) {
+ const script = document.createElement("script");
+ script.async = true;
+ script.src = feedbackScript;
+ const firstScript = document.getElementsByTagName("script")[0];
+ if (firstScript && firstScript.parentNode) {
+ firstScript.parentNode.insertBefore(script, firstScript);
+ }
+ }
+ }
+ }
- if (!config.POSTHOG_SURVEY_ID || !posthog.__loaded) return null;
return (
}
color="tertiary"
- className="feedback-button posthog-feedback-survey"
- title={t("posthog.cta")}
+ className="feedback-button"
+ title={t("feedback_widget.title")}
+ onClick={showWidget}
>
- {t("posthog.cta")}
+ {t("feedback_widget.shortTitle")}
)
}
diff --git a/src/frontend/src/features/ui/components/feedback-widget/index.tsx b/src/frontend/src/features/ui/components/feedback-widget/index.tsx
new file mode 100644
index 00000000..815ffbf1
--- /dev/null
+++ b/src/frontend/src/features/ui/components/feedback-widget/index.tsx
@@ -0,0 +1,79 @@
+import { useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import { useAuth } from "@/features/auth";
+
+interface FeedbackWidgetProps {
+ widget?: string;
+}
+
+export function FeedbackWidget({
+ widget = "feedback",
+}: FeedbackWidgetProps) {
+ const { t } = useTranslation();
+ const { user } = useAuth();
+
+ const apiUrl = process.env.NEXT_PUBLIC_FEEDBACK_WIDGET_API_URL;
+ const widgetPath = process.env.NEXT_PUBLIC_FEEDBACK_WIDGET_PATH;
+ const channel = process.env.NEXT_PUBLIC_FEEDBACK_WIDGET_CHANNEL;
+
+ const title: string = t("feedback_widget.title");
+ const placeholder: string = t("feedback_widget.placeholder");
+ const emailPlaceholder: string = t("feedback_widget.email_placeholder");
+ const submitText: string = t("feedback_widget.submit_text");
+ const successText: string = t("feedback_widget.success_text");
+ const successText2: string = t("feedback_widget.success_text2");
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ useEffect(() => {
+ if (!channel || !apiUrl || !widgetPath) return;
+
+ // Initialize the widget array if it doesn't exist
+ if (typeof window !== "undefined" && widgetPath) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (window as any)._stmsg_widget = (window as any)._stmsg_widget || [];
+
+ // Construct script URLs from the base path
+ const loaderScript = `${widgetPath}loader.js`;
+ const feedbackScript = `${widgetPath}feedback.js`;
+
+ // Push the widget configuration
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (window as any)._stmsg_widget.push([
+ "loader",
+ "init",
+ {
+ params: {
+ title,
+ api: apiUrl,
+ channel,
+ placeholder,
+ emailPlaceholder,
+ submitText,
+ successText,
+ successText2,
+ // Add email parameter if user is logged in
+ ...(user?.email && { email: user.email }),
+ },
+ script: feedbackScript,
+ widget,
+ label: title,
+ },
+ ]);
+
+ // Load the loader script if not already loaded
+ if (!document.querySelector(`script[src="${loaderScript}"]`)) {
+ const script = document.createElement("script");
+ script.async = true;
+ script.src = loaderScript;
+ const firstScript = document.getElementsByTagName("script")[0];
+ if (firstScript && firstScript.parentNode) {
+ firstScript.parentNode.insertBefore(script, firstScript);
+ }
+ }
+ }
+ }, [title, channel, apiUrl, widgetPath, widget, emailPlaceholder, submitText, successText, successText2, user?.email]);
+
+ // This component doesn't render anything visible
+ // The widget is injected via the script
+ return null;
+}
diff --git a/src/frontend/src/pages/_app.tsx b/src/frontend/src/pages/_app.tsx
index 95577387..e903f38b 100644
--- a/src/frontend/src/pages/_app.tsx
+++ b/src/frontend/src/pages/_app.tsx
@@ -9,7 +9,7 @@ import {
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
-import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
+// import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import "../styles/main.scss";
import "../features/i18n/initI18n";
@@ -22,7 +22,6 @@ import Head from "next/head";
import { useTranslation } from "react-i18next";
import { Auth } from "@/features/auth";
import { ConfigProvider } from "@/features/providers/config";
-import { PostHogProvider } from "@/features/providers/posthog";
export type NextPageWithLayout = NextPage
& {
getLayout?: (page: ReactElement) => ReactNode;
@@ -86,15 +85,13 @@ export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
/>
-
+ {/* */}
-
-
-
- {getLayout()}
-
-
-
+
+
+ {getLayout()}
+
+
>
diff --git a/src/frontend/src/pages/_document.tsx b/src/frontend/src/pages/_document.tsx
index 743d7c35..9cff10fd 100644
--- a/src/frontend/src/pages/_document.tsx
+++ b/src/frontend/src/pages/_document.tsx
@@ -1,6 +1,8 @@
import { Html, Head, Main, NextScript } from "next/document";
import { useTranslation } from "react-i18next";
+
+
export default function Document() {
const { t } = useTranslation();
diff --git a/src/frontend/src/pages/index.tsx b/src/frontend/src/pages/index.tsx
index 365303f4..49caf5e8 100644
--- a/src/frontend/src/pages/index.tsx
+++ b/src/frontend/src/pages/index.tsx
@@ -5,6 +5,7 @@ import { MainLayout } from "@/features/layouts/components/main";
import { LanguagePicker } from "@/features/layouts/components/main/language-picker";
import { AppLayout } from "@/features/layouts/components/main/layout";
import { LeftPanel } from "@/features/layouts/components/main/left-panel";
+import { FeedbackWidget } from "@/features/ui/components/feedback-widget";
export default function HomePage() {
@@ -35,6 +36,7 @@ export default function HomePage() {
+
);
}
diff --git a/src/mta-in/README.md b/src/mta-in/README.md
index 54adaa42..5c54fe5b 100644
--- a/src/mta-in/README.md
+++ b/src/mta-in/README.md
@@ -9,9 +9,9 @@ This MTA container is based on standard technologies such as Postfix with a cust
It is battle-tested with a complete Python test suite.
After receiving an email through SMTP, it processes each message synchronously during the SMTP session using a custom Postfix milter that:
- - Validates each recipient with a REST API call to `{env.MDA_API_BASE_URL}/check-recipients` during the RCPT TO command
- - Delivers the complete message via REST API call to `{env.MDA_API_BASE_URL}/inbound-email` during the DATA command
- - Either accepts (discards from queue) or rejects the SMTP session based on delivery results
+- Validates each recipient with a REST API call to `{env.MDA_API_BASE_URL}/inbound/mta/check/` during the RCPT TO command
+- Delivers the complete message via REST API call to `{env.MDA_API_BASE_URL}/inbound/mta/deliver/` during the DATA command
+- Either accepts (discards from queue) or rejects the SMTP session based on delivery results
This architecture ensures true synchronous delivery - delivery failures cause immediate SMTP session rejection, and successful deliveries prevent the message from entering the Postfix queue.
diff --git a/src/mta-in/src/delivery_milter.py b/src/mta-in/src/delivery_milter.py
index b4fc1fdf..98b71cd4 100644
--- a/src/mta-in/src/delivery_milter.py
+++ b/src/mta-in/src/delivery_milter.py
@@ -63,7 +63,7 @@ class DeliveryMilter(Milter.Base):
try:
# Check if recipient exists via MDA API
status_code, response = mda_api_call(
- "check-recipients/",
+ "inbound/mta/check/",
"application/json",
json.dumps({"addresses": [clean_to]}).encode("utf-8"),
{},
@@ -122,7 +122,7 @@ class DeliveryMilter(Milter.Base):
# Perform synchronous delivery via MDA API
status_code, response = mda_api_call(
- "inbound-email/",
+ "inbound/mta/deliver/",
"message/rfc822",
message_content,
{
diff --git a/src/mta-in/tests/conftest.py b/src/mta-in/tests/conftest.py
index 4b027a07..6a2bacb2 100644
--- a/src/mta-in/tests/conftest.py
+++ b/src/mta-in/tests/conftest.py
@@ -67,7 +67,7 @@ class MockAPIServer:
return await call_next(request)
- @self.app.post("/api/mail/inbound-email/")
+ @self.app.post("/api/mail/inbound/mta/deliver/")
async def receive_mail(request: Request):
logger.info("Email received by API!")
@@ -93,7 +93,7 @@ class MockAPIServer:
self.received_emails.append(email_data)
return {"status": "ok"}
- @self.app.post("/api/mail/check-recipients/")
+ @self.app.post("/api/mail/inbound/mta/check/")
async def check_recipient(request: Request):
logger.info("Recipient check received")
data = await request.json()
diff --git a/src/nginx/servers.conf.erb b/src/nginx/servers.conf.erb
index b5438196..3cd728bf 100644
--- a/src/nginx/servers.conf.erb
+++ b/src/nginx/servers.conf.erb
@@ -19,7 +19,7 @@ server {
location ^~ /api/ {
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-For $remote_addr;
proxy_redirect off;
proxy_pass http://backend_server;
@@ -29,7 +29,7 @@ server {
location ^~ /<%= ENV["DJANGO_ADMIN_URL"] || "admin/" %> {
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-For $remote_addr;
proxy_redirect off;
proxy_pass http://backend_server;
diff --git a/src/frontend/Dockerfile.dev b/src/widgets/Dockerfile
similarity index 68%
rename from src/frontend/Dockerfile.dev
rename to src/widgets/Dockerfile
index 86d039c3..24a7a247 100644
--- a/src/frontend/Dockerfile.dev
+++ b/src/widgets/Dockerfile
@@ -1,6 +1,6 @@
-FROM node:22-slim AS frontend-deps
+FROM node:22-slim AS widgets-deps
-WORKDIR /home/frontend/
+WORKDIR /home/widgets/
RUN npm install -g npm@11.3.0 && npm cache clean -f
diff --git a/src/widgets/build.js b/src/widgets/build.js
new file mode 100644
index 00000000..1438cfc5
--- /dev/null
+++ b/src/widgets/build.js
@@ -0,0 +1,30 @@
+import { build } from 'vite'
+import { readdirSync } from 'node:fs'
+import { join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const __dirname = fileURLToPath(new URL('.', import.meta.url))
+
+const widgetsDir = join(__dirname, 'src', 'widgets')
+
+function discoverWidgets() {
+ return readdirSync(widgetsDir, { withFileTypes: true })
+ .filter(dirent => dirent.isDirectory())
+ .map(dirent => dirent.name)
+}
+
+// Run an independent build for each widget
+for (const widget of discoverWidgets()) {
+ await build({
+ build: {
+ emptyOutDir:false,
+ rollupOptions: {
+ input: join(widgetsDir, widget, 'main.ts'),
+ output: {
+ entryFileNames: widget + '.js',
+ format: 'iife'
+ }
+ },
+ },
+ })
+}
diff --git a/src/widgets/demo/api-error.json b/src/widgets/demo/api-error.json
new file mode 100644
index 00000000..3d1cff43
--- /dev/null
+++ b/src/widgets/demo/api-error.json
@@ -0,0 +1 @@
+{"success": true, "config": {"captcha": false, "submitUrl": "/error"}}
\ No newline at end of file
diff --git a/src/widgets/demo/api.json b/src/widgets/demo/api.json
new file mode 100644
index 00000000..ed13f726
--- /dev/null
+++ b/src/widgets/demo/api.json
@@ -0,0 +1 @@
+{"success": true, "config": {"captcha": false}}
\ No newline at end of file
diff --git a/src/widgets/demo/feedback.html b/src/widgets/demo/feedback.html
new file mode 100644
index 00000000..5ef5364e
--- /dev/null
+++ b/src/widgets/demo/feedback.html
@@ -0,0 +1,85 @@
+
+
+
+
+
+ Feedback Widget Demo
+
+
+
+ Back to index
+ Feedback Widget Demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/widgets/demo/loader.html b/src/widgets/demo/loader.html
new file mode 100644
index 00000000..0409f2ed
--- /dev/null
+++ b/src/widgets/demo/loader.html
@@ -0,0 +1,69 @@
+
+
+
+
+
+ Loader Widget Demo
+
+
+
+ Back to index
+ Loader Widget Demo
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/widgets/index.html b/src/widgets/index.html
new file mode 100644
index 00000000..f6e947e9
--- /dev/null
+++ b/src/widgets/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ Widgets demo
+
+
+ Widgets demo
+
+
+
diff --git a/src/widgets/package-lock.json b/src/widgets/package-lock.json
new file mode 100644
index 00000000..db62c61b
--- /dev/null
+++ b/src/widgets/package-lock.json
@@ -0,0 +1,1044 @@
+{
+ "name": "widgets",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "widgets",
+ "version": "0.0.0",
+ "devDependencies": {
+ "typescript": "5.9.2",
+ "vite": "7.1.7"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
+ "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
+ "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
+ "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
+ "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
+ "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
+ "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
+ "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
+ "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
+ "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
+ "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
+ "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
+ "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
+ "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
+ "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
+ "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
+ "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
+ "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
+ "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
+ "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz",
+ "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
+ "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz",
+ "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
+ "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
+ "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
+ "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
+ "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz",
+ "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz",
+ "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz",
+ "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz",
+ "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz",
+ "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz",
+ "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz",
+ "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz",
+ "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz",
+ "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz",
+ "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz",
+ "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz",
+ "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz",
+ "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz",
+ "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz",
+ "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz",
+ "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz",
+ "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz",
+ "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz",
+ "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz",
+ "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
+ "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.8",
+ "@esbuild/android-arm": "0.25.8",
+ "@esbuild/android-arm64": "0.25.8",
+ "@esbuild/android-x64": "0.25.8",
+ "@esbuild/darwin-arm64": "0.25.8",
+ "@esbuild/darwin-x64": "0.25.8",
+ "@esbuild/freebsd-arm64": "0.25.8",
+ "@esbuild/freebsd-x64": "0.25.8",
+ "@esbuild/linux-arm": "0.25.8",
+ "@esbuild/linux-arm64": "0.25.8",
+ "@esbuild/linux-ia32": "0.25.8",
+ "@esbuild/linux-loong64": "0.25.8",
+ "@esbuild/linux-mips64el": "0.25.8",
+ "@esbuild/linux-ppc64": "0.25.8",
+ "@esbuild/linux-riscv64": "0.25.8",
+ "@esbuild/linux-s390x": "0.25.8",
+ "@esbuild/linux-x64": "0.25.8",
+ "@esbuild/netbsd-arm64": "0.25.8",
+ "@esbuild/netbsd-x64": "0.25.8",
+ "@esbuild/openbsd-arm64": "0.25.8",
+ "@esbuild/openbsd-x64": "0.25.8",
+ "@esbuild/openharmony-arm64": "0.25.8",
+ "@esbuild/sunos-x64": "0.25.8",
+ "@esbuild/win32-arm64": "0.25.8",
+ "@esbuild/win32-ia32": "0.25.8",
+ "@esbuild/win32-x64": "0.25.8"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz",
+ "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.46.2",
+ "@rollup/rollup-android-arm64": "4.46.2",
+ "@rollup/rollup-darwin-arm64": "4.46.2",
+ "@rollup/rollup-darwin-x64": "4.46.2",
+ "@rollup/rollup-freebsd-arm64": "4.46.2",
+ "@rollup/rollup-freebsd-x64": "4.46.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.46.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.46.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.46.2",
+ "@rollup/rollup-linux-arm64-musl": "4.46.2",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.46.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.46.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.46.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.46.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.46.2",
+ "@rollup/rollup-linux-x64-gnu": "4.46.2",
+ "@rollup/rollup-linux-x64-musl": "4.46.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.46.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.46.2",
+ "@rollup/rollup-win32-x64-msvc": "4.46.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz",
+ "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/src/widgets/package.json b/src/widgets/package.json
new file mode 100644
index 00000000..b551e4f5
--- /dev/null
+++ b/src/widgets/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@gouvfr-lasuite/widgets",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host 0.0.0.0 --port 8905",
+ "build": "tsc && node build.js",
+ "preview": "vite preview --host 0.0.0.0 --port 8905"
+ },
+ "devDependencies": {
+ "typescript": "5.9.2",
+ "vite": "7.1.7"
+ }
+}
diff --git a/src/widgets/src/shared/events.ts b/src/widgets/src/shared/events.ts
new file mode 100644
index 00000000..d1c60bd6
--- /dev/null
+++ b/src/widgets/src/shared/events.ts
@@ -0,0 +1,11 @@
+const NAMESPACE = `stmsg-widget`;
+
+export const triggerEvent = (widgetName: string, eventName: string, detail?: any, root?: any) => {
+ return (root || document).dispatchEvent(new CustomEvent(`${NAMESPACE}-${widgetName}-${eventName}`, detail ? { detail } : undefined));
+}
+
+export const listenEvent = (widgetName: string, eventName: string, root: any, once: boolean, callback: (data: any) => void) => {
+ const cb = (e: CustomEvent) => callback(e.detail);
+ (root || document).addEventListener(`${NAMESPACE}-${widgetName}-${eventName}`, cb, once ? { once: true } : undefined);
+ return () => (root || document).removeEventListener(`${NAMESPACE}-${widgetName}-${eventName}`, cb, once ? { once: true } : undefined);
+}
diff --git a/src/widgets/src/shared/script.ts b/src/widgets/src/shared/script.ts
new file mode 100644
index 00000000..742973b8
--- /dev/null
+++ b/src/widgets/src/shared/script.ts
@@ -0,0 +1,72 @@
+import { triggerEvent } from './events';
+
+type WidgetEvent = [string, string, any];
+
+type EventArray = Array & { _loaded?: Record };
+
+declare global {
+ var _stmsg_widget: EventArray;
+}
+
+// This could have been an enum but we want to support erasableSyntaxOnly TS settings
+export const STATE_NOT_LOADED = 0;
+export const STATE_LOADING = 1;
+export const STATE_LOADED = 2;
+
+export const getLoaded = (widgetName: string) => {
+ return window._stmsg_widget?._loaded?.[widgetName];
+}
+
+export const setLoaded = (widgetName: string, status: number) => {
+ if (!window._stmsg_widget?._loaded) return;
+ window._stmsg_widget._loaded[widgetName] = status;
+}
+
+// Replace the push method of the _stmsg_widget array used for communication between the widget and the page
+export const installHook = (widgetName: string) => {
+
+ if (!window._stmsg_widget) {
+ window._stmsg_widget = [] as EventArray;
+ }
+ const W = window._stmsg_widget;
+
+ // Keep track of the loaded state of each widget
+ if (!W._loaded) {
+ W._loaded = {} as Record;
+ }
+
+ if (getLoaded(widgetName) !== STATE_LOADED) {
+ // Replace the push method of the _stmsg_widget array used for communication between the widget and the page
+ W.push = ((...elts: WidgetEvent[]): number => {
+ for (const elt of elts) {
+ // If the target widget is loaded, fire the event
+ if (getLoaded(elt[0]) === STATE_LOADED) {
+ triggerEvent(elt[0], elt[1], elt[2]);
+ } else {
+ W[W.length] = elt;
+ }
+ }
+ return W.length;
+ }) as typeof Array.prototype.push;
+
+ setLoaded(widgetName, STATE_LOADED);
+
+ // Empty the existing array and re-push all events that were received before the hook was installed
+ for (const evt of W.splice(0, W.length)) {
+ W.push(evt);
+ }
+ }
+
+ // Finally, fire an event to signal that we are loaded
+ triggerEvent(widgetName, 'loaded');
+
+}
+
+// Loads another widget from the same directory
+export const injectScript = (url: string, type: string = "") => {
+ const newScript = document.createElement('script');
+ newScript.src = url;
+ newScript.type = type;
+ newScript.defer = true;
+ document.body.appendChild(newScript);
+}
\ No newline at end of file
diff --git a/src/widgets/src/shared/shadow-dom.ts b/src/widgets/src/shared/shadow-dom.ts
new file mode 100644
index 00000000..c8f94916
--- /dev/null
+++ b/src/widgets/src/shared/shadow-dom.ts
@@ -0,0 +1,30 @@
+// Shared utility for creating shadow DOM widgets
+export function createShadowWidget(widgetName: string, htmlContent: string, cssContent: string): HTMLDivElement {
+ const id = `stmsg-widget-${widgetName}-shadow`;
+ // Check if widget already exists
+ const existingWidget = document.getElementById(id);
+ if (existingWidget) {
+ existingWidget.remove();
+ }
+
+ // Create container element
+ const container = document.createElement('div');
+ container.id = id;
+
+ // Create shadow root
+ const shadow = container.attachShadow({ mode: 'open' });
+
+ // Create style element for scoped CSS
+ const style = document.createElement('style');
+ style.textContent = cssContent;
+
+ // Create content element
+ const content = document.createElement('div');
+ content.innerHTML = htmlContent;
+
+ // Append style and content to shadow DOM
+ shadow.appendChild(style);
+ shadow.appendChild(content);
+
+ return container;
+}
\ No newline at end of file
diff --git a/src/widgets/src/vite-env.d.ts b/src/widgets/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/src/widgets/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/src/widgets/src/widgets/feedback/icon.svg b/src/widgets/src/widgets/feedback/icon.svg
new file mode 100644
index 00000000..0519ecba
--- /dev/null
+++ b/src/widgets/src/widgets/feedback/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/widgets/src/widgets/feedback/main.ts b/src/widgets/src/widgets/feedback/main.ts
new file mode 100644
index 00000000..b51da321
--- /dev/null
+++ b/src/widgets/src/widgets/feedback/main.ts
@@ -0,0 +1,164 @@
+import styles from './styles.css?inline'
+import { createShadowWidget } from '../../shared/shadow-dom'
+import { installHook } from '../../shared/script'
+import { listenEvent, triggerEvent } from '../../shared/events'
+
+const widgetName = "feedback";
+
+type ConfigData = {
+ title?: string;
+ placeholder?: string;
+ emailPlaceholder?: string;
+ submitText?: string;
+ successText?: string;
+ successText2?: string;
+ submitUrl?: string;
+};
+
+type ConfigResponse = {
+ success?: boolean;
+ detail?: string;
+ captcha?: boolean;
+ config?: ConfigData;
+};
+
+listenEvent(widgetName, 'init', null, false, async (args) => {
+
+ if (!args.api || !args.channel) {
+ console.error("Feedback widget requires an API URL and a channel ID");
+ return;
+ }
+
+ let configData: ConfigData | undefined;
+ try {
+ const config = await fetch(`${args.api}config/`, {
+ 'headers': {
+ 'X-Channel-ID': args.channel
+ }
+ });
+ const configResponse = await config.json() as ConfigResponse;
+ if (!configResponse.success) throw new Error(configResponse.detail || 'Unknown error');
+ if (configResponse.captcha) throw new Error('Captcha is not supported yet');
+ configData = configResponse.config;
+ } catch (error) {
+ console.error("Error fetching config", error);
+ triggerEvent(widgetName, 'closed');
+ return;
+ }
+
+ const title = args.title || configData?.title || 'Feedback';
+ const placeholder = args.placeholder || configData?.placeholder || 'Share your feedback...';
+ const emailPlaceholder = args.emailPlaceholder || configData?.emailPlaceholder || 'Your email...';
+ const submitText = args.submitText || configData?.submitText || 'Send Feedback';
+ const successText = args.successText || configData?.successText || 'Thank you for your feedback!';
+ const successText2 = args.successText2 || configData?.successText2;
+
+ const htmlContent = ``;
+
+ // Create shadow DOM widget
+ const shadowContainer = createShadowWidget(widgetName, htmlContent, styles);
+ const shadowRoot = shadowContainer.shadowRoot!;
+
+ const titleSpan = shadowRoot.querySelector('#title')!
+ const submitBtn = shadowRoot.querySelector('#submit')!
+ const feedbackText = shadowRoot.querySelector('#feedback-text')!
+ const statusDiv = shadowRoot.querySelector('#status')!
+ const closeBtn = shadowRoot.querySelector('#close')!
+ const emailInput = shadowRoot.querySelector('#email')!
+ const form = shadowRoot.querySelector('form')!
+
+ titleSpan.textContent = title;
+ feedbackText.placeholder = placeholder;
+ emailInput.placeholder = emailPlaceholder;
+ submitBtn.textContent = submitText;
+
+ if (args.email) {
+ emailInput.remove();
+ }
+
+ const setStatus = (status: string, success: boolean) => {
+ statusDiv.innerHTML = '';
+ const statusSpan = document.createElement('div');
+ statusSpan.id = 'statusmsg';
+ statusSpan.classList.add(success ? 'success' : 'error');
+ statusSpan.textContent = status;
+ statusDiv.appendChild(statusSpan);
+ if (successText2) {
+ const statusSpan2 = document.createElement('div');
+ statusSpan2.id = 'statusmsg2';
+ statusSpan2.classList.add('success');
+ statusSpan2.textContent = successText2;
+ statusDiv.appendChild(statusSpan2);
+ }
+ }
+
+ form.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const message = feedbackText.value.trim()
+ const email = args.email || emailInput.value.trim();
+ try {
+ if (!message) {
+ feedbackText.focus();
+ throw new Error("Missing value");
+ }
+ if (!email) {
+ emailInput.focus();
+ throw new Error("Missing value");
+ }
+
+ const ret = await fetch(configData?.submitUrl || `${args.api}deliver/`, {
+ 'method': 'POST',
+ 'headers': {
+ 'Content-Type': 'application/json',
+ 'X-Channel-ID': args.channel
+ },
+ 'body': JSON.stringify({ textBody: message, email })
+ });
+ let retData;
+ try {
+ retData = await ret.json();
+ } catch (error) {
+ throw new Error('Invalid response from server');
+ }
+
+ if (!retData.success) throw new Error(retData.detail || 'Unknown error');
+
+ setStatus(successText, true);
+
+ feedbackText.remove();
+ emailInput.remove();
+ submitBtn.remove();
+
+ } catch (error) {
+ setStatus(error instanceof Error ? error.message : 'Unknown error', false);
+ }
+ });
+
+ const closeWidget = () => {
+ shadowRoot.host.remove();
+ triggerEvent(widgetName, 'closed');
+ }
+
+ closeBtn.addEventListener('click', closeWidget);
+ listenEvent(widgetName, 'close', null, false, closeWidget);
+
+ document.body.appendChild(shadowContainer);
+
+ feedbackText.focus();
+
+ triggerEvent(widgetName, 'opened');
+
+});
+
+installHook(widgetName);
\ No newline at end of file
diff --git a/src/widgets/src/widgets/feedback/styles.css b/src/widgets/src/widgets/feedback/styles.css
new file mode 100644
index 00000000..a40c0904
--- /dev/null
+++ b/src/widgets/src/widgets/feedback/styles.css
@@ -0,0 +1,111 @@
+/* Widget styles */
+#wrapper {
+ position: fixed;
+ bottom: 100px;
+ right: 20px;
+ z-index: 1000;
+ width: 350px;
+ height: 400px;
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+#header {
+ background: #000091;
+ color: white;
+ padding: 16px;
+ font-weight: 600;
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+#close {
+ background: none;
+ border: none;
+ color: white;
+ cursor: pointer;
+ font-size: 18px;
+ padding: 0;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+#close:hover {
+ opacity: 0.8;
+}
+
+#content {
+ flex: 1;
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+#email {
+ border: 1px solid #e0e0e0;
+ border-radius: 8px;
+ padding: 12px;
+ font-family: inherit;
+ font-size: 14px;
+}
+
+textarea {
+ flex: 1;
+ border: 1px solid #e0e0e0;
+ border-radius: 8px;
+ padding: 12px;
+ font-family: inherit;
+ font-size: 14px;
+ resize: none;
+}
+
+button[type="submit"] {
+ background: #000091;
+ color: white;
+ border: none;
+ border-radius: 8px;
+ padding: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+button[type="submit"]:hover {
+ background: #1212ff;
+}
+
+#status {
+ font-size: 14px;
+ font-weight: 500;
+ text-align: center;
+}
+
+.success {
+ color: #28a745;
+ display:table;
+ margin:40px auto;
+}
+
+.error {
+ color: #dc3545;
+}
+
+/* Responsive */
+@media (max-width: 420px) {
+ #wrapper {
+ width: 100%;
+ right:0px;
+ border-radius: 0;
+ bottom:75px;
+ }
+}
\ No newline at end of file
diff --git a/src/widgets/src/widgets/loader/icon.svg b/src/widgets/src/widgets/loader/icon.svg
new file mode 100644
index 00000000..3c0fc095
--- /dev/null
+++ b/src/widgets/src/widgets/loader/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/widgets/src/widgets/loader/main.ts b/src/widgets/src/widgets/loader/main.ts
new file mode 100644
index 00000000..f0ba7815
--- /dev/null
+++ b/src/widgets/src/widgets/loader/main.ts
@@ -0,0 +1,79 @@
+import styles from './styles.css?inline'
+import { createShadowWidget } from '../../shared/shadow-dom'
+import icon from './icon.svg?raw'
+import { injectScript, installHook, getLoaded, setLoaded, STATE_LOADED, STATE_LOADING } from '../../shared/script'
+import { triggerEvent, listenEvent } from '../../shared/events'
+const widgetName = "loader";
+
+// The init event is sent from the embedding code
+listenEvent(widgetName, 'init', null, false, (args) => {
+
+ const targetWidget = args.widget || 'feedback';
+
+ const htmlContent = ``;
+
+ // Create shadow DOM widget
+ const shadowContainer = createShadowWidget(widgetName, htmlContent, styles)
+ const shadowRoot = shadowContainer.shadowRoot!;
+
+ const btn = shadowRoot.querySelector('button')!
+
+ const ariaOpen = () => {
+ btn.setAttribute('aria-label', String(args.closeLabel || 'Close widget'))
+ btn.setAttribute('aria-expanded', 'true')
+ // TODO: How could we set the aria-controls attribute too, given that we
+ // have no id for the widget? Should we ask for it via an event?
+ }
+ const ariaClose = () => {
+ btn.setAttribute('aria-label', String(args.label || 'Load widget'))
+ btn.setAttribute('aria-expanded', 'false')
+ }
+ ariaClose();
+
+ listenEvent(targetWidget, 'closed', null, false, () => {
+ btn.classList.remove('opened')
+ ariaClose();
+ })
+ listenEvent(targetWidget, 'opened', null, false, () => {
+ btn.classList.add('opened')
+ ariaOpen();
+ })
+
+ btn.addEventListener('click', () => {
+
+ if (btn.classList.contains('opened')) {
+ triggerEvent(targetWidget, 'close');
+ return;
+ }
+
+ const loadTimeout = setTimeout(() => {
+ btn.classList.remove('loading');
+ }, 10000)
+
+ // Add loading state to the UI
+ btn.classList.add('loading')
+
+ const loadedCallback = () => {
+ clearTimeout(loadTimeout)
+ btn.classList.remove('loading')
+ window._stmsg_widget.push([targetWidget, "init", args.params]);
+ }
+
+ if (getLoaded(targetWidget) === STATE_LOADED) {
+ loadedCallback();
+ } else {
+ listenEvent(targetWidget, 'loaded', null, true, loadedCallback);
+ // If it isn't even loading, we need to inject the script
+ if (!getLoaded(targetWidget)) {
+ injectScript(args.script, args.scriptType || "");
+ setLoaded(targetWidget, STATE_LOADING);
+ }
+ }
+
+ })
+
+ document.body.appendChild(shadowContainer);
+
+});
+
+installHook(widgetName);
\ No newline at end of file
diff --git a/src/widgets/src/widgets/loader/styles.css b/src/widgets/src/widgets/loader/styles.css
new file mode 100644
index 00000000..592b234b
--- /dev/null
+++ b/src/widgets/src/widgets/loader/styles.css
@@ -0,0 +1,98 @@
+/* Widget styles */
+div {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ z-index: 1000;
+}
+
+button {
+ width: 60px;
+ height: 60px;
+ border-radius: 50%;
+ background: #000091;
+ border: none;
+ cursor: pointer;
+ box-shadow: 0 4px 12px rgba(0, 0, 145, 0.3);
+ transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding:0;
+ margin:0;
+}
+
+svg {
+ width: 35px;
+ height: 35px;
+}
+
+button:focus-visible {
+ outline: 3px solid #0a76f6;
+ outline-offset: 2px;
+}
+
+button:hover {
+ background: #1212ff;
+ transform: scale(1.1);
+ box-shadow: 0 6px 16px rgba(18, 18, 255, 0.4);
+}
+
+button:active {
+ transform: scale(0.95);
+}
+
+button.loading {
+ background: #2323ff;
+}
+
+button.loading::after {
+ content: '';
+ width: 25px;
+ height: 25px;
+ border: 3px solid transparent;
+ border-top: 3px solid white;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+button.loading svg {
+ display: none;
+}
+
+button.opened svg {
+ display:none;
+}
+
+button.opened::after {
+ content: '+';
+ font-size: 50px;
+ color: white;
+ height:60px;
+ line-height: 60px;
+ font-family: Arial;
+ transform: rotate(45deg);
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+@media (max-width: 420px) {
+ button {
+ width: 40px;
+ height: 40px;
+ }
+ svg {
+ width: 25px;
+ height: 25px;
+ }
+ div {
+ right:15px;
+ bottom:15px;
+ }
+ button.opened::after {
+ font-size: 40px;
+ }
+}
\ No newline at end of file
diff --git a/src/widgets/tsconfig.json b/src/widgets/tsconfig.json
new file mode 100644
index 00000000..4f5edc24
--- /dev/null
+++ b/src/widgets/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/src/widgets/vite.config.ts b/src/widgets/vite.config.ts
new file mode 100644
index 00000000..17479f82
--- /dev/null
+++ b/src/widgets/vite.config.ts
@@ -0,0 +1,5 @@
+import { defineConfig } from 'vite'
+
+
+export default defineConfig({
+})
\ No newline at end of file