first commit

This commit is contained in:
mickael 2026-01-20 11:04:04 +03:00
commit b583aaf404
25 changed files with 13978 additions and 0 deletions

30
.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
android
.idea
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env
.env.staging
.env.production
src/

47
Dockerfile Normal file
View file

@ -0,0 +1,47 @@
# ================================
# Base
# ================================
FROM node:22 AS base
WORKDIR /app
COPY package.json package-lock.json ./
# ================================
# DEV
# ================================
FROM base AS dev
ENV NODE_ENV=development
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# ================================
# STAGING
# ================================
FROM base AS staging
ARG VITE_API_URL
ENV NODE_ENV=staging
ENV VITE_API_URL=$VITE_API_URL
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "preview"]
# ================================
# PROD
# ================================
FROM base AS builder
ARG VITE_API_URL
ENV NODE_ENV=production
ENV VITE_API_URL=$VITE_API_URL
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine AS runner
WORKDIR /usr/share/nginx/html
RUN rm -rf ./*
COPY --from=builder /app/dist .
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

17
docker-compose.dev.yml Normal file
View file

@ -0,0 +1,17 @@
services:
frontend:
container_name: dev_SAGE
build:
context: .
target: dev
args:
VITE_API_URL: "https://prod.dataven.fr/api"
env_file: .env
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
NODE_ENV: development
restart: unless-stopped

14
docker-compose.prod.yml Normal file
View file

@ -0,0 +1,14 @@
services:
frontend:
container_name: prod_SAGE
build:
context: .
target: runner
args:
VITE_API_URL: "http://51.91.76.136:8004"
env_file: .env.production
ports:
- "80:80"
environment:
NODE_ENV: production
restart: always

View file

@ -0,0 +1,14 @@
services:
frontend:
container_name: staging_SAGE
build:
context: .
target: staging
args:
VITE_API_URL: "https://uat.dataven.fr/api"
env_file: .env.staging
ports:
- "3002:3000"
environment:
NODE_ENV: staging
restart: unless-stopped

4
docker-compose.yml Normal file
View file

@ -0,0 +1,4 @@
services:
frontend:
build:
context: .

14
index.html Normal file
View file

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="generator" content="Hostinger Horizons" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dataven</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

10995
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

71
package.json Normal file
View file

@ -0,0 +1,71 @@
{
"name": "bijou-erp",
"type": "module",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite --host :: --port 3000",
"build": "vite build",
"preview": "vite preview --host :: --port 3000"
},
"dependencies": {
"@emotion/is-prop-valid": "^1.3.1",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@hello-pangea/dnd": "^16.3.0",
"@hookform/resolvers": "^3.3.2",
"@mui/material": "^7.3.6",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@reduxjs/toolkit": "^2.11.0",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.0",
"classnames": "^2.5.1",
"clsx": "^2.0.0",
"date-fns": "^2.30.0",
"framer-motion": "^10.16.4",
"html2canvas": "^1.4.1",
"js-cookie": "^3.0.5",
"jspdf": "^3.0.4",
"lucide-react": "^0.292.0",
"react": "^18.2.0",
"react-day-picker": "^8.9.1",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"react-hook-form": "^7.48.2",
"react-redux": "^9.2.0",
"react-router-dom": "^6.16.0",
"recharts": "^2.9.3",
"redux-persist": "^6.0.0",
"tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/js-cookie": "^3.0.6",
"@types/react": "^18.3.27",
"@types/react-dom": "^18.3.7",
"@types/react-helmet": "^6.1.11",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16",
"eslint": "^8.57.1",
"eslint-config-react-app": "^7.0.1",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5",
"typescript": "^5.9.3",
"vite": "^4.5.0"
}
}

View file

@ -0,0 +1,430 @@
const ALLOWED_PARENT_ORIGINS = [
'https://horizons.hostinger.com',
'https://horizons.hostinger.dev',
'https://horizons-frontend-local.hostinger.dev',
'http://localhost:4000',
];
const IMPORTANT_STYLES = [
'display',
'position',
'flex-direction',
'justify-content',
'align-items',
'width',
'height',
'padding',
'margin',
'border',
'background-color',
'color',
'font-size',
'font-weight',
'font-family',
'border-radius',
'box-shadow',
'gap',
'grid-template-columns',
];
const PRIMARY_400_COLOR = '#7B68EE';
const TEXT_CONTEXT_MAX_LENGTH = 500;
const DATA_SELECTION_MODE_ENABLED_ATTRIBUTE = 'data-selection-mode-enabled';
const MESSAGE_TYPE_ENABLE_SELECTION_MODE = 'enableSelectionMode';
const MESSAGE_TYPE_DISABLE_SELECTION_MODE = 'disableSelectionMode';
let selectionModeEnabled = false;
let currentHoverElement = null;
let overlayDiv = null;
let selectedOverlayDiv = null;
let selectedElement = null;
function injectStyles() {
if (document.getElementById('selection-mode-styles')) {
return;
}
const style = document.createElement('style');
style.id = 'selection-mode-styles';
style.textContent = `
#selection-mode-overlay {
position: absolute;
border: 2px dashed ${PRIMARY_400_COLOR};
pointer-events: none;
z-index: 999999;
}
#selection-mode-selected-overlay {
position: absolute;
border: 3px solid ${PRIMARY_400_COLOR};
pointer-events: none;
z-index: 999998;
}
`;
document.head.appendChild(style);
}
function getParentOrigin() {
if (
window.location.ancestorOrigins
&& window.location.ancestorOrigins.length > 0
) {
return window.location.ancestorOrigins[0];
}
if (document.referrer) {
try {
return new URL(document.referrer).origin;
} catch {
console.warn('[SELECTION MODE] Invalid referrer URL:', document.referrer);
}
}
return null;
}
/**
* Extract file path from React Fiber metadata (simplified - only for filePath)
* @param {*} node - DOM node
* @returns {string|null} - File path if found, null otherwise
*/
function getFilePathFromNode(node) {
const fiberKey = Object.keys(node).find(k => k.startsWith('__reactFiber'));
if (!fiberKey) {
return null;
}
const fiber = node[fiberKey];
if (!fiber) {
return null;
}
// Traverse up the fiber tree to find source metadata
let currentFiber = fiber;
while (currentFiber) {
const source = currentFiber._debugSource
|| currentFiber.memoizedProps?.__source
|| currentFiber.pendingProps?.__source;
if (source?.fileName) {
return source.fileName;
}
currentFiber = currentFiber.return;
}
return null;
}
/**
* Generate a CSS selector path to uniquely identify the element
* @param {*} element
* @returns {string} CSS selector path
*/
function getPathToElement(element) {
const path = [];
let current = element;
let depth = 0;
const maxDepth = 20; // Prevent infinite loops
while (current && current.nodeType === Node.ELEMENT_NODE && depth < maxDepth) {
let selector = current.nodeName.toLowerCase();
if (current.id) {
selector += `#${current.id}`;
path.unshift(selector);
break; // ID is unique, stop here
}
if (current.className && typeof current.className === 'string') {
const classes = current.className.trim().split(/\s+/).filter(c => c.length > 0);
if (classes.length > 0) {
selector += `.${classes.join('.')}`;
}
}
if (current.parentElement) {
const siblings = Array.from(current.parentElement.children);
const sameTypeSiblings = siblings.filter(s => s.nodeName === current.nodeName);
if (sameTypeSiblings.length > 1) {
const index = sameTypeSiblings.indexOf(current) + 1;
selector += `:nth-of-type(${index})`;
}
}
path.unshift(selector);
current = current.parentElement;
depth++;
}
return path.join(' > ');
}
function getComputedStyles(element) {
const computedStyles = window.getComputedStyle(element);
return Object.fromEntries(IMPORTANT_STYLES.map((style) => {
const styleValue = computedStyles.getPropertyValue(style)?.trim();
return styleValue && styleValue !== 'none' && styleValue !== 'normal'
? [style, styleValue]
: null;
})
.filter(Boolean));
}
function extractDOMContext(element) {
if (!element) {
return null;
}
const textContent = element.textContent?.trim();
return {
outerHTML: element.outerHTML,
selector: getPathToElement(element),
attributes: (element.attributes && element.attributes.length > 0)
? Object.fromEntries(Array.from(element.attributes).map((attr) => [attr.name, attr.value]))
: {},
computedStyles: getComputedStyles(element),
textContent: (textContent && textContent.length > 0 && textContent.length < TEXT_CONTEXT_MAX_LENGTH)
? element.textContent?.trim()
: null
};
}
function createOverlay() {
if (overlayDiv) {
return;
}
injectStyles();
overlayDiv = document.createElement('div');
overlayDiv.id = 'selection-mode-overlay';
document.body.appendChild(overlayDiv);
}
function createSelectedOverlay() {
if (selectedOverlayDiv) {
return;
}
injectStyles();
selectedOverlayDiv = document.createElement('div');
selectedOverlayDiv.id = 'selection-mode-selected-overlay';
document.body.appendChild(selectedOverlayDiv);
}
function removeOverlay() {
if (overlayDiv && overlayDiv.parentNode) {
overlayDiv.parentNode.removeChild(overlayDiv);
overlayDiv = null;
}
if (selectedOverlayDiv && selectedOverlayDiv.parentNode) {
selectedOverlayDiv.parentNode.removeChild(selectedOverlayDiv);
selectedOverlayDiv = null;
}
}
function showOverlay(element) {
if (!overlayDiv) {
createOverlay();
}
const rect = element.getBoundingClientRect();
overlayDiv.style.left = `${rect.left + window.scrollX}px`;
overlayDiv.style.top = `${rect.top + window.scrollY}px`;
overlayDiv.style.width = `${rect.width}px`;
overlayDiv.style.height = `${rect.height}px`;
overlayDiv.style.display = 'block';
}
function showSelectedOverlay(element) {
if (!selectedOverlayDiv) {
createSelectedOverlay();
}
const rect = element.getBoundingClientRect();
selectedOverlayDiv.style.left = `${rect.left + window.scrollX}px`;
selectedOverlayDiv.style.top = `${rect.top + window.scrollY}px`;
selectedOverlayDiv.style.width = `${rect.width}px`;
selectedOverlayDiv.style.height = `${rect.height}px`;
selectedOverlayDiv.style.display = 'block';
}
function hideOverlay() {
if (overlayDiv) {
overlayDiv.style.display = 'none';
}
}
function handleMouseMove(event) {
if (!selectionModeEnabled) {
return;
}
const element = document.elementFromPoint(event.clientX, event.clientY);
if (!element) {
hideOverlay();
currentHoverElement = null;
return;
}
if (element === overlayDiv || element === selectedOverlayDiv) {
return;
}
// Only update if we're hovering a different element
if (currentHoverElement !== element) {
currentHoverElement = element;
// Show outline on the element
showOverlay(element);
}
}
function handleTouchStart(event) {
if (!selectionModeEnabled) {
return;
}
const touch = event.touches[0];
if (!touch) {
return;
}
const element = document.elementFromPoint(touch.clientX, touch.clientY);
if (!element) {
currentHoverElement = null;
return;
}
if (element === overlayDiv || element === selectedOverlayDiv) {
return;
}
currentHoverElement = element;
showOverlay(element);
}
function stripFilePath(filePath) {
if (!filePath) {
return filePath;
}
const publicHtmlIndex = filePath.indexOf('public_html/');
if (publicHtmlIndex !== -1) {
return filePath.substring(publicHtmlIndex + 'public_html/'.length);
}
return filePath;
}
function handleClick(event) {
if (!selectionModeEnabled) {
return;
}
if (!currentHoverElement) {
const element = document.elementFromPoint(event.clientX, event.clientY);
if (!element || element === overlayDiv || element === selectedOverlayDiv) {
return;
}
currentHoverElement = element;
}
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
const domContext = extractDOMContext(currentHoverElement);
if (!domContext) {
return;
}
selectedElement = currentHoverElement;
if (selectedElement) {
showSelectedOverlay(selectedElement);
}
// Extract file path from React Fiber (if available)
const filePath = getFilePathFromNode(currentHoverElement);
const strippedFilePath = filePath ? stripFilePath(filePath) : undefined;
// Send domContext and filePath to parent window
const parentOrigin = getParentOrigin();
if (parentOrigin && ALLOWED_PARENT_ORIGINS.includes(parentOrigin)) {
window.parent.postMessage(
{
type: 'elementSelected',
payload: {
filePath: strippedFilePath,
domContext,
},
},
parentOrigin,
);
}
}
function handleMouseLeave() {
if (!selectionModeEnabled) {
return;
}
hideOverlay();
currentHoverElement = null;
}
function enableSelectionMode() {
if (selectionModeEnabled) {
return;
}
selectionModeEnabled = true;
document.getElementById('root')?.setAttribute(DATA_SELECTION_MODE_ENABLED_ATTRIBUTE, 'true');
document.body.style.userSelect = 'none';
createOverlay();
document.addEventListener('mousemove', handleMouseMove, true);
document.addEventListener('touchstart', handleTouchStart, true);
document.addEventListener('click', handleClick, true);
document.addEventListener('mouseleave', handleMouseLeave, true);
}
function disableSelectionMode() {
if (!selectionModeEnabled) {
return;
}
selectionModeEnabled = false;
document.getElementById('root')?.removeAttribute(DATA_SELECTION_MODE_ENABLED_ATTRIBUTE);
document.body.style.userSelect = '';
hideOverlay();
removeOverlay();
currentHoverElement = null;
selectedElement = null;
document.removeEventListener('mousemove', handleMouseMove, true);
document.removeEventListener('touchstart', handleTouchStart, true);
document.removeEventListener('click', handleClick, true);
document.removeEventListener('mouseleave', handleMouseLeave, true);
}
window.addEventListener('message', (event) => {
if (event.data?.type === MESSAGE_TYPE_ENABLE_SELECTION_MODE) {
enableSelectionMode();
}
if (event.data?.type === MESSAGE_TYPE_DISABLE_SELECTION_MODE) {
disableSelectionMode();
}
});

View file

@ -0,0 +1,27 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = resolve(__filename, '..');
export default function selectionModePlugin() {
return {
name: 'vite:selection-mode',
apply: 'serve',
transformIndexHtml() {
const scriptPath = resolve(__dirname, 'selection-mode-script.js');
const scriptContent = readFileSync(scriptPath, 'utf-8');
return [
{
tag: 'script',
attrs: { type: 'module' },
children: scriptContent,
injectTo: 'body',
},
];
},
};
}

279
plugins/utils/ast-utils.js Normal file
View file

@ -0,0 +1,279 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import generate from '@babel/generator';
import { parse } from '@babel/parser';
import traverseBabel from '@babel/traverse';
import {
isJSXIdentifier,
isJSXMemberExpression,
} from '@babel/types';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const VITE_PROJECT_ROOT = path.resolve(__dirname, '../..');
// Blacklist of components that should not be extracted (utility/non-visual components)
const COMPONENT_BLACKLIST = new Set([
'Helmet',
'HelmetProvider',
'Head',
'head',
'Meta',
'meta',
'Script',
'script',
'NoScript',
'noscript',
'Style',
'style',
'title',
'Title',
'link',
'Link',
]);
/**
* Validates that a file path is safe to access
* @param {string} filePath - Relative file path
* @returns {{ isValid: boolean, absolutePath?: string, error?: string }} - Object containing validation result
*/
export function validateFilePath(filePath) {
if (!filePath) {
return { isValid: false, error: 'Missing filePath' };
}
const absoluteFilePath = path.resolve(VITE_PROJECT_ROOT, filePath);
if (filePath.includes('..')
|| !absoluteFilePath.startsWith(VITE_PROJECT_ROOT)
|| absoluteFilePath.includes('node_modules')) {
return { isValid: false, error: 'Invalid path' };
}
if (!fs.existsSync(absoluteFilePath)) {
return { isValid: false, error: 'File not found' };
}
return { isValid: true, absolutePath: absoluteFilePath };
}
/**
* Parses a file into a Babel AST
* @param {string} absoluteFilePath - Absolute path to file
* @returns {object} Babel AST
*/
export function parseFileToAST(absoluteFilePath) {
const content = fs.readFileSync(absoluteFilePath, 'utf-8');
return parse(content, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
errorRecovery: true,
});
}
/**
* Finds a JSX opening element at a specific line and column
* @param {object} ast - Babel AST
* @param {number} line - Line number (1-indexed)
* @param {number} column - Column number (0-indexed for get-code-block, 1-indexed for apply-edit)
* @returns {object | null} Babel path to the JSX opening element
*/
export function findJSXElementAtPosition(ast, line, column) {
let targetNodePath = null;
let closestNodePath = null;
let closestDistance = Infinity;
const allNodesOnLine = [];
const visitor = {
JSXOpeningElement(path) {
const node = path.node;
if (node.loc) {
// Exact match (with tolerance for off-by-one column differences)
if (node.loc.start.line === line
&& Math.abs(node.loc.start.column - column) <= 1) {
targetNodePath = path;
path.stop();
return;
}
// Track all nodes on the same line
if (node.loc.start.line === line) {
allNodesOnLine.push({
path,
column: node.loc.start.column,
distance: Math.abs(node.loc.start.column - column),
});
}
// Track closest match on the same line for fallback
if (node.loc.start.line === line) {
const distance = Math.abs(node.loc.start.column - column);
if (distance < closestDistance) {
closestDistance = distance;
closestNodePath = path;
}
}
}
},
// Also check JSXElement nodes that contain the position
JSXElement(path) {
const node = path.node;
if (!node.loc) {
return;
}
// Check if this element spans the target line (for multi-line elements)
if (node.loc.start.line > line || node.loc.end.line < line) {
return;
}
// If we're inside this element's range, consider its opening element
if (!path.node.openingElement?.loc) {
return;
}
const openingLine = path.node.openingElement.loc.start.line;
const openingCol = path.node.openingElement.loc.start.column;
// Prefer elements that start on the exact line
if (openingLine === line) {
const distance = Math.abs(openingCol - column);
if (distance < closestDistance) {
closestDistance = distance;
closestNodePath = path.get('openingElement');
}
return;
}
// Handle elements that start before the target line
if (openingLine < line) {
const distance = (line - openingLine) * 100; // Penalize by line distance
if (distance < closestDistance) {
closestDistance = distance;
closestNodePath = path.get('openingElement');
}
}
},
};
traverseBabel.default(ast, visitor);
// Return exact match if found, otherwise return closest match if within reasonable distance
// Use larger threshold (50 chars) for same-line elements, 5 lines for multi-line elements
const threshold = closestDistance < 100 ? 50 : 500;
return targetNodePath || (closestDistance <= threshold ? closestNodePath : null);
}
/**
* Checks if a JSX element name is blacklisted
* @param {object} jsxOpeningElement - Babel JSX opening element node
* @returns {boolean} True if blacklisted
*/
function isBlacklistedComponent(jsxOpeningElement) {
if (!jsxOpeningElement || !jsxOpeningElement.name) {
return false;
}
// Handle JSXIdentifier (e.g., <Helmet>)
if (isJSXIdentifier(jsxOpeningElement.name)) {
return COMPONENT_BLACKLIST.has(jsxOpeningElement.name.name);
}
// Handle JSXMemberExpression (e.g., <React.Fragment>)
if (isJSXMemberExpression(jsxOpeningElement.name)) {
let current = jsxOpeningElement.name;
while (isJSXMemberExpression(current)) {
current = current.property;
}
if (isJSXIdentifier(current)) {
return COMPONENT_BLACKLIST.has(current.name);
}
}
return false;
}
/**
* Generates code from an AST node
* @param {object} node - Babel AST node
* @param {object} options - Generator options
* @returns {string} Generated code
*/
export function generateCode(node, options = {}) {
const generateFunction = generate.default || generate;
const output = generateFunction(node, options);
return output.code;
}
/**
* Generates a full source file from AST with source maps
* @param {object} ast - Babel AST
* @param {string} sourceFileName - Source file name for source map
* @param {string} originalCode - Original source code
* @returns {{code: string, map: object}} - Object containing generated code and source map
*/
export function generateSourceWithMap(ast, sourceFileName, originalCode) {
const generateFunction = generate.default || generate;
return generateFunction(ast, {
sourceMaps: true,
sourceFileName,
}, originalCode);
}
/**
* Extracts code blocks from a JSX element at a specific location
* @param {string} filePath - Relative file path
* @param {number} line - Line number
* @param {number} column - Column number
* @param {object} [domContext] - Optional DOM context to return on failure
* @returns {{success: boolean, filePath?: string, specificLine?: string, error?: string, domContext?: object}} - Object with metadata for LLM
*/
export function extractCodeBlocks(filePath, line, column, domContext) {
try {
// Validate file path
const validation = validateFilePath(filePath);
if (!validation.isValid) {
return { success: false, error: validation.error, domContext };
}
// Parse AST
const ast = parseFileToAST(validation.absolutePath);
// Find target node
const targetNodePath = findJSXElementAtPosition(ast, line, column);
if (!targetNodePath) {
return { success: false, error: 'Target node not found at specified line/column', domContext };
}
// Check if the target node is a blacklisted component
const isBlacklisted = isBlacklistedComponent(targetNodePath.node);
if (isBlacklisted) {
return {
success: true,
filePath,
specificLine: '',
};
}
// Get specific line code
const specificLine = generateCode(targetNodePath.parentPath?.node || targetNodePath.node);
return {
success: true,
filePath,
specificLine,
};
} catch (error) {
console.error('[ast-utils] Error extracting code blocks:', error);
return { success: false, error: 'Failed to extract code blocks', domContext };
}
}
/**
* Project root path
*/
export { VITE_PROJECT_ROOT };

View file

@ -0,0 +1,356 @@
import { POPUP_STYLES } from "./plugins/visual-editor/visual-editor-config.js";
const PLUGIN_APPLY_EDIT_API_URL = "/api/apply-edit";
const ALLOWED_PARENT_ORIGINS = [
"https://horizons.hostinger.com",
"https://horizons.hostinger.dev",
"https://horizons-frontend-local.hostinger.dev",
"http://localhost:4000",
];
let disabledTooltipElement = null;
let currentDisabledHoverElement = null;
let translations = {
disabledTooltipText: "This text can be changed only through chat.",
disabledTooltipTextImage: "This image can only be changed through chat.",
};
let areStylesInjected = false;
let globalEventHandlers = null;
let currentEditingInfo = null;
function injectPopupStyles() {
if (areStylesInjected) return;
const styleElement = document.createElement("style");
styleElement.id = "inline-editor-styles";
styleElement.textContent = POPUP_STYLES;
document.head.appendChild(styleElement);
areStylesInjected = true;
}
function findEditableElementAtPoint(event) {
let editableElement = event.target.closest("[data-edit-id]");
if (editableElement) {
return editableElement;
}
const elementsAtPoint = document.elementsFromPoint(
event.clientX,
event.clientY
);
const found = elementsAtPoint.find(
(el) => el !== event.target && el.hasAttribute("data-edit-id")
);
if (found) return found;
return null;
}
function findDisabledElementAtPoint(event) {
const direct = event.target.closest("[data-edit-disabled]");
if (direct) return direct;
const elementsAtPoint = document.elementsFromPoint(
event.clientX,
event.clientY
);
const found = elementsAtPoint.find(
(el) => el !== event.target && el.hasAttribute("data-edit-disabled")
);
if (found) return found;
return null;
}
function showPopup(targetElement, editId, currentContent, isImage = false) {
currentEditingInfo = { editId, targetElement };
const parentOrigin = getParentOrigin();
if (parentOrigin && ALLOWED_PARENT_ORIGINS.includes(parentOrigin)) {
const eventType = isImage ? "imageEditEnter" : "editEnter";
window.parent.postMessage(
{
type: eventType,
payload: { currentText: currentContent },
},
parentOrigin
);
}
}
function handleGlobalEvent(event) {
if (
!document.getElementById("root")?.getAttribute("data-edit-mode-enabled")
) {
return;
}
// Don't handle if selection mode is active
if (document.getElementById("root")?.getAttribute("data-selection-mode-enabled") === "true") {
return;
}
if (event.target.closest("#inline-editor-popup")) {
return;
}
const editableElement = findEditableElementAtPoint(event);
if (editableElement) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
if (event.type === "click") {
const editId = editableElement.getAttribute("data-edit-id");
if (!editId) {
console.warn("[INLINE EDITOR] Clicked element missing data-edit-id");
return;
}
const isImage = editableElement.tagName.toLowerCase() === "img";
let currentContent = "";
if (isImage) {
currentContent = editableElement.getAttribute("src") || "";
} else {
currentContent = editableElement.textContent || "";
}
showPopup(editableElement, editId, currentContent, isImage);
}
} else {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
}
}
function getParentOrigin() {
if (
window.location.ancestorOrigins &&
window.location.ancestorOrigins.length > 0
) {
return window.location.ancestorOrigins[0];
}
if (document.referrer) {
try {
return new URL(document.referrer).origin;
} catch (e) {
console.warn("Invalid referrer URL:", document.referrer);
}
}
return null;
}
async function handleEditSave(updatedText) {
const newText = updatedText
// Replacing characters that cause Babel parser to crash
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/{/g, "&#123;")
.replace(/}/g, "&#125;");
const { editId } = currentEditingInfo;
try {
const response = await fetch(PLUGIN_APPLY_EDIT_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
editId: editId,
newFullText: newText,
}),
});
const result = await response.json();
if (result.success) {
const parentOrigin = getParentOrigin();
if (parentOrigin && ALLOWED_PARENT_ORIGINS.includes(parentOrigin)) {
window.parent.postMessage(
{
type: "editApplied",
payload: {
editId: editId,
fileContent: result.newFileContent,
beforeCode: result.beforeCode,
afterCode: result.afterCode,
},
},
parentOrigin
);
} else {
console.error("Unauthorized parent origin:", parentOrigin);
}
} else {
console.error(
`[vite][visual-editor] Error saving changes: ${result.error}`
);
}
} catch (error) {
console.error(
`[vite][visual-editor] Error during fetch for ${editId}:`,
error
);
}
}
function createDisabledTooltip() {
if (disabledTooltipElement) return;
disabledTooltipElement = document.createElement("div");
disabledTooltipElement.id = "inline-editor-disabled-tooltip";
document.body.appendChild(disabledTooltipElement);
}
function showDisabledTooltip(targetElement, isImage = false) {
if (!disabledTooltipElement) createDisabledTooltip();
disabledTooltipElement.textContent = isImage
? translations.disabledTooltipTextImage
: translations.disabledTooltipText;
if (!disabledTooltipElement.isConnected) {
document.body.appendChild(disabledTooltipElement);
}
disabledTooltipElement.classList.add("tooltip-active");
const tooltipWidth = disabledTooltipElement.offsetWidth;
const tooltipHeight = disabledTooltipElement.offsetHeight;
const rect = targetElement.getBoundingClientRect();
// Ensures that tooltip is not off the screen with 5px margin
let newLeft = rect.left + window.scrollX + rect.width / 2 - tooltipWidth / 2;
let newTop = rect.bottom + window.scrollY + 5;
if (newLeft < window.scrollX) {
newLeft = window.scrollX + 5;
}
if (newLeft + tooltipWidth > window.innerWidth + window.scrollX) {
newLeft = window.innerWidth + window.scrollX - tooltipWidth - 5;
}
if (newTop + tooltipHeight > window.innerHeight + window.scrollY) {
newTop = rect.top + window.scrollY - tooltipHeight - 5;
}
if (newTop < window.scrollY) {
newTop = window.scrollY + 5;
}
disabledTooltipElement.style.left = `${newLeft}px`;
disabledTooltipElement.style.top = `${newTop}px`;
}
function hideDisabledTooltip() {
if (disabledTooltipElement) {
disabledTooltipElement.classList.remove("tooltip-active");
}
}
function handleDisabledElementHover(event) {
const isImage = event.currentTarget.tagName.toLowerCase() === "img";
showDisabledTooltip(event.currentTarget, isImage);
}
function handleDisabledElementLeave() {
hideDisabledTooltip();
}
function handleDisabledGlobalHover(event) {
const disabledElement = findDisabledElementAtPoint(event);
if (disabledElement) {
if (currentDisabledHoverElement !== disabledElement) {
currentDisabledHoverElement = disabledElement;
const isImage = disabledElement.tagName.toLowerCase() === "img";
showDisabledTooltip(disabledElement, isImage);
}
} else {
if (currentDisabledHoverElement) {
currentDisabledHoverElement = null;
hideDisabledTooltip();
}
}
}
function enableEditMode() {
// Don't enable if selection mode is active
if (document.getElementById("root")?.getAttribute("data-selection-mode-enabled") === "true") {
console.warn("[EDIT MODE] Cannot enable edit mode while selection mode is active");
return;
}
document
.getElementById("root")
?.setAttribute("data-edit-mode-enabled", "true");
injectPopupStyles();
if (!globalEventHandlers) {
globalEventHandlers = {
mousedown: handleGlobalEvent,
pointerdown: handleGlobalEvent,
click: handleGlobalEvent,
};
Object.entries(globalEventHandlers).forEach(([eventType, handler]) => {
document.addEventListener(eventType, handler, true);
});
}
document.addEventListener("mousemove", handleDisabledGlobalHover, true);
document.querySelectorAll("[data-edit-disabled]").forEach((el) => {
el.removeEventListener("mouseenter", handleDisabledElementHover);
el.addEventListener("mouseenter", handleDisabledElementHover);
el.removeEventListener("mouseleave", handleDisabledElementLeave);
el.addEventListener("mouseleave", handleDisabledElementLeave);
});
}
function disableEditMode() {
document.getElementById("root")?.removeAttribute("data-edit-mode-enabled");
hideDisabledTooltip();
if (globalEventHandlers) {
Object.entries(globalEventHandlers).forEach(([eventType, handler]) => {
document.removeEventListener(eventType, handler, true);
});
globalEventHandlers = null;
}
document.removeEventListener("mousemove", handleDisabledGlobalHover, true);
currentDisabledHoverElement = null;
document.querySelectorAll("[data-edit-disabled]").forEach((el) => {
el.removeEventListener("mouseenter", handleDisabledElementHover);
el.removeEventListener("mouseleave", handleDisabledElementLeave);
});
}
window.addEventListener("message", function (event) {
if (event.data?.type === "edit-save") {
handleEditSave(event.data?.payload?.newText);
}
if (event.data?.type === "enable-edit-mode") {
if (event.data?.translations) {
translations = { ...translations, ...event.data.translations };
}
enableEditMode();
}
if (event.data?.type === "disable-edit-mode") {
disableEditMode();
}
});

View file

@ -0,0 +1,137 @@
export const POPUP_STYLES = `
#inline-editor-popup {
width: 360px;
position: fixed;
z-index: 10000;
background: #161718;
color: white;
border: 1px solid #4a5568;
border-radius: 16px;
padding: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
flex-direction: column;
gap: 10px;
display: none;
}
@media (max-width: 768px) {
#inline-editor-popup {
width: calc(100% - 20px);
}
}
#inline-editor-popup.is-active {
display: flex;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#inline-editor-popup.is-disabled-view {
padding: 10px 15px;
}
#inline-editor-popup textarea {
height: 100px;
padding: 4px 8px;
background: transparent;
color: white;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.42;
resize: none;
outline: none;
}
#inline-editor-popup .button-container {
display: flex;
justify-content: flex-end;
gap: 10px;
}
#inline-editor-popup .popup-button {
border: none;
padding: 6px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 0.75rem;
font-weight: 700;
height: 34px;
outline: none;
}
#inline-editor-popup .save-button {
background: #673de6;
color: white;
}
#inline-editor-popup .cancel-button {
background: transparent;
border: 1px solid #3b3d4a;
color: white;
&:hover {
background:#474958;
}
}
`;
export function getPopupHTMLTemplate(saveLabel, cancelLabel) {
return `
<textarea></textarea>
<div class="button-container">
<button class="popup-button cancel-button">${cancelLabel}</button>
<button class="popup-button save-button">${saveLabel}</button>
</div>
`;
}
export const EDIT_MODE_STYLES = `
#root[data-edit-mode-enabled="true"] [data-edit-id] {
cursor: pointer;
outline: 2px dashed #357DF9;
outline-offset: 2px;
min-height: 1em;
}
#root[data-edit-mode-enabled="true"] img[data-edit-id] {
outline-offset: -2px;
}
#root[data-edit-mode-enabled="true"] {
cursor: pointer;
}
#root[data-edit-mode-enabled="true"] [data-edit-id]:hover {
background-color: #357DF933;
outline-color: #357DF9;
}
@keyframes fadeInTooltip {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
#inline-editor-disabled-tooltip {
display: none;
opacity: 0;
position: absolute;
background-color: #1D1E20;
color: white;
padding: 4px 8px;
border-radius: 8px;
z-index: 10001;
font-size: 14px;
border: 1px solid #3B3D4A;
max-width: 184px;
text-align: center;
}
#inline-editor-disabled-tooltip.tooltip-active {
display: block;
animation: fadeInTooltip 0.2s ease-out forwards;
}
`;

View file

@ -0,0 +1,32 @@
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { fileURLToPath } from 'url';
import { EDIT_MODE_STYLES } from './visual-editor-config';
const __filename = fileURLToPath(import.meta.url);
const __dirname = resolve(__filename, '..');
export default function inlineEditDevPlugin() {
return {
name: 'vite:inline-edit-dev',
apply: 'serve',
transformIndexHtml() {
const scriptPath = resolve(__dirname, 'edit-mode-script.js');
const scriptContent = readFileSync(scriptPath, 'utf-8');
return [
{
tag: 'script',
attrs: { type: 'module' },
children: scriptContent,
injectTo: 'body'
},
{
tag: 'style',
children: EDIT_MODE_STYLES,
injectTo: 'head'
}
];
}
};
}

View file

@ -0,0 +1,365 @@
import path from 'path';
import { parse } from '@babel/parser';
import traverseBabel from '@babel/traverse';
import * as t from '@babel/types';
import fs from 'fs';
import {
validateFilePath,
parseFileToAST,
findJSXElementAtPosition,
generateCode,
generateSourceWithMap,
VITE_PROJECT_ROOT
} from '../utils/ast-utils.js';
const EDITABLE_HTML_TAGS = ["a", "Button", "button", "p", "span", "h1", "h2", "h3", "h4", "h5", "h6", "label", "Label", "img"];
function parseEditId(editId) {
const parts = editId.split(':');
if (parts.length < 3) {
return null;
}
const column = parseInt(parts.at(-1), 10);
const line = parseInt(parts.at(-2), 10);
const filePath = parts.slice(0, -2).join(':');
if (!filePath || isNaN(line) || isNaN(column)) {
return null;
}
return { filePath, line, column };
}
function checkTagNameEditable(openingElementNode, editableTagsList) {
if (!openingElementNode || !openingElementNode.name) return false;
const nameNode = openingElementNode.name;
// Check 1: Direct name (for <p>, <Button>)
if (nameNode.type === 'JSXIdentifier' && editableTagsList.includes(nameNode.name)) {
return true;
}
// Check 2: Property name of a member expression (for <motion.h1>, check if "h1" is in editableTagsList)
if (nameNode.type === 'JSXMemberExpression' && nameNode.property && nameNode.property.type === 'JSXIdentifier' && editableTagsList.includes(nameNode.property.name)) {
return true;
}
return false;
}
function validateImageSrc(openingNode) {
if (!openingNode || !openingNode.name || openingNode.name.name !== 'img') {
return { isValid: true, reason: null }; // Not an image, skip validation
}
const hasPropsSpread = openingNode.attributes.some(attr =>
t.isJSXSpreadAttribute(attr) &&
attr.argument &&
t.isIdentifier(attr.argument) &&
attr.argument.name === 'props'
);
if (hasPropsSpread) {
return { isValid: false, reason: 'props-spread' };
}
const srcAttr = openingNode.attributes.find(attr =>
t.isJSXAttribute(attr) &&
attr.name &&
attr.name.name === 'src'
);
if (!srcAttr) {
return { isValid: false, reason: 'missing-src' };
}
if (!t.isStringLiteral(srcAttr.value)) {
return { isValid: false, reason: 'dynamic-src' };
}
if (!srcAttr.value.value || srcAttr.value.value.trim() === '') {
return { isValid: false, reason: 'empty-src' };
}
return { isValid: true, reason: null };
}
export default function inlineEditPlugin() {
return {
name: 'vite-inline-edit-plugin',
enforce: 'pre',
transform(code, id) {
if (!/\.(jsx|tsx)$/.test(id) || !id.startsWith(VITE_PROJECT_ROOT) || id.includes('node_modules')) {
return null;
}
const relativeFilePath = path.relative(VITE_PROJECT_ROOT, id);
const webRelativeFilePath = relativeFilePath.split(path.sep).join('/');
try {
const babelAst = parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
errorRecovery: true
});
let attributesAdded = 0;
traverseBabel.default(babelAst, {
enter(path) {
if (path.isJSXOpeningElement()) {
const openingNode = path.node;
const elementNode = path.parentPath.node; // The JSXElement itself
if (!openingNode.loc) {
return;
}
const alreadyHasId = openingNode.attributes.some(
(attr) => t.isJSXAttribute(attr) && attr.name.name === 'data-edit-id'
);
if (alreadyHasId) {
return;
}
// Condition 1: Is the current element tag type editable?
const isCurrentElementEditable = checkTagNameEditable(openingNode, EDITABLE_HTML_TAGS);
if (!isCurrentElementEditable) {
return;
}
const imageValidation = validateImageSrc(openingNode);
if (!imageValidation.isValid) {
const disabledAttribute = t.jsxAttribute(
t.jsxIdentifier('data-edit-disabled'),
t.stringLiteral('true')
);
openingNode.attributes.push(disabledAttribute);
attributesAdded++;
return;
}
let shouldBeDisabledDueToChildren = false;
// Condition 2: Does the element have dynamic or editable children
if (t.isJSXElement(elementNode) && elementNode.children) {
// Check if element has {...props} spread attribute - disable editing if it does
const hasPropsSpread = openingNode.attributes.some(attr => t.isJSXSpreadAttribute(attr)
&& attr.argument
&& t.isIdentifier(attr.argument)
&& attr.argument.name === 'props'
);
const hasDynamicChild = elementNode.children.some(child =>
t.isJSXExpressionContainer(child)
);
if (hasDynamicChild || hasPropsSpread) {
shouldBeDisabledDueToChildren = true;
}
}
if (!shouldBeDisabledDueToChildren && t.isJSXElement(elementNode) && elementNode.children) {
const hasEditableJsxChild = elementNode.children.some(child => {
if (t.isJSXElement(child)) {
return checkTagNameEditable(child.openingElement, EDITABLE_HTML_TAGS);
}
return false;
});
if (hasEditableJsxChild) {
shouldBeDisabledDueToChildren = true;
}
}
if (shouldBeDisabledDueToChildren) {
const disabledAttribute = t.jsxAttribute(
t.jsxIdentifier('data-edit-disabled'),
t.stringLiteral('true')
);
openingNode.attributes.push(disabledAttribute);
attributesAdded++;
return;
}
// Condition 3: Parent is non-editable if AT LEAST ONE child JSXElement is a non-editable type.
if (t.isJSXElement(elementNode) && elementNode.children && elementNode.children.length > 0) {
let hasNonEditableJsxChild = false;
for (const child of elementNode.children) {
if (t.isJSXElement(child)) {
if (!checkTagNameEditable(child.openingElement, EDITABLE_HTML_TAGS)) {
hasNonEditableJsxChild = true;
break;
}
}
}
if (hasNonEditableJsxChild) {
const disabledAttribute = t.jsxAttribute(
t.jsxIdentifier('data-edit-disabled'),
t.stringLiteral("true")
);
openingNode.attributes.push(disabledAttribute);
attributesAdded++;
return;
}
}
// Condition 4: Is any ancestor JSXElement also editable?
let currentAncestorCandidatePath = path.parentPath.parentPath;
while (currentAncestorCandidatePath) {
const ancestorJsxElementPath = currentAncestorCandidatePath.isJSXElement()
? currentAncestorCandidatePath
: currentAncestorCandidatePath.findParent(p => p.isJSXElement());
if (!ancestorJsxElementPath) {
break;
}
if (checkTagNameEditable(ancestorJsxElementPath.node.openingElement, EDITABLE_HTML_TAGS)) {
return;
}
currentAncestorCandidatePath = ancestorJsxElementPath.parentPath;
}
const line = openingNode.loc.start.line;
const column = openingNode.loc.start.column + 1;
const editId = `${webRelativeFilePath}:${line}:${column}`;
const idAttribute = t.jsxAttribute(
t.jsxIdentifier('data-edit-id'),
t.stringLiteral(editId)
);
openingNode.attributes.push(idAttribute);
attributesAdded++;
}
}
});
if (attributesAdded > 0) {
const output = generateSourceWithMap(babelAst, webRelativeFilePath, code);
return { code: output.code, map: output.map };
}
return null;
} catch (error) {
console.error(`[vite][visual-editor] Error transforming ${id}:`, error);
return null;
}
},
// Updates source code based on the changes received from the client
configureServer(server) {
server.middlewares.use('/api/apply-edit', async (req, res, next) => {
if (req.method !== 'POST') return next();
let body = '';
req.on('data', chunk => { body += chunk.toString(); });
req.on('end', async () => {
let absoluteFilePath = '';
try {
const { editId, newFullText } = JSON.parse(body);
if (!editId || typeof newFullText === 'undefined') {
res.writeHead(400, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Missing editId or newFullText' }));
}
const parsedId = parseEditId(editId);
if (!parsedId) {
res.writeHead(400, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Invalid editId format (filePath:line:column)' }));
}
const { filePath, line, column } = parsedId;
// Validate file path
const validation = validateFilePath(filePath);
if (!validation.isValid) {
res.writeHead(400, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: validation.error }));
}
absoluteFilePath = validation.absolutePath;
// Parse AST
const originalContent = fs.readFileSync(absoluteFilePath, 'utf-8');
const babelAst = parseFileToAST(absoluteFilePath);
// Find target node (note: apply-edit uses column+1)
const targetNodePath = findJSXElementAtPosition(babelAst, line, column + 1);
if (!targetNodePath) {
res.writeHead(404, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Target node not found by line/column', editId }));
}
const targetOpeningElement = targetNodePath.node;
const parentElementNode = targetNodePath.parentPath?.node;
const isImageElement = targetOpeningElement.name && targetOpeningElement.name.name === 'img';
let beforeCode = '';
let afterCode = '';
let modified = false;
if (isImageElement) {
// Handle image src attribute update
beforeCode = generateCode(targetOpeningElement);
const srcAttr = targetOpeningElement.attributes.find(attr =>
t.isJSXAttribute(attr) && attr.name && attr.name.name === 'src'
);
if (srcAttr && t.isStringLiteral(srcAttr.value)) {
srcAttr.value = t.stringLiteral(newFullText);
modified = true;
afterCode = generateCode(targetOpeningElement);
}
} else {
if (parentElementNode && t.isJSXElement(parentElementNode)) {
beforeCode = generateCode(parentElementNode);
parentElementNode.children = [];
if (newFullText && newFullText.trim() !== '') {
const newTextNode = t.jsxText(newFullText);
parentElementNode.children.push(newTextNode);
}
modified = true;
afterCode = generateCode(parentElementNode);
}
}
if (!modified) {
res.writeHead(409, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Could not apply changes to AST.' }));
}
const webRelativeFilePath = path.relative(VITE_PROJECT_ROOT, absoluteFilePath).split(path.sep).join('/');
const output = generateSourceWithMap(babelAst, webRelativeFilePath, originalContent);
const newContent = output.code;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
newFileContent: newContent,
beforeCode,
afterCode,
}));
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Internal server error during edit application.' }));
}
});
});
}
};
}

View file

@ -0,0 +1,125 @@
export default function iframeRouteRestorationPlugin() {
return {
name: 'vite:iframe-route-restoration',
apply: 'serve',
transformIndexHtml() {
const script = `
const ALLOWED_PARENT_ORIGINS = [
"https://horizons.hostinger.com",
"https://horizons.hostinger.dev",
"https://horizons-frontend-local.hostinger.dev",
];
// Check to see if the page is in an iframe
if (window.self !== window.top) {
const STORAGE_KEY = 'horizons-iframe-saved-route';
const getCurrentRoute = () => location.pathname + location.search + location.hash;
const save = () => {
try {
const currentRoute = getCurrentRoute();
sessionStorage.setItem(STORAGE_KEY, currentRoute);
window.parent.postMessage({message: 'route-changed', route: currentRoute}, '*');
} catch {}
};
const replaceHistoryState = (url) => {
try {
history.replaceState(null, '', url);
window.dispatchEvent(new PopStateEvent('popstate', { state: history.state }));
return true;
} catch {}
return false;
};
const restore = () => {
try {
const saved = sessionStorage.getItem(STORAGE_KEY);
if (!saved) return;
if (!saved.startsWith('/')) {
sessionStorage.removeItem(STORAGE_KEY);
return;
}
const current = getCurrentRoute();
if (current !== saved) {
if (!replaceHistoryState(saved)) {
replaceHistoryState('/');
}
requestAnimationFrame(() => setTimeout(() => {
try {
const text = (document.body?.innerText || '').trim();
// If the restored route results in too little content, assume it is invalid and navigate home
if (text.length < 50) {
replaceHistoryState('/');
}
} catch {}
}, 1000));
}
} catch {}
};
const originalPushState = history.pushState;
history.pushState = function(...args) {
originalPushState.apply(this, args);
save();
};
const originalReplaceState = history.replaceState;
history.replaceState = function(...args) {
originalReplaceState.apply(this, args);
save();
};
const getParentOrigin = () => {
if (
window.location.ancestorOrigins &&
window.location.ancestorOrigins.length > 0
) {
return window.location.ancestorOrigins[0];
}
if (document.referrer) {
try {
return new URL(document.referrer).origin;
} catch (e) {
console.warn("Invalid referrer URL:", document.referrer);
}
}
return null;
};
window.addEventListener('popstate', save);
window.addEventListener('hashchange', save);
window.addEventListener("message", function (event) {
const parentOrigin = getParentOrigin();
if (event.data?.type === "redirect-home" && parentOrigin && ALLOWED_PARENT_ORIGINS.includes(parentOrigin)) {
const saved = sessionStorage.getItem(STORAGE_KEY);
if(saved && saved !== '/') {
replaceHistoryState('/')
}
}
});
restore();
}
`;
return [
{
tag: 'script',
attrs: { type: 'module' },
children: script,
injectTo: 'head'
}
];
}
};
}

6
postcss.config.js Normal file
View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

19
public/.htaccess Normal file
View file

@ -0,0 +1,19 @@
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.html [L]
</IfModule>
<IfModule mod_headers.c>
Header set X-Powered-By "Hostinger Horizons"
# Cache everything on CDN by default
Header set Cache-Control "public, s-maxage=604800, max-age=0"
# Cache in browser all assets
<If "%{REQUEST_URI} =~ m#^/assets/.*$#">
Header set Cache-Control "public, max-age=604800"
</If>
</IfModule>

1
public/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 135 KiB

3
rest.http Normal file
View file

@ -0,0 +1,3 @@
### Get embed artifact
GET https://api.sage-ai-studio.webexpr.dev/api/embed/artifact/968ba24a-47d4-4ac4-b838-f1f7a60c310d?mode=light&theme=dataven
Authorization: Bearer sk_live_bef70d4dd6bc671fd815d21104d5dce8

86
tailwind.config.js Normal file
View file

@ -0,0 +1,86 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ['class'],
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
'./app/**/*.{js,ts,jsx,tsx}',
'./src/**/*.{js,ts,jsx,tsx}',
],
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
extend: {
colors: {
redDataven: '#C94635',
redDarkDataven: '#941403',
grayDataven: '#6f6f6f',
blackDataven: '#000000',
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
keyframes: {
'accordion-down': {
from: { height: 0 },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: 0 },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
fontFamily: {
'headline': ['Sage Headline', 'sans-serif'],
'sage': ['Sage Text', 'sans-serif'],
'ui': ['Sage UI', 'sans-serif'],
},
},
},
plugins: [require('tailwindcss-animate')],
};

181
tools/generate-llms.js Normal file
View file

@ -0,0 +1,181 @@
#!/usr/bin/env node
import fs from 'fs';
import path from 'path';
const CLEAN_CONTENT_REGEX = {
comments: /\/\*[\s\S]*?\*\/|\/\/.*$/gm,
templateLiterals: /`[\s\S]*?`/g,
strings: /'[^']*'|"[^"]*"/g,
jsxExpressions: /\{.*?\}/g,
htmlEntities: {
quot: /&quot;/g,
amp: /&amp;/g,
lt: /&lt;/g,
gt: /&gt;/g,
apos: /&apos;/g
}
};
const EXTRACTION_REGEX = {
route: /<Route\s+[^>]*>/g,
path: /path=["']([^"']+)["']/,
element: /element=\{<(\w+)[^}]*\/?\s*>\}/,
helmet: /<Helmet[^>]*?>([\s\S]*?)<\/Helmet>/i,
helmetTest: /<Helmet[\s\S]*?<\/Helmet>/i,
title: /<title[^>]*?>\s*(.*?)\s*<\/title>/i,
description: /<meta\s+name=["']description["']\s+content=["'](.*?)["']/i
};
function cleanContent(content) {
return content
.replace(CLEAN_CONTENT_REGEX.comments, '')
.replace(CLEAN_CONTENT_REGEX.templateLiterals, '""')
.replace(CLEAN_CONTENT_REGEX.strings, '""');
}
function cleanText(text) {
if (!text) return text;
return text
.replace(CLEAN_CONTENT_REGEX.jsxExpressions, '')
.replace(CLEAN_CONTENT_REGEX.htmlEntities.quot, '"')
.replace(CLEAN_CONTENT_REGEX.htmlEntities.amp, '&')
.replace(CLEAN_CONTENT_REGEX.htmlEntities.lt, '<')
.replace(CLEAN_CONTENT_REGEX.htmlEntities.gt, '>')
.replace(CLEAN_CONTENT_REGEX.htmlEntities.apos, "'")
.trim();
}
function extractRoutes(appJsxPath) {
if (!fs.existsSync(appJsxPath)) return new Map();
try {
const content = fs.readFileSync(appJsxPath, 'utf8');
const routes = new Map();
const routeMatches = [...content.matchAll(EXTRACTION_REGEX.route)];
for (const match of routeMatches) {
const routeTag = match[0];
const pathMatch = routeTag.match(EXTRACTION_REGEX.path);
const elementMatch = routeTag.match(EXTRACTION_REGEX.element);
const isIndex = routeTag.includes('index');
if (elementMatch) {
const componentName = elementMatch[1];
let routePath;
if (isIndex) {
routePath = '/';
} else if (pathMatch) {
routePath = pathMatch[1].startsWith('/') ? pathMatch[1] : `/${pathMatch[1]}`;
}
routes.set(componentName, routePath);
}
}
return routes;
} catch (error) {
return new Map();
}
}
function findReactFiles(dir) {
return fs.readdirSync(dir).map(item => path.join(dir, item));
}
function extractHelmetData(content, filePath, routes) {
const cleanedContent = cleanContent(content);
if (!EXTRACTION_REGEX.helmetTest.test(cleanedContent)) {
return null;
}
const helmetMatch = content.match(EXTRACTION_REGEX.helmet);
if (!helmetMatch) return null;
const helmetContent = helmetMatch[1];
const titleMatch = helmetContent.match(EXTRACTION_REGEX.title);
const descMatch = helmetContent.match(EXTRACTION_REGEX.description);
const title = cleanText(titleMatch?.[1]);
const description = cleanText(descMatch?.[1]);
const fileName = path.basename(filePath, path.extname(filePath));
const url = routes.length && routes.has(fileName)
? routes.get(fileName)
: generateFallbackUrl(fileName);
return {
url,
title: title || 'Untitled Page',
description: description || 'No description available'
};
}
function generateFallbackUrl(fileName) {
const cleanName = fileName.replace(/Page$/, '').toLowerCase();
return cleanName === 'app' ? '/' : `/${cleanName}`;
}
function generateLlmsTxt(pages) {
const sortedPages = pages.sort((a, b) => a.title.localeCompare(b.title));
const pageEntries = sortedPages.map(page =>
`- [${page.title}](${page.url}): ${page.description}`
).join('\n');
return `## Pages\n${pageEntries}`;
}
function ensureDirectoryExists(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
function processPageFile(filePath, routes) {
try {
const content = fs.readFileSync(filePath, 'utf8');
return extractHelmetData(content, filePath, routes);
} catch (error) {
console.error(`❌ Error processing ${filePath}:`, error.message);
return null;
}
}
function main() {
const pagesDir = path.join(process.cwd(), 'src', 'pages');
const appJsxPath = path.join(process.cwd(), 'src', 'App.jsx');
let pages = [];
if (!fs.existsSync(pagesDir)) {
pages.push(processPageFile(appJsxPath, []));
} else {
const routes = extractRoutes(appJsxPath);
const reactFiles = findReactFiles(pagesDir);
pages = reactFiles
.map(filePath => processPageFile(filePath, routes))
.filter(Boolean);
if (pages.length === 0) {
console.error('❌ No pages with Helmet components found!');
process.exit(1);
}
}
const llmsTxtContent = generateLlmsTxt(pages);
const outputPath = path.join(process.cwd(), 'public', 'llms.txt');
ensureDirectoryExists(path.dirname(outputPath));
fs.writeFileSync(outputPath, llmsTxtContent, 'utf8');
}
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
if (isMainModule) {
main();
}

18
tsconfig.json Normal file
View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"jsx": "react-jsx",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowJs": true,
"checkJs": false,
"strict": true,
"baseUrl": "src",
"paths": {
"@/*": ["*"]
}
},
"include": ["src"]
}

707
vite.config.js Normal file
View file

@ -0,0 +1,707 @@
import path from "node:path";
import react from "@vitejs/plugin-react";
import { createLogger, defineConfig } from "vite";
import inlineEditPlugin from "./plugins/visual-editor/vite-plugin-react-inline-editor.js";
import editModeDevPlugin from "./plugins/visual-editor/vite-plugin-edit-mode.js";
import iframeRouteRestorationPlugin from "./plugins/vite-plugin-iframe-route-restoration.js";
import selectionModePlugin from "./plugins/selection-mode/vite-plugin-selection-mode.js";
const isDev = process.env.NODE_ENV !== "production";
// Configuration des origines autorisées pour postMessage
// En production, définir via variable d'environnement
const ALLOWED_PARENT_ORIGINS = isDev
? [
"http://localhost:3000",
"http://localhost:5173",
"https://prod.dataven.fr",
]
: (
process.env.VITE_ALLOWED_PARENT_ORIGINS || "https://prod.dataven.fr"
).split(",");
// Utilitaire pour postMessage sécurisé avec handshake
const configSecurePostMessage = `
(function() {
const ALLOWED_ORIGINS = ${JSON.stringify(ALLOWED_PARENT_ORIGINS)};
let verifiedParentOrigin = null;
let isEmbedded = false;
// Détecter si on est dans une iframe
try {
isEmbedded = window.self !== window.top;
} catch (e) {
isEmbedded = true; // Erreur de cross-origin = on est dans une iframe
}
// Écouter le handshake du parent
window.addEventListener('message', (event) => {
if (!ALLOWED_ORIGINS.includes(event.origin)) {
return; // Ignorer les origines non autorisées
}
if (event.data?.type === 'HORIZONS_HANDSHAKE') {
verifiedParentOrigin = event.origin;
// Confirmer le handshake
window.parent.postMessage({ type: 'HORIZONS_HANDSHAKE_ACK' }, event.origin);
}
});
// Fonction sécurisée pour envoyer des messages au parent
window.__securePostMessage = function(payload) {
if (!isEmbedded) return;
if (!verifiedParentOrigin) {
// En attente de handshake, stocker temporairement (optionnel)
console.debug('[SecurePostMessage] No verified parent origin yet');
return;
}
// Sanitiser le payload avant envoi
const sanitizedPayload = sanitizePayload(payload);
window.parent.postMessage(sanitizedPayload, verifiedParentOrigin);
};
// Sanitisation du payload
function sanitizePayload(payload) {
const sanitized = { ...payload };
${
isDev
? `
// En DEV: conserver les détails pour le debugging
return sanitized;
`
: `
// En PROD: supprimer les infos sensibles
if (sanitized.error) {
// Remplacer la stack trace par un ID de corrélation
sanitized.errorId = generateCorrelationId();
sanitized.error = sanitized.error.split('\\n')[0]; // Garder uniquement la première ligne
delete sanitized.stack;
}
// Ne pas exposer les URLs internes ou les détails de réponse
if (sanitized.url) {
sanitized.url = '[REDACTED]';
}
return sanitized;
`
}
}
function generateCorrelationId() {
return 'err_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
}
// Exposer pour vérification
window.__isSecureMessagingReady = () => !!verifiedParentOrigin;
})();
`;
// Handler d'erreurs Vite sécurisé
const configHorizonsViteErrorHandler = `
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const addedNode of mutation.addedNodes) {
if (
addedNode.nodeType === Node.ELEMENT_NODE &&
(addedNode.tagName?.toLowerCase() === 'vite-error-overlay' ||
addedNode.classList?.contains('backdrop'))
) {
handleViteOverlay(addedNode);
}
}
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
function handleViteOverlay(node) {
if (!node.shadowRoot) return;
const backdrop = node.shadowRoot.querySelector('.backdrop');
if (!backdrop) return;
const messageBodyElement = backdrop.querySelector('.message-body');
const messageText = messageBodyElement ? messageBodyElement.textContent.trim() : '';
// Utiliser la fonction sécurisée
window.__securePostMessage?.({
type: 'horizons-vite-error',
error: messageText,
timestamp: Date.now()
});
}
`;
// Handler d'erreurs runtime sécurisé
const configHorizonsRuntimeErrorHandler = `
window.onerror = (message, source, lineno, colno, errorObj) => {
${
isDev
? `
// DEV: Détails complets pour debugging
const errorDetails = errorObj ? {
name: errorObj.name,
message: errorObj.message,
stack: errorObj.stack,
source,
lineno,
colno,
} : null;
`
: `
// PROD: Informations minimales
const errorDetails = errorObj ? {
name: errorObj.name,
message: errorObj.message?.substring(0, 100), // Tronquer
} : null;
`
}
window.__securePostMessage?.({
type: 'horizons-runtime-error',
message: typeof message === 'string' ? message.substring(0, 200) : 'Unknown error',
error: errorDetails,
timestamp: Date.now()
});
};
`;
// Handler console.error sécurisé
const configHorizonsConsoleErrorHandler = `
const originalConsoleError = console.error;
console.error = function(...args) {
originalConsoleError.apply(console, args);
${
isDev
? `
// DEV: Logger les détails
let errorString = '';
for (const arg of args) {
if (arg instanceof Error) {
errorString = arg.stack || \`\${arg.name}: \${arg.message}\`;
break;
}
}
if (!errorString) {
errorString = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ');
}
`
: `
// PROD: Message générique uniquement
let errorString = 'An error occurred';
for (const arg of args) {
if (arg instanceof Error) {
errorString = arg.name + ': ' + (arg.message || '').substring(0, 50);
break;
}
}
`
}
window.__securePostMessage?.({
type: 'horizons-console-error',
error: errorString,
timestamp: Date.now()
});
};
`;
// Monkey patch fetch sécurisé
const configWindowFetchMonkeyPatch = `
const originalFetch = window.fetch;
window.fetch = function(...args) {
const url = args[0] instanceof Request ? args[0].url : args[0];
// Skip WebSocket URLs
if (url.startsWith('ws:') || url.startsWith('wss:')) {
return originalFetch.apply(this, args);
}
return originalFetch.apply(this, args)
.then(async response => {
const contentType = response.headers.get('Content-Type') || '';
const isDocumentResponse =
contentType.includes('text/html') ||
contentType.includes('application/xhtml+xml');
if (!response.ok && !isDocumentResponse) {
${
isDev
? `
// DEV: Logger le contenu de l'erreur
const responseClone = response.clone();
const errorFromRes = await responseClone.text();
console.error(\`Fetch error from \${response.url}: \${errorFromRes}\`);
`
: `
// PROD: Ne pas exposer le contenu de la réponse
console.error(\`Fetch error: \${response.status}\`);
`
}
}
return response;
})
.catch(error => {
if (!url.match(/\\.html?$/i)) {
${
isDev
? `
console.error(error);
`
: `
// PROD: Ne pas exposer l'URL ou les détails
console.error('Network request failed');
`
}
}
throw error;
});
};
`;
// Handler de navigation sécurisé
const configNavigationHandler = `
if (window.navigation && window.self !== window.top) {
window.navigation.addEventListener('navigate', (event) => {
const url = event.destination.url;
try {
const destinationUrl = new URL(url);
const currentOrigin = window.location.origin;
if (destinationUrl.origin === currentOrigin) {
return;
}
} catch (error) {
return;
}
window.__securePostMessage?.({
type: 'horizons-navigation-error',
${isDev ? `url,` : `url: '[external]',`}
timestamp: Date.now()
});
});
}
`;
const addTransformIndexHtml = {
name: "add-transform-index-html",
transformIndexHtml(html) {
const tags = [
// Le script de postMessage sécurisé doit être chargé EN PREMIER
{
tag: "script",
attrs: { type: "module" },
children: configSecurePostMessage,
injectTo: "head-prepend", // Avant tous les autres scripts
},
{
tag: "script",
attrs: { type: "module" },
children: configHorizonsRuntimeErrorHandler,
injectTo: "head",
},
{
tag: "script",
attrs: { type: "module" },
children: configHorizonsViteErrorHandler,
injectTo: "head",
},
{
tag: "script",
attrs: { type: "module" },
children: configHorizonsConsoleErrorHandler,
injectTo: "head",
},
{
tag: "script",
attrs: { type: "module" },
children: configWindowFetchMonkeyPatch,
injectTo: "head",
},
{
tag: "script",
attrs: { type: "module" },
children: configNavigationHandler,
injectTo: "head",
},
];
if (
!isDev &&
process.env.TEMPLATE_BANNER_SCRIPT_URL &&
process.env.TEMPLATE_REDIRECT_URL
) {
tags.push({
tag: "script",
attrs: {
src: process.env.TEMPLATE_BANNER_SCRIPT_URL,
"template-redirect-url": process.env.TEMPLATE_REDIRECT_URL,
},
injectTo: "head",
});
}
return { html, tags };
},
};
console.warn = () => {};
const logger = createLogger();
const loggerError = logger.error;
logger.error = (msg, options) => {
if (options?.error?.toString().includes("CssSyntaxError: [postcss]")) {
return;
}
loggerError(msg, options);
};
export default defineConfig({
customLogger: logger,
plugins: [
...(isDev
? [
inlineEditPlugin(),
editModeDevPlugin(),
iframeRouteRestorationPlugin(),
selectionModePlugin(),
]
: []),
react(),
addTransformIndexHtml,
],
server: {
cors: true,
headers: {
"Cross-Origin-Embedder-Policy": "credentialless",
// Ajouter CSP pour frame-ancestors si l'app ne doit pas être iframée
// ou limiter aux origines autorisées
...(isDev
? {}
: {
"Content-Security-Policy": `frame-ancestors ${ALLOWED_PARENT_ORIGINS.join(
" "
)}`,
"X-Frame-Options": "SAMEORIGIN", // Fallback pour anciens navigateurs
}),
},
allowedHosts: true,
hmr: { overlay: false },
watch: { usePolling: true, ignored: ["**/.git/**"] },
},
optimizeDeps: { force: true },
// esbuild: {
// loader: 'jsx',
// include: /\.js$/,
// },
resolve: {
extensions: [".jsx", ".js", ".tsx", ".ts", ".json"],
alias: { "@": path.resolve(__dirname, "./src") },
},
build: {
rollupOptions: {
external: [
"@babel/parser",
"@babel/traverse",
"@babel/generator",
"@babel/types",
],
},
},
cacheDir: "node_modules/.vite",
});
// import path from 'node:path';
// import react from '@vitejs/plugin-react';
// import { createLogger, defineConfig } from 'vite';
// import inlineEditPlugin from './plugins/visual-editor/vite-plugin-react-inline-editor.js';
// import editModeDevPlugin from './plugins/visual-editor/vite-plugin-edit-mode.js';
// import iframeRouteRestorationPlugin from './plugins/vite-plugin-iframe-route-restoration.js';
// import selectionModePlugin from './plugins/selection-mode/vite-plugin-selection-mode.js';
// const isDev = process.env.NODE_ENV !== 'production';
// const configHorizonsViteErrorHandler = `
// const observer = new MutationObserver((mutations) => {
// for (const mutation of mutations) {
// for (const addedNode of mutation.addedNodes) {
// if (
// addedNode.nodeType === Node.ELEMENT_NODE &&
// (
// addedNode.tagName?.toLowerCase() === 'vite-error-overlay' ||
// addedNode.classList?.contains('backdrop')
// )
// ) {
// handleViteOverlay(addedNode);
// }
// }
// }
// });
// observer.observe(document.documentElement, {
// childList: true,
// subtree: true
// });
// function handleViteOverlay(node) {
// if (!node.shadowRoot) {
// return;
// }
// const backdrop = node.shadowRoot.querySelector('.backdrop');
// if (backdrop) {
// const overlayHtml = backdrop.outerHTML;
// const parser = new DOMParser();
// const doc = parser.parseFromString(overlayHtml, 'text/html');
// const messageBodyElement = doc.querySelector('.message-body');
// const fileElement = doc.querySelector('.file');
// const messageText = messageBodyElement ? messageBodyElement.textContent.trim() : '';
// const fileText = fileElement ? fileElement.textContent.trim() : '';
// const error = messageText + (fileText ? ' File:' + fileText : '');
// window.parent.postMessage({
// type: 'horizons-vite-error',
// error,
// }, '*');
// }
// }
// `;
// const configHorizonsRuntimeErrorHandler = `
// window.onerror = (message, source, lineno, colno, errorObj) => {
// const errorDetails = errorObj ? JSON.stringify({
// name: errorObj.name,
// message: errorObj.message,
// stack: errorObj.stack,
// source,
// lineno,
// colno,
// }) : null;
// window.parent.postMessage({
// type: 'horizons-runtime-error',
// message,
// error: errorDetails
// }, '*');
// };
// `;
// const configHorizonsConsoleErrroHandler = `
// const originalConsoleError = console.error;
// console.error = function(...args) {
// originalConsoleError.apply(console, args);
// let errorString = '';
// for (let i = 0; i < args.length; i++) {
// const arg = args[i];
// if (arg instanceof Error) {
// errorString = arg.stack || \`\${arg.name}: \${arg.message}\`;
// break;
// }
// }
// if (!errorString) {
// errorString = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ');
// }
// window.parent.postMessage({
// type: 'horizons-console-error',
// error: errorString
// }, '*');
// };
// `;
// const configWindowFetchMonkeyPatch = `
// const originalFetch = window.fetch;
// window.fetch = function(...args) {
// const url = args[0] instanceof Request ? args[0].url : args[0];
// // Skip WebSocket URLs
// if (url.startsWith('ws:') || url.startsWith('wss:')) {
// return originalFetch.apply(this, args);
// }
// return originalFetch.apply(this, args)
// .then(async response => {
// const contentType = response.headers.get('Content-Type') || '';
// // Exclude HTML document responses
// const isDocumentResponse =
// contentType.includes('text/html') ||
// contentType.includes('application/xhtml+xml');
// if (!response.ok && !isDocumentResponse) {
// const responseClone = response.clone();
// const errorFromRes = await responseClone.text();
// const requestUrl = response.url;
// console.error(\`Fetch error from \${requestUrl}: \${errorFromRes}\`);
// }
// return response;
// })
// .catch(error => {
// if (!url.match(/\.html?$/i)) {
// console.error(error);
// }
// throw error;
// });
// };
// `;
// const configNavigationHandler = `
// if (window.navigation && window.self !== window.top) {
// window.navigation.addEventListener('navigate', (event) => {
// const url = event.destination.url;
// try {
// const destinationUrl = new URL(url);
// const destinationOrigin = destinationUrl.origin;
// const currentOrigin = window.location.origin;
// if (destinationOrigin === currentOrigin) {
// return;
// }
// } catch (error) {
// return;
// }
// window.parent.postMessage({
// type: 'horizons-navigation-error',
// url,
// }, '*');
// });
// }
// `;
// const addTransformIndexHtml = {
// name: 'add-transform-index-html',
// transformIndexHtml(html) {
// const tags = [
// {
// tag: 'script',
// attrs: { type: 'module' },
// children: configHorizonsRuntimeErrorHandler,
// injectTo: 'head',
// },
// {
// tag: 'script',
// attrs: { type: 'module' },
// children: configHorizonsViteErrorHandler,
// injectTo: 'head',
// },
// {
// tag: 'script',
// attrs: {type: 'module'},
// children: configHorizonsConsoleErrroHandler,
// injectTo: 'head',
// },
// {
// tag: 'script',
// attrs: { type: 'module' },
// children: configWindowFetchMonkeyPatch,
// injectTo: 'head',
// },
// {
// tag: 'script',
// attrs: { type: 'module' },
// children: configNavigationHandler,
// injectTo: 'head',
// },
// ];
// if (!isDev && process.env.TEMPLATE_BANNER_SCRIPT_URL && process.env.TEMPLATE_REDIRECT_URL) {
// tags.push(
// {
// tag: 'script',
// attrs: {
// src: process.env.TEMPLATE_BANNER_SCRIPT_URL,
// 'template-redirect-url': process.env.TEMPLATE_REDIRECT_URL,
// },
// injectTo: 'head',
// }
// );
// }
// return {
// html,
// tags,
// };
// },
// };
// console.warn = () => {};
// const logger = createLogger()
// const loggerError = logger.error
// logger.error = (msg, options) => {
// if (options?.error?.toString().includes('CssSyntaxError: [postcss]')) {
// return;
// }
// loggerError(msg, options);
// }
// export default defineConfig({
// customLogger: logger,
// plugins: [
// ...(isDev ? [inlineEditPlugin(), editModeDevPlugin(), iframeRouteRestorationPlugin(), selectionModePlugin()] : []),
// react(),
// addTransformIndexHtml
// ],
// server: {
// cors: true,
// headers: {
// 'Cross-Origin-Embedder-Policy': 'credentialless',
// },
// allowedHosts: true,
// // watch: {
// // ignored: ['**/.git/**']
// // },
// hmr: {
// overlay: true, // Afficher les erreurs en overlay
// },
// watch: {
// usePolling: true, // Utile sur certains systèmes
// }
// },
// optimizeDeps: {
// force: true, // Force la ré-optimisation des dépendances
// },
// resolve: {
// extensions: ['.jsx', '.js', '.tsx', '.ts', '.json', ],
// alias: {
// '@': path.resolve(__dirname, './src'),
// },
// },
// build: {
// rollupOptions: {
// external: [
// '@babel/parser',
// '@babel/traverse',
// '@babel/generator',
// '@babel/types'
// ]
// }
// },
// // optimizeDeps: {
// // exclude: ['.git']
// // },
// // Vider le cache au démarrage si nécessaire
// cacheDir: 'node_modules/.vite',
// });