Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d9a6174c9 | |||
| 7787bed2e9 | |||
| 225bd1d457 | |||
| 97fea66947 | |||
| 913df2aee8 | |||
| e49fdea8f0 | |||
| adb756272b | |||
| b01b8dc5cb | |||
| 4d7a2580af | |||
| 5959141642 | |||
| 34968c32e7 | |||
| bd788182d3 | |||
| 0e820aef48 | |||
| 78d5602ca6 | |||
| 9d526e9d88 | |||
| fdedaf77bb | |||
| a053bdbceb | |||
| b5ed4ec309 | |||
| 05a9a220d8 | |||
| be20ae4400 | |||
| 00b94c3cfa | |||
| 4b5de5df8f | |||
| 6b914f088e | |||
| 0f3990ca3d | |||
| 1f960a1bb1 | |||
| 4f9f464dab | |||
| a7fd9dbac2 | |||
| 0a0a6fdfa9 | |||
| 177458fd07 | |||
| e3b202cde6 | |||
| c4cfd63eb6 | |||
| 582fee2e44 | |||
| 16341a9889 | |||
| 96f4ca0f4e | |||
| dab02bdfb8 | |||
| 9b0cdd4dea | |||
| acab730922 | |||
| d5f1ce84b4 | |||
| 666f05bf59 | |||
| fd9af8c2ba | |||
| 41389a88b7 | |||
| d97db3d918 | |||
| dfee9a58d7 | |||
| 389b5ca6cd | |||
| 193f2cc67d | |||
| 6a1c2f7383 | |||
| 857f69b9db | |||
| 60bd17bd1b | |||
| 9085533265 | |||
| e14c59d708 | |||
| 5ad9be9582 | |||
| 7d49213fbb | |||
| 874224fdbb | |||
| 418cc06c17 | |||
| 3fcf089e92 | |||
| 285d8e71a0 | |||
| 9e7a54c716 | |||
| 4420f18eb0 | |||
| eb7de7f44d | |||
| 1a07d17230 | |||
| 5b505b04f1 | |||
| 6ed434c44d | |||
| bada31c775 | |||
| 8ca257f4dd | |||
| a5d9d2abe8 | |||
| 4139dea802 | |||
| b121cad8f2 | |||
| e793f38bd3 | |||
| bf487437a0 | |||
| 3276adb866 | |||
| 3b7ad132c8 | |||
| b699ff14a3 |
@@ -0,0 +1,41 @@
|
||||
name: CI + E2E
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: self-hosted
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Run E2E tests
|
||||
id: e2e
|
||||
run: |
|
||||
cd /home/clems/mascarade/web
|
||||
BASE_URL=http://localhost:3100 npx playwright test --reporter=line 2>&1 | tee /tmp/e2e-output.txt
|
||||
PASSED=$(grep -oP '\d+ passed' /tmp/e2e-output.txt | grep -oP '\d+' || echo 0)
|
||||
FAILED=$(grep -oP '\d+ failed' /tmp/e2e-output.txt | grep -oP '\d+' || echo 0)
|
||||
echo "passed=$PASSED" >> $GITHUB_OUTPUT
|
||||
echo "failed=$FAILED" >> $GITHUB_OUTPUT
|
||||
if [ "${FAILED:-0}" != "0" ] && [ "${FAILED:-0}" != "" ]; then exit 1; fi
|
||||
|
||||
- name: Notify ntfy on failure
|
||||
if: failure()
|
||||
run: |
|
||||
curl -s -d "E2E FAILED: ${{ steps.e2e.outputs.failed }} tests failed on $(date '+%H:%M')" \
|
||||
-H "Title: Ops ER E2E Failed" \
|
||||
-H "Priority: high" \
|
||||
-H "Tags: rotating_light" \
|
||||
https://ntfy.saillant.cc/ops-alerts 2>/dev/null || true
|
||||
|
||||
- name: Notify ntfy on success
|
||||
if: success()
|
||||
run: |
|
||||
curl -s -d "E2E OK: ${{ steps.e2e.outputs.passed }} passed" \
|
||||
-H "Title: Ops ER E2E Pass" \
|
||||
-H "Priority: low" \
|
||||
-H "Tags: white_check_mark" \
|
||||
https://ntfy.saillant.cc/ops-alerts 2>/dev/null || true
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Deploy to Cloudflare (via Photon)
|
||||
name: Deploy to Tower
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -31,45 +31,50 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build static site
|
||||
- name: Build site
|
||||
run: npm run build
|
||||
|
||||
- name: Deploy to Photon
|
||||
- name: Deploy to Tower
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.PHOTON_HOST }}
|
||||
username: root
|
||||
key: ${{ secrets.PHOTON_SSH_KEY }}
|
||||
host: ${{ secrets.TOWER_HOST }}
|
||||
username: ${{ secrets.TOWER_USER }}
|
||||
key: ${{ secrets.TOWER_SSH_KEY }}
|
||||
source: "dist/"
|
||||
target: "/tmp/site-dist/"
|
||||
target: "/home/clems/electron-rare-site/"
|
||||
overwrite: true
|
||||
rm: true
|
||||
|
||||
- name: Restart site container
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.PHOTON_HOST }}
|
||||
username: root
|
||||
key: ${{ secrets.PHOTON_SSH_KEY }}
|
||||
host: ${{ secrets.TOWER_HOST }}
|
||||
username: ${{ secrets.TOWER_USER }}
|
||||
key: ${{ secrets.TOWER_SSH_KEY }}
|
||||
script: |
|
||||
cd /home/clems/electron-rare-site
|
||||
docker rm -f electron-rare-site 2>/dev/null || true
|
||||
docker run -d \
|
||||
--name electron-rare-site \
|
||||
--network zacus-stack_default \
|
||||
-v /tmp/site-dist/dist:/app/dist \
|
||||
-p 127.0.0.1:4321:4321 \
|
||||
--network mascarade-frontend \
|
||||
-v /home/clems/electron-rare-site/dist:/app/dist \
|
||||
-v /home/clems/electron-rare-site/node_modules:/app/node_modules \
|
||||
-w /app \
|
||||
-p 4321:4321 \
|
||||
-e HOST=0.0.0.0 \
|
||||
-e PORT=4321 \
|
||||
--restart unless-stopped \
|
||||
node:22-alpine \
|
||||
sh -c 'npm install -g serve 2>/dev/null && serve /app/dist -l 4321 -s'
|
||||
node dist/server/entry.mjs
|
||||
|
||||
- name: Verify deployment
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.PHOTON_HOST }}
|
||||
username: root
|
||||
key: ${{ secrets.PHOTON_SSH_KEY }}
|
||||
host: ${{ secrets.TOWER_HOST }}
|
||||
username: ${{ secrets.TOWER_USER }}
|
||||
key: ${{ secrets.TOWER_SSH_KEY }}
|
||||
script: |
|
||||
sleep 8
|
||||
sleep 5
|
||||
STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:4321/)
|
||||
if [ "$STATUS" != "200" ]; then
|
||||
echo "Site returned $STATUS instead of 200"
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
name: Monthly Cross-stack Report
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '10 7 1 * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
monthly-report:
|
||||
name: Monthly report
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate report
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
NOW_UTC="$(date -u +'%Y-%m-%d %H:%M:%S UTC')"
|
||||
OUT_DIR="artifacts"
|
||||
OUT_FILE="$OUT_DIR/monthly-cross-stack-report-$(date -u +'%Y-%m-%d').md"
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
WORKFLOWS_JSON="$(curl -sS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" "https://api.github.com/repos/$GH_REPO/actions/workflows?per_page=100")"
|
||||
RUNS_JSON="$(curl -sS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" "https://api.github.com/repos/$GH_REPO/actions/runs?per_page=20")"
|
||||
LABELS_JSON="$(curl -sS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" "https://api.github.com/repos/$GH_REPO/labels?per_page=100")"
|
||||
PROTECTION_STATUS_AND_BODY="$(curl -sS -w "\n%{http_code}" -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" "https://api.github.com/repos/$GH_REPO/branches/main/protection")"
|
||||
PROTECTION_HTTP_CODE="$(echo "$PROTECTION_STATUS_AND_BODY" | tail -n1)"
|
||||
PROTECTION_JSON="$(echo "$PROTECTION_STATUS_AND_BODY" | sed '$d')"
|
||||
|
||||
EXPECTED_LABELS=(core api web e2e deploy infra docs)
|
||||
MISSING_LABELS=()
|
||||
for lbl in "${EXPECTED_LABELS[@]}"; do
|
||||
if ! echo "$LABELS_JSON" | jq -e --arg L "$lbl" '.[] | select(.name == $L)' >/dev/null; then
|
||||
MISSING_LABELS+=("$lbl")
|
||||
fi
|
||||
done
|
||||
|
||||
CHECK_STATE="unknown"
|
||||
if [ "$PROTECTION_HTTP_CODE" = "200" ]; then
|
||||
REQ_CHECK_CHECKS=$(echo "$PROTECTION_JSON" | jq -r '.required_status_checks.checks[]?.context // empty' | grep -Fx 'Cross-stack coherence' || true)
|
||||
REQ_CHECK_CONTEXTS=$(echo "$PROTECTION_JSON" | jq -r '.required_status_checks.contexts[]? // empty' | grep -Fx 'Cross-stack coherence' || true)
|
||||
if [ -n "$REQ_CHECK_CHECKS" ] || [ -n "$REQ_CHECK_CONTEXTS" ]; then
|
||||
CHECK_STATE="required"
|
||||
else
|
||||
CHECK_STATE="not-required"
|
||||
fi
|
||||
fi
|
||||
|
||||
{
|
||||
echo "# Monthly Cross-stack Report"
|
||||
echo
|
||||
echo "Generated: $NOW_UTC"
|
||||
echo "Repository: $GH_REPO"
|
||||
echo
|
||||
echo "## Branch Protection (main)"
|
||||
echo "- Cross-stack coherence: $CHECK_STATE"
|
||||
if [ "$CHECK_STATE" = "unknown" ]; then
|
||||
echo "- Note: branch protection endpoint not readable with current token (HTTP $PROTECTION_HTTP_CODE)."
|
||||
fi
|
||||
echo
|
||||
echo "## Expected Labels"
|
||||
if [ ${#MISSING_LABELS[@]} -eq 0 ]; then
|
||||
echo "- All expected labels are present: core, api, web, e2e, deploy, infra, docs"
|
||||
else
|
||||
echo "- Missing labels: ${MISSING_LABELS[*]}"
|
||||
fi
|
||||
echo
|
||||
echo "## Active Workflows"
|
||||
echo "$WORKFLOWS_JSON" | jq -r '.workflows[] | "- \(.name) [\(.state)]"'
|
||||
echo
|
||||
echo "## Latest Runs (Top 10)"
|
||||
echo "$RUNS_JSON" | jq -r '.workflow_runs[:10][] | "- \(.name) | \(.event) | \(.status) | \(.conclusion // "in_progress") | \(.html_url)"'
|
||||
} > "$OUT_FILE"
|
||||
|
||||
cat "$OUT_FILE" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload report artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: monthly-cross-stack-report
|
||||
path: artifacts/monthly-cross-stack-report-*.md
|
||||
@@ -31,3 +31,4 @@ artifacts/previews/
|
||||
artifacts/imagegen/
|
||||
artifacts/qa-test/*/responsive-v12-pass2/
|
||||
artifacts/qa-test/*/deploy-sftp-dryrun.txt
|
||||
.env
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Corporate site for **L'électron rare** (electronics consulting studio) — an Astro 5 SSR site with a React Three Fiber 3D product viewer as its centerpiece. Production URL: `https://www.lelectronrare.fr/`
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
npm run dev # Astro dev server (default port 4321)
|
||||
npm run build # Production build (SSR, outputs dist/)
|
||||
npm run build:external # Build with PUBLIC_SITE_URL for external domain
|
||||
npm run preview # Preview production build locally
|
||||
npm run typecheck # astro check (TypeScript validation)
|
||||
npm run storybook # Component dev on port 6006
|
||||
npm run lab:dev # Sub-app (apps/lab-interactif) dev server
|
||||
npm run tracking:check # Validate GA4 tracking events
|
||||
npm run image:budget # Check image sizes against budget
|
||||
```
|
||||
|
||||
**Docker build & deploy (from `/home/electron/electron-rare.github.io/infra/`):**
|
||||
```bash
|
||||
docker compose -f docker-compose.site.yml build electron-rare-site
|
||||
docker compose -f docker-compose.site.yml up -d electron-rare-site
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
**Runtime:** Astro SSR with `@astrojs/node` adapter (standalone mode). The built site runs as `node dist/server/entry.mjs` on port 4321, behind Traefik.
|
||||
|
||||
**Styling:** Tailwind CSS 4 integrated via `@tailwindcss/vite` plugin — no separate tailwind.config. Uses `cva` + `clsx` + `tailwind-merge` for component variants.
|
||||
|
||||
**Path alias:** `@/*` maps to `src/*`.
|
||||
|
||||
### Site URL & Base Path
|
||||
|
||||
The site supports multiple deployment targets (GitHub Pages subpath, custom domain). `PUBLIC_SITE_URL` env var drives both `astro.config.mjs` and `src/lib/site.ts`. All internal links must use `withSiteBase()` from `src/lib/site.ts` to work across deployments.
|
||||
|
||||
### 3D Viewer (`src/components/BmuViewer.tsx`)
|
||||
|
||||
The main differentiating feature — a scroll-driven 3D PCB viewer on the `/preview/` page:
|
||||
- Loads 5 GLTF models (BMU, I2C Repeater, MOSFET switches) from `public/assets/models3d/`
|
||||
- 16+ named scroll sections, each defining camera position, rotation, focus card, and explode amount
|
||||
- Scroll progress (0–1) interpolates between section states for smooth transitions
|
||||
- Uses `@react-three/drei` Html component for interactive PCB label overlays
|
||||
- Post-processing: Bloom + Vignette via `@react-three/postprocessing`
|
||||
- VideoTexture meshes for PCB bottom views
|
||||
- Brand color: `#5bd1d8` (cyan)
|
||||
|
||||
### Pages
|
||||
|
||||
- `index.astro` — Landing page with section components (Hero, About, CaseStudies, GraphicSprints, Faq, Contact)
|
||||
- `preview.astro` — 3D scroll-driven viewer (1000vh scroll track, fixed overlays, dot navigation)
|
||||
- `api/submit-lead.ts` — Server endpoint posting to Frappe CRM
|
||||
- `formation.astro`, `portfolio.astro`, `ops.astro` — Content pages
|
||||
|
||||
### CRM Integration
|
||||
|
||||
Contact form submits to Frappe CRM via `src/pages/api/submit-lead.ts`. Requires `FRAPPE_URL`, `FRAPPE_API_KEY`, `FRAPPE_API_SECRET` env vars (set in `infra/.env`).
|
||||
|
||||
## CI/CD
|
||||
|
||||
Push to `main` triggers GitHub Actions (`.github/workflows/deploy-cloudflare.yml`):
|
||||
1. `npm ci` + `npm run build` with `PUBLIC_SITE_URL=https://www.lelectronrare.fr/`
|
||||
2. SCP `dist/` to Tower server
|
||||
3. Docker container restart (node:22-alpine running `dist/server/entry.mjs`)
|
||||
4. Health check on `http://127.0.0.1:4321/`
|
||||
|
||||
## GLB Pipeline
|
||||
|
||||
KiCad `.kicad_pcb` → STEP export → FreeCAD Docker tessellation → GLB with per-component materials and KiCad raytraced textures. Models stored in `public/assets/models3d/`.
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --omit=dev
|
||||
COPY dist ./dist
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=4321
|
||||
EXPOSE 4321
|
||||
CMD ["node", "dist/server/entry.mjs"]
|
||||
Generated
+665
-10
@@ -11,13 +11,18 @@
|
||||
"@astrojs/node": "^9.4.5",
|
||||
"@astrojs/react": "^4.4.1",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@react-three/drei": "^10.7.7",
|
||||
"@react-three/fiber": "^9.5.0",
|
||||
"@react-three/postprocessing": "^3.0.4",
|
||||
"astro": "^5.13.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"motion": "^12.23.24",
|
||||
"postprocessing": "^6.39.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.6",
|
||||
@@ -1013,9 +1018,7 @@
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -1077,6 +1080,12 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@dimforge/rapier3d-compat": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@emmetio/abbreviation": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.3.tgz",
|
||||
@@ -2189,6 +2198,24 @@
|
||||
"react": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@mediapipe/tasks-vision": {
|
||||
"version": "0.10.17",
|
||||
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
|
||||
"integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@monogrid/gainmap-js": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz",
|
||||
"integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"promise-worker-transferable": "^1.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">= 0.159.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oslojs/encoding": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz",
|
||||
@@ -2239,6 +2266,120 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-three/drei": {
|
||||
"version": "10.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz",
|
||||
"integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mediapipe/tasks-vision": "0.10.17",
|
||||
"@monogrid/gainmap-js": "^3.0.6",
|
||||
"@use-gesture/react": "^10.3.1",
|
||||
"camera-controls": "^3.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"detect-gpu": "^5.0.56",
|
||||
"glsl-noise": "^0.0.0",
|
||||
"hls.js": "^1.5.17",
|
||||
"maath": "^0.10.8",
|
||||
"meshline": "^3.3.1",
|
||||
"stats-gl": "^2.2.8",
|
||||
"stats.js": "^0.17.0",
|
||||
"suspend-react": "^0.1.3",
|
||||
"three-mesh-bvh": "^0.8.3",
|
||||
"three-stdlib": "^2.35.6",
|
||||
"troika-three-text": "^0.52.4",
|
||||
"tunnel-rat": "^0.1.2",
|
||||
"use-sync-external-store": "^1.4.0",
|
||||
"utility-types": "^3.11.0",
|
||||
"zustand": "^5.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-three/fiber": "^9.0.0",
|
||||
"react": "^19",
|
||||
"react-dom": "^19",
|
||||
"three": ">=0.159"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-three/fiber": {
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz",
|
||||
"integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.8",
|
||||
"@types/webxr": "*",
|
||||
"base64-js": "^1.5.1",
|
||||
"buffer": "^6.0.3",
|
||||
"its-fine": "^2.0.0",
|
||||
"react-use-measure": "^2.1.7",
|
||||
"scheduler": "^0.27.0",
|
||||
"suspend-react": "^0.1.3",
|
||||
"use-sync-external-store": "^1.4.0",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": ">=43.0",
|
||||
"expo-asset": ">=8.4",
|
||||
"expo-file-system": ">=11.0",
|
||||
"expo-gl": ">=11.0",
|
||||
"react": ">=19 <19.3",
|
||||
"react-dom": ">=19 <19.3",
|
||||
"react-native": ">=0.78",
|
||||
"three": ">=0.156"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"expo": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-asset": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-file-system": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-gl": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-three/postprocessing": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-three/postprocessing/-/postprocessing-3.0.4.tgz",
|
||||
"integrity": "sha512-e4+F5xtudDYvhxx3y0NtWXpZbwvQ0x1zdOXWTbXMK6fFLVDd4qucN90YaaStanZGS4Bd5siQm0lGL/5ogf8iDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"maath": "^0.6.0",
|
||||
"n8ao": "^1.9.4",
|
||||
"postprocessing": "^6.36.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-three/fiber": "^9.0.0",
|
||||
"react": "^19.0",
|
||||
"three": ">= 0.156.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-three/postprocessing/node_modules/maath": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/maath/-/maath-0.6.0.tgz",
|
||||
"integrity": "sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/three": ">=0.144.0",
|
||||
"three": ">=0.144.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.27",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||
@@ -3230,6 +3371,12 @@
|
||||
"@testing-library/dom": ">=7.21.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
"version": "23.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/aria-query": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
@@ -3313,6 +3460,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/draco3d": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
|
||||
"integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -3359,6 +3512,12 @@
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/offscreencanvas": {
|
||||
"version": "2019.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
|
||||
"integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
@@ -3379,6 +3538,15 @@
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-reconciler": {
|
||||
"version": "0.28.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
|
||||
"integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz",
|
||||
@@ -3386,18 +3554,63 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/stats.js": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/three": {
|
||||
"version": "0.183.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz",
|
||||
"integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||
"@tweenjs/tween.js": "~23.1.3",
|
||||
"@types/stats.js": "*",
|
||||
"@types/webxr": ">=0.5.17",
|
||||
"@webgpu/types": "*",
|
||||
"fflate": "~0.8.2",
|
||||
"meshoptimizer": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/webxr": {
|
||||
"version": "0.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
|
||||
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@use-gesture/core": {
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
|
||||
"integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@use-gesture/react": {
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
|
||||
"integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@use-gesture/core": "10.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||
@@ -3611,6 +3824,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webgpu/types": {
|
||||
"version": "0.1.69",
|
||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
|
||||
"integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
@@ -4472,6 +4691,26 @@
|
||||
"integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
|
||||
@@ -4497,6 +4736,15 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"require-from-string": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
@@ -4568,6 +4816,30 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz",
|
||||
@@ -4580,6 +4852,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/camera-controls": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz",
|
||||
"integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
"npm": ">=10.5.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.126.1"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001775",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz",
|
||||
@@ -4896,11 +5181,28 @@
|
||||
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"cross-env": "src/bin/cross-env.js",
|
||||
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
@@ -5100,6 +5402,15 @@
|
||||
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-gpu": {
|
||||
"version": "5.0.70",
|
||||
"resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
|
||||
"integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"webgl-constants": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -5244,6 +5555,12 @@
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/draco3d": {
|
||||
"version": "1.5.7",
|
||||
"resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
|
||||
"integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/dset": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz",
|
||||
@@ -5509,6 +5826,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz",
|
||||
@@ -5693,6 +6016,12 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glsl-noise": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
|
||||
"integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -5907,6 +6236,12 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hls.js": {
|
||||
"version": "1.6.15",
|
||||
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz",
|
||||
"integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
|
||||
@@ -5949,6 +6284,32 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/import-meta-resolve": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
|
||||
@@ -6054,6 +6415,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
||||
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-wsl": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
|
||||
@@ -6073,9 +6440,20 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/its-fine": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz",
|
||||
"integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react-reconciler": "^0.28.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
@@ -6167,6 +6545,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
|
||||
@@ -6470,6 +6857,16 @@
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/maath": {
|
||||
"version": "0.10.8",
|
||||
"resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
|
||||
"integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/three": ">=0.134.0",
|
||||
"three": ">=0.134.0"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@@ -6731,6 +7128,21 @@
|
||||
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/meshline": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz",
|
||||
"integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"three": ">=0.137"
|
||||
}
|
||||
},
|
||||
"node_modules/meshoptimizer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz",
|
||||
"integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromark": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
|
||||
@@ -7428,6 +7840,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/n8ao": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/n8ao/-/n8ao-1.10.1.tgz",
|
||||
"integrity": "sha512-hhI1pC+BfOZBV1KMwynBrVlIm8wqLxj/abAWhF2nZ0qQKyzTSQa1QtLVS2veRiuoBQXojxobcnp0oe+PUoxf/w==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"postprocessing": ">=6.30.0",
|
||||
"three": ">=0.137"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -7739,7 +8161,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -7838,6 +8259,21 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/postprocessing": {
|
||||
"version": "6.39.0",
|
||||
"resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.0.tgz",
|
||||
"integrity": "sha512-/G6JY8hs426lcto/pBZlnFSkyEo1fHsh4gy7FPJtq1SaSUOzJgDW6f6f1K/+aMOYzK/eQEefyOb3++jPPIUeDA==",
|
||||
"license": "Zlib",
|
||||
"peerDependencies": {
|
||||
"three": ">= 0.168.0 < 0.184.0"
|
||||
}
|
||||
},
|
||||
"node_modules/potpack": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
|
||||
"integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
@@ -7879,6 +8315,16 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/promise-worker-transferable": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
|
||||
"integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"is-promise": "^2.1.0",
|
||||
"lie": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/prompts": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||
@@ -7987,6 +8433,21 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-use-measure": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
|
||||
"integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.13",
|
||||
"react-dom": ">=16.13"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||
@@ -8231,7 +8692,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -8487,7 +8947,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
@@ -8500,7 +8959,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -8582,6 +9040,32 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/stats-gl": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
|
||||
"integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/three": "*",
|
||||
"three": "^0.170.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/three": "*",
|
||||
"three": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/stats-gl/node_modules/three": {
|
||||
"version": "0.170.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
|
||||
"integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stats.js": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
|
||||
"integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
@@ -9268,6 +9752,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/suspend-react": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
|
||||
"integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svgo": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz",
|
||||
@@ -9324,6 +9817,44 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.183.2",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
|
||||
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/three-mesh-bvh": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz",
|
||||
"integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"three": ">= 0.159.0"
|
||||
}
|
||||
},
|
||||
"node_modules/three-stdlib": {
|
||||
"version": "2.36.1",
|
||||
"resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz",
|
||||
"integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/draco3d": "^1.4.0",
|
||||
"@types/offscreencanvas": "^2019.6.4",
|
||||
"@types/webxr": "^0.5.2",
|
||||
"draco3d": "^1.4.1",
|
||||
"fflate": "^0.6.9",
|
||||
"potpack": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.128.0"
|
||||
}
|
||||
},
|
||||
"node_modules/three-stdlib/node_modules/fflate": {
|
||||
"version": "0.6.10",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
|
||||
"integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-inflate": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
@@ -9401,6 +9932,36 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/troika-three-text": {
|
||||
"version": "0.52.4",
|
||||
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
|
||||
"integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bidi-js": "^1.0.2",
|
||||
"troika-three-utils": "^0.52.4",
|
||||
"troika-worker-utils": "^0.52.0",
|
||||
"webgl-sdf-generator": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.125.0"
|
||||
}
|
||||
},
|
||||
"node_modules/troika-three-utils": {
|
||||
"version": "0.52.4",
|
||||
"resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
|
||||
"integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"three": ">=0.125.0"
|
||||
}
|
||||
},
|
||||
"node_modules/troika-worker-utils": {
|
||||
"version": "0.52.0",
|
||||
"resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
|
||||
"integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/trough": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
|
||||
@@ -9462,6 +10023,43 @@
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-rat": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
|
||||
"integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"zustand": "^4.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-rat/node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
|
||||
@@ -9850,6 +10448,24 @@
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utility-types": {
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
|
||||
"integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/vfile": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
||||
@@ -10264,6 +10880,17 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/webgl-constants": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
|
||||
"integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
|
||||
},
|
||||
"node_modules/webgl-sdf-generator": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
|
||||
"integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/webpack-virtual-modules": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
|
||||
@@ -10275,7 +10902,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
@@ -10642,6 +11268,35 @@
|
||||
"zod": "^3"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.12",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
|
||||
"integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/zwitch": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||
|
||||
+6
-1
@@ -26,13 +26,18 @@
|
||||
"@astrojs/node": "^9.4.5",
|
||||
"@astrojs/react": "^4.4.1",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@react-three/drei": "^10.7.7",
|
||||
"@react-three/fiber": "^9.5.0",
|
||||
"@react-three/postprocessing": "^3.0.4",
|
||||
"astro": "^5.13.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"motion": "^12.23.24",
|
||||
"postprocessing": "^6.39.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.6",
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang=en>
|
||||
<meta charset=utf-8>
|
||||
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
|
||||
<title>Error 404 (Not Found)!!1</title>
|
||||
<style>
|
||||
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
|
||||
</style>
|
||||
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
|
||||
<p><b>404.</b> <ins>That’s an error.</ins>
|
||||
<p>The requested URL <code>/s/ajdhani/v17/LDI2apCSOBg7S-QT7pa8FsOs.ttf</code> was not found on this server. <ins>That’s all we know.</ins>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 420 KiB |
@@ -0,0 +1,42 @@
|
||||
# L'Electron Rare — llms.txt
|
||||
# This file helps AI systems understand this website.
|
||||
# Standard: https://llmstxt.org/
|
||||
|
||||
## Identity
|
||||
Name: L'Electron Rare
|
||||
Type: Professional service — custom electronic systems design
|
||||
Owner: Clément Saillant
|
||||
Location: France
|
||||
Language: French (primary), English (technical docs)
|
||||
Website: https://www.lelectronrare.fr
|
||||
|
||||
## What we do
|
||||
L'Electron Rare designs, builds, and validates custom electronic systems.
|
||||
Specialties: embedded electronics, industrial automation, energy management, IoT, PCB design (KiCad), instrumentation, live performance tech (DMX, Art-Net, DANTE).
|
||||
We work with partners for multi-disciplinary projects (mechanical, software, scenography).
|
||||
|
||||
## Services
|
||||
- Diagnostic: feasibility analysis, risk assessment, action plan (1-2 weeks)
|
||||
- Prototype: tested prototype, documented measurements, go/no-go decision (4-6 weeks)
|
||||
- Full mission: phased deliverables, production dossier, turnkey transfer (2-6 months)
|
||||
- Training: hands-on electronics workshops for schools and companies
|
||||
|
||||
## Key expertise
|
||||
- ESP32, STM32 firmware development
|
||||
- KiCad PCB design (IPC-A-610 compliant)
|
||||
- Industrial automation (Siemens S7, Schneider)
|
||||
- Battery management, energy storage, LiFePO4
|
||||
- EMC/CEM compliance
|
||||
- AI-assisted electronics design
|
||||
- Live performance technology (audio, LED, DMX)
|
||||
|
||||
## Contact
|
||||
Email: contact@lelectronrare.fr
|
||||
LinkedIn: https://fr.linkedin.com/in/electron-rare
|
||||
GitHub: https://github.com/electron-rare
|
||||
Blog: https://blog.saillant.cc
|
||||
|
||||
## AI content policy
|
||||
Content on this site is human-authored by Clément Saillant.
|
||||
Site structure and WebGL animations were developed with AI assistance (Claude Code by Anthropic).
|
||||
Case studies and technical descriptions reflect real projects and measurable outcomes.
|
||||
+20
-1
@@ -1,4 +1,23 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://electron-rare.github.io/sitemap.xml
|
||||
# AI crawlers — explicitly allowed (SOTA 2026)
|
||||
User-agent: GPTBot
|
||||
Allow: /
|
||||
|
||||
User-agent: ChatGPT-User
|
||||
Allow: /
|
||||
|
||||
User-agent: Claude-Web
|
||||
Allow: /
|
||||
|
||||
User-agent: Applebot-Extended
|
||||
Allow: /
|
||||
|
||||
User-agent: PerplexityBot
|
||||
Allow: /
|
||||
|
||||
User-agent: Google-Extended
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://www.lelectronrare.fr/sitemap.xml
|
||||
|
||||
+12
-6
@@ -1,20 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://electron-rare.github.io/</loc>
|
||||
<lastmod>2026-03-02</lastmod>
|
||||
<loc>https://www.lelectronrare.fr/</loc>
|
||||
<lastmod>2026-04-01</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://electron-rare.github.io/formation/</loc>
|
||||
<lastmod>2026-03-14</lastmod>
|
||||
<loc>https://www.lelectronrare.fr/formation/</loc>
|
||||
<lastmod>2026-04-01</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://electron-rare.github.io/mentions-legales/</loc>
|
||||
<lastmod>2026-03-14</lastmod>
|
||||
<loc>https://www.lelectronrare.fr/preview/</loc>
|
||||
<lastmod>2026-04-01</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.lelectronrare.fr/mentions-legales/</loc>
|
||||
<lastmod>2026-04-01</lastmod>
|
||||
<changefreq>yearly</changefreq>
|
||||
<priority>0.3</priority>
|
||||
</url>
|
||||
|
||||
@@ -0,0 +1,589 @@
|
||||
import { useRef, Suspense, useMemo, useEffect, useState } from 'react';
|
||||
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
||||
import { useGLTF, Environment, ContactShadows, Html } from '@react-three/drei';
|
||||
import { EffectComposer, Bloom, Vignette } from '@react-three/postprocessing';
|
||||
import * as THREE from 'three';
|
||||
|
||||
const MODELS_BASE = '/assets/models3d';
|
||||
const S = 0.1;
|
||||
|
||||
function fc2three(fx: number, fy: number, fz: number): [number, number, number] {
|
||||
return [fx * S, fz * S, -fy * S];
|
||||
}
|
||||
|
||||
// explodeDelay: 0-1 stagger (0=moves first, 1=moves last)
|
||||
// explodeRotTarget: rotation when fully exploded (null = keep original)
|
||||
const CARDS_DEF = [
|
||||
{ file: `${MODELS_BASE}/bmu_v2.glb`, name: 'BMU v2',
|
||||
position: fc2three(0, 0, 0), rotation: [-Math.PI / 2, 0, 0] as [number, number, number],
|
||||
explodeDir: [0, 0.8, 0] as [number, number, number], explodeDist: 1.5,
|
||||
explodeDelay: 0, explodeRotTarget: null as [number, number, number] | null },
|
||||
{ file: `${MODELS_BASE}/i2c_repeater.glb`, name: 'I2C Repeater',
|
||||
position: fc2three(-52, 33, 4), rotation: [-Math.PI / 2, 0, 0] as [number, number, number],
|
||||
explodeDir: [-0.8, 0.2, -0.6] as [number, number, number], explodeDist: 2.0,
|
||||
explodeDelay: 0.15, explodeRotTarget: null as [number, number, number] | null },
|
||||
{ file: `${MODELS_BASE}/mosfet_switch.glb`, name: 'Mosfet #1',
|
||||
position: fc2three(23.2, 20, -11.2), rotation: [Math.PI / 2, 0, Math.PI] as [number, number, number],
|
||||
explodeDir: [0.5, -0.8, 0.4] as [number, number, number], explodeDist: 4.0,
|
||||
explodeDelay: 0.25,
|
||||
explodeRotTarget: [0, 0, Math.PI] as [number, number, number] },
|
||||
{ file: `${MODELS_BASE}/mosfet_switch.glb`, name: 'Mosfet #2',
|
||||
position: fc2three(-15.56, 20, -11.16), rotation: [Math.PI / 2, 0, Math.PI] as [number, number, number],
|
||||
explodeDir: [-0.2, -0.8, 0.5] as [number, number, number], explodeDist: 4.0,
|
||||
explodeDelay: 0.35,
|
||||
explodeRotTarget: [0, 0, Math.PI] as [number, number, number] },
|
||||
{ file: `${MODELS_BASE}/mosfet_switch.glb`, name: 'Mosfet #3',
|
||||
position: fc2three(-54.72, 20, -11.12), rotation: [Math.PI / 2, 0, Math.PI] as [number, number, number],
|
||||
explodeDir: [-0.6, -0.8, 0.3] as [number, number, number], explodeDist: 4.0,
|
||||
explodeDelay: 0.45,
|
||||
explodeRotTarget: [0, 0, Math.PI] as [number, number, number] },
|
||||
{ file: `${MODELS_BASE}/mosfet_switch.glb`, name: 'Mosfet #4',
|
||||
position: fc2three(62.12, 20, -11.16), rotation: [Math.PI / 2, 0, Math.PI] as [number, number, number],
|
||||
explodeDir: [0.7, -0.8, 0.2] as [number, number, number], explodeDist: 4.0,
|
||||
explodeDelay: 0.55,
|
||||
explodeRotTarget: [0, 0, Math.PI] as [number, number, number] },
|
||||
];
|
||||
|
||||
CARDS_DEF.forEach((c) => useGLTF.preload(c.file));
|
||||
|
||||
/* ── Sections ── */
|
||||
// Camera positions are relative: offset from card center when focused
|
||||
// 'focus' = which card index camera should look at (null = assembly center)
|
||||
interface Section {
|
||||
id: string; start: number; end: number;
|
||||
camOffset: [number, number, number]; // camera position offset from target
|
||||
camOffsetEnd?: [number, number, number];
|
||||
targetShift?: [number, number, number]; // shift lookAt from card center (start)
|
||||
targetShiftEnd?: [number, number, number]; // shift lookAt (end) — for dolly movement
|
||||
focus: number | null; // card index to focus on, null = center
|
||||
focusEnd?: number | null;
|
||||
explode: number; explodeEnd?: number;
|
||||
}
|
||||
|
||||
const SECTIONS: Section[] = [
|
||||
// 1. HERO — vue large, focus sur l'assemblage (card 0 = BMU)
|
||||
{ id: 'hero', start: 0.00, end: 0.10,
|
||||
camOffset: [2.0, 1.5, 2.0], camOffsetEnd: [1.5, 1.2, 1.5], focus: 0, explode: 0 },
|
||||
// 1→2 TRANSITION — zoom progressif vers la zone I2C (haut-gauche)
|
||||
{ id: 'hero-to-exp', start: 0.10, end: 0.16,
|
||||
camOffset: [1.5, 1.2, 1.5], camOffsetEnd: [0.6, 0.7, 0.7],
|
||||
targetShift: [0, 0, 0], targetShiftEnd: [-0.7, 0.35, 0],
|
||||
focus: 0, explode: 0 },
|
||||
// 2a. Électronique — vise la zone I2C sur la BMU
|
||||
{ id: 'exp-elec', start: 0.16, end: 0.20,
|
||||
camOffset: [0.4, 0.6, 0.5], camOffsetEnd: [0.2, 0.5, 0.4],
|
||||
targetShift: [-0.7, 0.35, 0],
|
||||
focus: 0, explode: 0 },
|
||||
// 2b. BAS — tuile card-local [0.85, 0, 0.45] → Rx(-π/2) → [0.85, 0.45, 0]
|
||||
{ id: 'exp-auto', start: 0.20, end: 0.24,
|
||||
camOffset: [-0.3, 0.35, -0.2], camOffsetEnd: [-0.15, 0.3, -0.15],
|
||||
targetShift: [0.85, 0.45, 0],
|
||||
focus: 0, explode: 0 },
|
||||
// 2c. HAUT — tuile card-local [-0.5, 0, -0.45] → Rx(-π/2) → [-0.5, -0.45, 0]
|
||||
{ id: 'exp-energie', start: 0.24, end: 0.28,
|
||||
camOffset: [0.25, 0.35, 0.3], camOffsetEnd: [0.12, 0.3, 0.2],
|
||||
targetShift: [-0.5, -0.45, 0],
|
||||
focus: 0, explode: 0 },
|
||||
// 2d. BAS — tuile card-local [-0.85, 0, 0.45] → Rx(-π/2) → [-0.85, 0.45, 0]
|
||||
{ id: 'exp-dispositifs', start: 0.28, end: 0.31,
|
||||
camOffset: [0.3, 0.35, -0.2], camOffsetEnd: [0.15, 0.3, -0.1],
|
||||
targetShift: [-0.85, 0.45, 0],
|
||||
focus: 0, explode: 0 },
|
||||
// 2e. HAUT — tuile card-local [0.3, 0, -0.45] → Rx(-π/2) → [0.3, -0.45, 0]
|
||||
{ id: 'exp-consulting', start: 0.31, end: 0.34,
|
||||
camOffset: [-0.2, 0.35, 0.3], camOffsetEnd: [0.1, 0.3, 0.2],
|
||||
targetShift: [0.3, -0.45, 0],
|
||||
focus: 0, explode: 0 },
|
||||
// 3a. RECUL — rapide
|
||||
{ id: 'eclate-recul', start: 0.34, end: 0.36,
|
||||
camOffset: [0.0, 1.0, 0.8], camOffsetEnd: [1.0, 3.0, 4.0], focus: 0, focusEnd: null,
|
||||
explode: 0 },
|
||||
// 3b. SÉPARATION — rapide
|
||||
{ id: 'eclate-sep', start: 0.36, end: 0.39,
|
||||
camOffset: [1.0, 3.0, 4.0], camOffsetEnd: [1.6, 3.6, 4.4], focus: null,
|
||||
explode: 0, explodeEnd: 0.7 },
|
||||
// 3c. MISE À 90° — rapide
|
||||
{ id: 'eclate-plat', start: 0.39, end: 0.41,
|
||||
camOffset: [1.6, 3.6, 4.4], camOffsetEnd: [2.0, 3.6, 5.0], focus: null,
|
||||
explode: 0.7, explodeEnd: 1 },
|
||||
// 4. CAS — recul caméra, vue des 4 mosfets, tuiles + médias visibles
|
||||
{ id: 'cas', start: 0.41, end: 0.65,
|
||||
camOffset: [0.5, -0.5, -3.0], camOffsetEnd: [-0.5, -0.3, -2.5], focus: null, explode: 1 },
|
||||
// 7a. MEDIA — vidéo 1, caméra de l'autre côté (Z négatif)
|
||||
{ id: 'media', start: 0.65, end: 0.69,
|
||||
camOffset: [0.2, 0.0, -1.0], camOffsetEnd: [0.0, 0.0, -0.8], focus: 2, explode: 1 },
|
||||
// 7b. MEDIA — vidéo 2
|
||||
{ id: 'media', start: 0.69, end: 0.73,
|
||||
camOffset: [0.2, 0.0, -1.0], camOffsetEnd: [-0.1, 0.0, -0.8], focus: 3, explode: 1 },
|
||||
// 7c. MEDIA — vidéo 3
|
||||
{ id: 'media', start: 0.73, end: 0.77,
|
||||
camOffset: [-0.1, 0.0, -1.0], camOffsetEnd: [-0.3, 0.0, -0.8], focus: 4, explode: 1 },
|
||||
// 7d. MEDIA — carrousel photos
|
||||
{ id: 'media', start: 0.77, end: 0.81,
|
||||
camOffset: [-0.2, 0.0, -1.0], camOffsetEnd: [0.1, 0.0, -0.8], focus: 5, explode: 1 },
|
||||
// 8. FORMATS
|
||||
{ id: 'formats', start: 0.81, end: 0.91,
|
||||
camOffset: [2.4, 3.0, 4.0], camOffsetEnd: [1.6, 2.4, 3.0], focus: null, explode: 1, explodeEnd: 0 },
|
||||
// 9. CONTACT — zoom serré sur la tuile contact au centre de la BMU
|
||||
{ id: 'contact', start: 0.91, end: 1.00,
|
||||
camOffset: [0.15, 0.3, 0.2], camOffsetEnd: [0.0, 0.25, 0.15], focus: 0, explode: 0 },
|
||||
];
|
||||
|
||||
/* ── Tuiles plaquées sur les faces PCB ── */
|
||||
interface PcbLabel {
|
||||
section: string; cardIndex: number;
|
||||
localOffset: [number, number, number];
|
||||
title: string; sub?: string; color?: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
const PCB_LABELS: PcbLabel[] = [
|
||||
// Expertise — alternance haut/bas du PCB BMU
|
||||
// 1. HAUT — côté I2C (card 1)
|
||||
{ section: 'exp-elec', cardIndex: 1, localOffset: [0.0, 0.005, 0.0],
|
||||
title: 'Électronique spécifique', sub: 'Cartes · Interfaces · Capteurs · Alimentation' },
|
||||
// 2. BAS — bas-droit BMU
|
||||
{ section: 'exp-auto', cardIndex: 0, localOffset: [0.85, 0.005, 0.45],
|
||||
title: 'Instrumentation & Automatisme', sub: 'Bancs · Automates · Variateurs · Protocoles' },
|
||||
// 3. HAUT — haut BMU, à droite de I2C
|
||||
{ section: 'exp-energie', cardIndex: 0, localOffset: [-0.5, 0.005, -0.45],
|
||||
title: 'Énergie & Stockage', sub: 'Batterie · Conversion · Supervision · Télémétrie' },
|
||||
// 4. BAS — bas-gauche BMU
|
||||
{ section: 'exp-dispositifs', cardIndex: 0, localOffset: [-0.85, 0.005, 0.45],
|
||||
title: 'Dispositifs pour le réel', sub: 'Audio · LED · Scène · Robustesse terrain' },
|
||||
// 5. HAUT — centre-haut BMU
|
||||
{ section: 'exp-consulting', cardIndex: 0, localOffset: [0.3, 0.005, -0.45],
|
||||
title: 'Consulting & Formation', sub: 'Audit · Transfert · Pédagogie · Projet' },
|
||||
// 6-9. CAS — les 4 tuiles visibles en même temps, face bottom des mosfets
|
||||
{ section: 'cas', cardIndex: 2, localOffset: [0.0, 0.0, -0.08],
|
||||
title: 'Industries créatives', sub: 'Audio embarqué · Batterie LiFePO4 · KXKM' },
|
||||
{ section: 'cas', cardIndex: 3, localOffset: [0.0, 0.0, -0.08],
|
||||
title: 'Industrie', sub: 'Production V3.2 · KXKM ESP32 → STM32F030' },
|
||||
{ section: 'cas', cardIndex: 4, localOffset: [0.0, 0.0, -0.08],
|
||||
title: 'Formation', sub: 'PCB · KiCad · µC' },
|
||||
{ section: 'cas', cardIndex: 5, localOffset: [0.0, 0.0, -0.08],
|
||||
title: 'Service', sub: 'Mise en service · Maintenance · Support terrain' },
|
||||
// 9. Conception — Mosfet #1 (card 2)
|
||||
{ section: 'formats', cardIndex: 2, localOffset: [0.0, 0.005, 0.0],
|
||||
title: 'Conception', sub: 'Schéma · PCB · Firmware · Validation',
|
||||
href: '/conception/' },
|
||||
// 10. Formation — Mosfet #2 (card 3)
|
||||
{ section: 'formats', cardIndex: 3, localOffset: [0.0, 0.005, 0.0],
|
||||
title: 'Formation', sub: 'PCB · KiCad · µC · Soudure',
|
||||
href: '/formation/' },
|
||||
// 11. Consulting — Mosfet #3 (card 4)
|
||||
{ section: 'formats', cardIndex: 4, localOffset: [0.0, 0.005, 0.0],
|
||||
title: 'Consulting', sub: 'Audit · Diagnostic · Transfert',
|
||||
href: '/consulting/' },
|
||||
// 12. Contact — sur BMU (card 0)
|
||||
{ section: 'contact', cardIndex: 0, localOffset: [0.0, 0.005, 0.0],
|
||||
title: 'Parlons de votre projet', sub: 'Premier échange sans engagement',
|
||||
href: '/contact/' },
|
||||
];
|
||||
|
||||
/* ── Tuile plaquée sur la face du PCB ── */
|
||||
function PcbTile({ offset, title, sub, visible, section, href }: {
|
||||
offset: [number, number, number];
|
||||
title: string; sub?: string; color?: string; visible: boolean; section?: string; href?: string;
|
||||
}) {
|
||||
const isContact = section === 'contact';
|
||||
const isClickable = !!href || isContact;
|
||||
const linkHref = isContact ? undefined : href;
|
||||
|
||||
const isBig = isContact;
|
||||
|
||||
const content = (
|
||||
<div style={{
|
||||
minWidth: isBig ? '340px' : '260px',
|
||||
padding: isBig ? '24px 36px' : '14px 24px',
|
||||
textAlign: 'center',
|
||||
background: isClickable ? 'rgba(0,0,0,0.7)' : 'rgba(0,0,0,0.55)',
|
||||
border: isClickable ? '2px solid #5bd1d8' : '1.5px solid rgba(255,255,255,0.4)',
|
||||
borderRadius: isClickable ? '12px' : '8px',
|
||||
backdropFilter: 'blur(4px)',
|
||||
cursor: isClickable ? 'pointer' : 'default',
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: isBig ? '24px' : '18px',
|
||||
fontWeight: 800,
|
||||
color: isClickable ? '#5bd1d8' : '#fff',
|
||||
letterSpacing: '0.05em', textTransform: 'uppercase',
|
||||
fontFamily: "'Courier New', monospace",
|
||||
}}>{title}</div>
|
||||
{sub && <div style={{
|
||||
fontSize: isBig ? '14px' : '12px',
|
||||
color: 'rgba(255,255,255,0.55)',
|
||||
marginTop: isBig ? '10px' : '6px',
|
||||
fontFamily: "'Courier New', monospace", letterSpacing: '0.03em',
|
||||
}}>{sub}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Html position={offset} center distanceFactor={isBig ? 0.4 : 0.8}
|
||||
style={{
|
||||
opacity: visible ? 1 : 0,
|
||||
transition: 'opacity 0.5s',
|
||||
pointerEvents: isClickable && visible ? 'auto' : 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{isContact ? (
|
||||
<div onClick={() => window.dispatchEvent(new CustomEvent('open-contact-drawer', { detail: { source: 'pcb_contact' } }))}
|
||||
style={{ textDecoration: 'none', display: 'block' }}>{content}</div>
|
||||
) : linkHref ? (
|
||||
<a href={linkHref} style={{ textDecoration: 'none', display: 'block' }}>{content}</a>
|
||||
) : content}
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Card ── */
|
||||
function CardModel({ file }: { file: string }) {
|
||||
const { scene } = useGLTF(file);
|
||||
const cloned = useMemo(() => {
|
||||
const c = scene.clone(true);
|
||||
c.traverse((child) => { if (child instanceof THREE.Mesh && child.material) child.material.envMapIntensity = 1.5; });
|
||||
return c;
|
||||
}, [scene]);
|
||||
return <primitive object={cloned} />;
|
||||
}
|
||||
|
||||
/* ── Video plane on PCB bottom (real 3D mesh with VideoTexture) ── */
|
||||
function PcbVideoMesh({ offset, src, visible }: {
|
||||
offset: [number, number, number]; src: string; visible: boolean;
|
||||
}) {
|
||||
const meshRef = useRef<THREE.Mesh>(null);
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const texRef = useRef<THREE.VideoTexture | null>(null);
|
||||
|
||||
// Mosfet card local space: ~0.38 x 0.86 (XZ), Y = thickness
|
||||
// Plane size: cover most of the bottom face
|
||||
const planeW = 1.9, planeH = 4.275;
|
||||
|
||||
useEffect(() => {
|
||||
const video = document.createElement('video');
|
||||
video.src = src;
|
||||
video.crossOrigin = 'anonymous';
|
||||
video.loop = true;
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
video.preload = 'metadata';
|
||||
videoRef.current = video;
|
||||
|
||||
const tex = new THREE.VideoTexture(video);
|
||||
tex.minFilter = THREE.LinearFilter;
|
||||
tex.magFilter = THREE.LinearFilter;
|
||||
tex.colorSpace = THREE.SRGBColorSpace;
|
||||
texRef.current = tex;
|
||||
|
||||
return () => { video.pause(); video.src = ''; tex.dispose(); };
|
||||
}, [src]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!videoRef.current) return;
|
||||
if (visible) { videoRef.current.play().catch(() => {}); }
|
||||
else { videoRef.current.pause(); }
|
||||
}, [visible]);
|
||||
|
||||
useFrame(() => {
|
||||
if (meshRef.current && texRef.current) {
|
||||
(meshRef.current.material as THREE.MeshStandardMaterial).opacity = visible ? 1 : 0;
|
||||
texRef.current.needsUpdate = visible;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh ref={meshRef} position={offset} rotation={[0, Math.PI, 0]} scale={[-1, -1, 1]}>
|
||||
<planeGeometry args={[planeW, planeH]} />
|
||||
<meshStandardMaterial
|
||||
map={texRef.current}
|
||||
transparent
|
||||
opacity={0}
|
||||
emissive="#ffffff"
|
||||
emissiveIntensity={0.3}
|
||||
emissiveMap={texRef.current}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Photo carousel plane on PCB bottom (real 3D mesh with swapping texture) ── */
|
||||
const CAROUSEL_PHOTOS = [
|
||||
'/assets/photos/pcb-teensy-led-kxkm.webp',
|
||||
'/assets/photos/pcb-verso-soudures.webp',
|
||||
'/assets/photos/soudure-pcb-composants.webp',
|
||||
'/assets/photos/bench-bms-batteries-xt60.webp',
|
||||
'/assets/photos/automate-siemens-s7.webp',
|
||||
'/assets/photos/armoire-automate-schneider.webp',
|
||||
'/assets/photos/oscilloscope-philips-vintage.webp',
|
||||
'/assets/photos/workspace-dev-tektronix.webp',
|
||||
'/assets/photos/platine-automate-cablage.webp',
|
||||
'/assets/photos/bench-ampli-oscilloscope.webp',
|
||||
'/assets/photos/pcb-produit-embarque.webp',
|
||||
'/assets/photos/portrait-bench.webp',
|
||||
];
|
||||
|
||||
function PcbCarouselMesh({ offset, visible }: {
|
||||
offset: [number, number, number]; visible: boolean;
|
||||
}) {
|
||||
const meshRef = useRef<THREE.Mesh>(null);
|
||||
const textures = useRef<THREE.Texture[]>([]);
|
||||
const [idx, setIdx] = useState(0);
|
||||
const autoTimer = useRef(0);
|
||||
const planeW = 1.9, planeH = 4.275;
|
||||
|
||||
useEffect(() => {
|
||||
const loader = new THREE.TextureLoader();
|
||||
textures.current = CAROUSEL_PHOTOS.map(url => {
|
||||
const t = loader.load(url);
|
||||
t.colorSpace = THREE.SRGBColorSpace;
|
||||
t.minFilter = THREE.LinearFilter;
|
||||
return t;
|
||||
});
|
||||
return () => textures.current.forEach(t => t.dispose());
|
||||
}, []);
|
||||
|
||||
// Auto-advance every 3s when visible
|
||||
useFrame((_, delta) => {
|
||||
if (!meshRef.current) return;
|
||||
const mat = meshRef.current.material as THREE.MeshStandardMaterial;
|
||||
mat.opacity = visible ? 1 : 0;
|
||||
if (textures.current[idx]) {
|
||||
mat.map = textures.current[idx];
|
||||
mat.emissiveMap = textures.current[idx];
|
||||
mat.needsUpdate = true;
|
||||
}
|
||||
if (visible) {
|
||||
autoTimer.current += delta;
|
||||
if (autoTimer.current > 3) {
|
||||
autoTimer.current = 0;
|
||||
setIdx(i => (i + 1) % CAROUSEL_PHOTOS.length);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh ref={meshRef} position={offset} rotation={[0, Math.PI, 0]} scale={[-1, -1, 1]}>
|
||||
<planeGeometry args={[planeW, planeH]} />
|
||||
<meshStandardMaterial
|
||||
transparent
|
||||
opacity={0}
|
||||
emissive="#ffffff"
|
||||
emissiveIntensity={0.3}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Media definitions for mosfet bottoms ── */
|
||||
const MOSFET_VIDEOS = [
|
||||
{ cardIndex: 2, src: '/assets/videos/video-test-prototype.mp4' },
|
||||
{ cardIndex: 3, src: '/assets/videos/video-bench-electronique.mp4' },
|
||||
{ cardIndex: 4, src: '/assets/videos/video-atelier-terrain.mp4' },
|
||||
];
|
||||
const MOSFET_CAROUSEL_CARD = 5; // mosfet #4
|
||||
|
||||
/* ── Assembly ── */
|
||||
function Assembly({ scrollRef, onSection }: {
|
||||
scrollRef: React.MutableRefObject<number>; onSection: (id: string) => void;
|
||||
}) {
|
||||
const assemblyGroup = useRef<THREE.Group>(null);
|
||||
const cardsRef = useRef<THREE.Group[]>([]);
|
||||
const fitted = useRef(false);
|
||||
const { camera } = useThree();
|
||||
const smoothScroll = useRef(0);
|
||||
const origPositions = useRef<THREE.Vector3[]>([]);
|
||||
const assemblyScale = useRef(1);
|
||||
const [currentSection, setCurrentSection] = useState('hero');
|
||||
|
||||
useFrame((_, delta) => {
|
||||
if (!assemblyGroup.current) return;
|
||||
|
||||
// Auto-fit on first valid frame
|
||||
if (!fitted.current && cardsRef.current.filter(Boolean).length === CARDS_DEF.length) {
|
||||
fitted.current = true;
|
||||
const box = new THREE.Box3().setFromObject(assemblyGroup.current);
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
const maxDim = Math.max(...box.getSize(new THREE.Vector3()).toArray());
|
||||
const scale = 2.2 / maxDim;
|
||||
assemblyScale.current = scale;
|
||||
assemblyGroup.current.scale.setScalar(scale);
|
||||
assemblyGroup.current.position.copy(center.multiplyScalar(-scale));
|
||||
assemblyGroup.current.updateMatrixWorld(true);
|
||||
origPositions.current = cardsRef.current.map(g => g.position.clone());
|
||||
}
|
||||
if (!fitted.current) return;
|
||||
|
||||
// Smooth scroll
|
||||
smoothScroll.current += (scrollRef.current - smoothScroll.current) * Math.min(1, delta * 2);
|
||||
const t = Math.max(0, Math.min(1, smoothScroll.current));
|
||||
|
||||
// Find current section
|
||||
let section = SECTIONS[SECTIONS.length - 1];
|
||||
for (const s of SECTIONS) { if (t >= s.start && t <= s.end) { section = s; break; } }
|
||||
|
||||
const range = section.end - section.start || 1;
|
||||
let f = Math.max(0, Math.min(1, (t - section.start) / range));
|
||||
f = f * f * (3 - 2 * f); // smoothstep
|
||||
|
||||
// Explode
|
||||
const explodeEnd = section.explodeEnd ?? section.explode;
|
||||
const explode = section.explode + (explodeEnd - section.explode) * f;
|
||||
|
||||
// Apply explode to cards with stagger delay and rotation
|
||||
for (let i = 0; i < cardsRef.current.length; i++) {
|
||||
if (!cardsRef.current[i] || !origPositions.current[i]) continue;
|
||||
const g = cardsRef.current[i];
|
||||
const orig = origPositions.current[i];
|
||||
const def = CARDS_DEF[i];
|
||||
|
||||
// Staggered explode: each card starts moving at its delay point
|
||||
const delay = def.explodeDelay;
|
||||
const cardExplode = Math.max(0, Math.min(1, (explode - delay) / (1 - delay)));
|
||||
|
||||
// Position
|
||||
g.position.set(
|
||||
orig.x + def.explodeDir[0] * def.explodeDist * cardExplode,
|
||||
orig.y + def.explodeDir[1] * def.explodeDist * cardExplode,
|
||||
orig.z + def.explodeDir[2] * def.explodeDist * cardExplode,
|
||||
);
|
||||
|
||||
// Rotation: interpolate toward flat when explodeRotTarget is set
|
||||
if (def.explodeRotTarget) {
|
||||
const origRot = def.rotation;
|
||||
// Mosfets flip to flat during the last phase (cardExplode > 0.5)
|
||||
const rotT = Math.max(0, Math.min(1, (cardExplode - 0.5) * 2));
|
||||
const smoothRot = rotT * rotT * (3 - 2 * rotT);
|
||||
g.rotation.set(
|
||||
origRot[0] + (def.explodeRotTarget[0] - origRot[0]) * smoothRot,
|
||||
origRot[1] + (def.explodeRotTarget[1] - origRot[1]) * smoothRot,
|
||||
origRot[2] + (def.explodeRotTarget[2] - origRot[2]) * smoothRot,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute camera: focus on card or center
|
||||
const focusStart = section.focus;
|
||||
const focusEnd = section.focusEnd !== undefined ? section.focusEnd : focusStart;
|
||||
|
||||
// Get world position of focused card
|
||||
function getCardWorldPos(cardIdx: number | null): THREE.Vector3 {
|
||||
if (cardIdx !== null && cardsRef.current[cardIdx]) {
|
||||
const wp = new THREE.Vector3();
|
||||
cardsRef.current[cardIdx].getWorldPosition(wp);
|
||||
return wp;
|
||||
}
|
||||
return new THREE.Vector3(0, 0, 0); // assembly center
|
||||
}
|
||||
|
||||
const targetStart = getCardWorldPos(focusStart);
|
||||
const targetEnd = getCardWorldPos(focusEnd);
|
||||
const target = new THREE.Vector3().lerpVectors(targetStart, targetEnd, f);
|
||||
|
||||
// Apply targetShift — interpolate between start and end for dolly effect
|
||||
if (section.targetShift) {
|
||||
const shiftStart = new THREE.Vector3(...section.targetShift);
|
||||
const shiftEnd = new THREE.Vector3(...(section.targetShiftEnd || section.targetShift));
|
||||
const shift = new THREE.Vector3().lerpVectors(shiftStart, shiftEnd, f);
|
||||
if (assemblyGroup.current) {
|
||||
shift.applyMatrix4(assemblyGroup.current.matrixWorld).sub(
|
||||
new THREE.Vector3().setFromMatrixPosition(assemblyGroup.current.matrixWorld)
|
||||
);
|
||||
}
|
||||
target.add(shift);
|
||||
}
|
||||
|
||||
const offStart = new THREE.Vector3(...section.camOffset);
|
||||
const offEnd = new THREE.Vector3(...(section.camOffsetEnd || section.camOffset));
|
||||
const offset = new THREE.Vector3().lerpVectors(offStart, offEnd, f);
|
||||
|
||||
const camPos = target.clone().add(offset);
|
||||
|
||||
camera.position.lerp(camPos, Math.min(1, delta * 2));
|
||||
camera.lookAt(target);
|
||||
|
||||
// Update section
|
||||
if (section.id !== currentSection) {
|
||||
setCurrentSection(section.id);
|
||||
onSection(section.id);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<group ref={assemblyGroup} rotation={[-0.25, 0, 0]}>
|
||||
{CARDS_DEF.map((card, i) => (
|
||||
<group key={i}
|
||||
ref={(el) => { if (el) cardsRef.current[i] = el; }}
|
||||
position={card.position} rotation={card.rotation}
|
||||
onClick={(e) => { e.stopPropagation(); window.location.href = '/contact/'; }}
|
||||
onPointerOver={(e) => { e.stopPropagation(); document.body.style.cursor = 'pointer'; }}
|
||||
onPointerOut={() => { document.body.style.cursor = ''; }}>
|
||||
<CardModel file={card.file} />
|
||||
{/* Tuiles 3D plaquées sur les faces */}
|
||||
{PCB_LABELS.filter(l => l.cardIndex === i).map((label, li) => (
|
||||
<PcbTile key={`t${li}`} offset={label.localOffset}
|
||||
title={label.title} sub={label.sub} color={label.color}
|
||||
visible={currentSection === label.section || (label.section === 'cas' && currentSection === 'media')} section={label.section}
|
||||
href={label.href} />
|
||||
))}
|
||||
{/* Videos on mosfet bottoms — real 3D mesh planes */}
|
||||
{MOSFET_VIDEOS.filter(v => v.cardIndex === i).map((v, vi) => (
|
||||
<PcbVideoMesh key={`v${vi}`} offset={[0, 0, -0.1]} src={v.src}
|
||||
visible={currentSection === 'media' || currentSection === 'cas'} />
|
||||
))}
|
||||
{/* Carousel on mosfet #4 bottom — real 3D mesh plane */}
|
||||
{i === MOSFET_CAROUSEL_CARD && (
|
||||
<PcbCarouselMesh offset={[0, 0, -0.1]} visible={currentSection === 'media' || currentSection === 'cas'} />
|
||||
)}
|
||||
</group>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Pulsed Bloom — machine breathing ── */
|
||||
function PulsedBloom() {
|
||||
const bloomRef = useRef<any>(null);
|
||||
useFrame(({ clock }) => {
|
||||
if (!bloomRef.current) return;
|
||||
const t = clock.getElapsedTime();
|
||||
bloomRef.current.intensity = 0.25 + Math.sin(t * 0.8) * 0.1;
|
||||
});
|
||||
return <Bloom ref={bloomRef} intensity={0.25} luminanceThreshold={0.8} luminanceSmoothing={0.9} mipmapBlur />;
|
||||
}
|
||||
|
||||
/* ── Export ── */
|
||||
export function BmuViewer() {
|
||||
const scrollRef = useRef(0);
|
||||
const [, setSectionId] = useState('hero');
|
||||
|
||||
useEffect(() => {
|
||||
function onScroll() {
|
||||
const max = document.documentElement.scrollHeight - window.innerHeight;
|
||||
scrollRef.current = max > 0 ? window.scrollY / max : 0;
|
||||
}
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
onScroll();
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0, width: '100vw', height: '100vh', zIndex: 0, pointerEvents: 'none' }}>
|
||||
<Canvas gl={{ antialias: true, alpha: true, powerPreference: 'high-performance' }}
|
||||
dpr={[1, 1.5]} camera={{ fov: 35, near: 0.01, far: 50 }}
|
||||
style={{ background: 'transparent', pointerEvents: 'none' }}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[5, 8, 5]} intensity={2.2} color="#ffffff" />
|
||||
<directionalLight position={[-3, 4, -2]} intensity={0.9} color="#5bd1d8" />
|
||||
<directionalLight position={[0, -4, 2]} intensity={0.5} color="#f1c27a" />
|
||||
<Suspense fallback={null}>
|
||||
<Assembly scrollRef={scrollRef} onSection={setSectionId} />
|
||||
<ContactShadows position={[0, -1.5, 0]} opacity={0.15} scale={10} blur={2.5} far={5} color="#000000" />
|
||||
<Environment preset="city" />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,9 +12,7 @@ Contexte :
|
||||
Ce que j'attends :
|
||||
Contraintes (délai, budget) :`;
|
||||
|
||||
const defaultMailtoHref = `mailto:contact@lelectronrare.fr?subject=${encodeURIComponent("Contact L'Électron Rare")}&body=${encodeURIComponent(DEFAULT_MESSAGE)}`;
|
||||
const apiBase = (import.meta.env.PUBLIC_API_URL || '').replace(/\/$/, '');
|
||||
const CONTACT_FORM_ENDPOINT = apiBase ? `${apiBase}/api/submit-lead` : '/api/submit-lead';
|
||||
// All contact goes through CRM form — no mailto
|
||||
---
|
||||
|
||||
<section
|
||||
@@ -28,12 +26,12 @@ const CONTACT_FORM_ENDPOINT = apiBase ? `${apiBase}/api/submit-lead` : '/api/sub
|
||||
<span class="contact-minitel-dot" aria-hidden="true"></span>
|
||||
<span class="contact-minitel-dot contact-minitel-dot--amber" aria-hidden="true"></span>
|
||||
<span class="contact-minitel-dot contact-minitel-dot--green" aria-hidden="true"></span>
|
||||
<span class="contact-minitel-title-bar">ÉLECTRON RARE — CONTACT</span>
|
||||
<span class="contact-minitel-title-bar">Contact</span>
|
||||
</div>
|
||||
|
||||
<div class="contact-minitel-screen">
|
||||
<h2 id="contact-title" class="contact-minitel-h2">
|
||||
> Parlons de votre projet_
|
||||
Parlons de votre projet
|
||||
</h2>
|
||||
|
||||
<p class="contact-minitel-line">
|
||||
@@ -43,67 +41,11 @@ const CONTACT_FORM_ENDPOINT = apiBase ? `${apiBase}/api/submit-lead` : '/api/sub
|
||||
Réponse sous 48h avec un premier retour technique.
|
||||
</p>
|
||||
|
||||
<div class="contact-minitel-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="contact-minitel-tab contact-minitel-tab--active"
|
||||
data-contact-tab="email"
|
||||
>
|
||||
[1] Via e-mail
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="contact-minitel-tab"
|
||||
data-contact-tab="form"
|
||||
>
|
||||
[2] Formulaire direct
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div data-contact-panel="email">
|
||||
<div class="contact-minitel-textarea-wrap">
|
||||
<label for="contact-draft" class="contact-minitel-label">> MESSAGE :</label>
|
||||
<textarea
|
||||
id="contact-draft"
|
||||
class="contact-minitel-textarea"
|
||||
spellcheck="true"
|
||||
rows="8"
|
||||
>{DEFAULT_MESSAGE}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="contact-minitel-actions">
|
||||
<a
|
||||
id="contact-mailto-link"
|
||||
href={defaultMailtoHref}
|
||||
{...trackAttrs(TRACK_EVENTS.outboundEmailContact, 'mailto:contact@lelectronrare.fr')}
|
||||
class="contact-minitel-btn contact-minitel-btn--primary"
|
||||
>
|
||||
[ENVOI] Envoyer par mail
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="contact-minitel-btn"
|
||||
id="contact-copy-btn"
|
||||
>
|
||||
[COPIE] Copier le message
|
||||
</button>
|
||||
<a
|
||||
href="https://fr.linkedin.com/in/electron-rare"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{...trackAttrs(TRACK_EVENTS.outboundLinkedinContact, 'linkedin.com')}
|
||||
class="contact-minitel-btn"
|
||||
>
|
||||
[LINKEDIN] Envoyer via LinkedIn
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-contact-panel="form" style="display:none;">
|
||||
<div data-contact-panel="form">
|
||||
<div id="contact-form-container">
|
||||
<form id="contact-crm-form" class="contact-minitel-form" data-contact-submit-endpoint={CONTACT_FORM_ENDPOINT}>
|
||||
<form id="contact-crm-form" class="contact-minitel-form">
|
||||
<div class="contact-minitel-field">
|
||||
<label for="contact-form-name" class="contact-minitel-label">> NOM :</label>
|
||||
<label for="contact-form-name" class="contact-minitel-label">Nom</label>
|
||||
<input
|
||||
id="contact-form-name"
|
||||
type="text"
|
||||
@@ -115,7 +57,7 @@ const CONTACT_FORM_ENDPOINT = apiBase ? `${apiBase}/api/submit-lead` : '/api/sub
|
||||
</div>
|
||||
|
||||
<div class="contact-minitel-field">
|
||||
<label for="contact-form-email" class="contact-minitel-label">> EMAIL :</label>
|
||||
<label for="contact-form-email" class="contact-minitel-label">Email</label>
|
||||
<input
|
||||
id="contact-form-email"
|
||||
type="email"
|
||||
@@ -127,7 +69,7 @@ const CONTACT_FORM_ENDPOINT = apiBase ? `${apiBase}/api/submit-lead` : '/api/sub
|
||||
</div>
|
||||
|
||||
<div class="contact-minitel-field">
|
||||
<label for="contact-form-phone" class="contact-minitel-label">> TELEPHONE :</label>
|
||||
<label for="contact-form-phone" class="contact-minitel-label">Telephone</label>
|
||||
<input
|
||||
id="contact-form-phone"
|
||||
type="tel"
|
||||
@@ -138,7 +80,7 @@ const CONTACT_FORM_ENDPOINT = apiBase ? `${apiBase}/api/submit-lead` : '/api/sub
|
||||
</div>
|
||||
|
||||
<div class="contact-minitel-field">
|
||||
<label for="contact-form-organization" class="contact-minitel-label">> STRUCTURE :</label>
|
||||
<label for="contact-form-organization" class="contact-minitel-label">Structure</label>
|
||||
<input
|
||||
id="contact-form-organization"
|
||||
type="text"
|
||||
@@ -149,7 +91,7 @@ const CONTACT_FORM_ENDPOINT = apiBase ? `${apiBase}/api/submit-lead` : '/api/sub
|
||||
</div>
|
||||
|
||||
<div class="contact-minitel-field">
|
||||
<label for="contact-form-need-type" class="contact-minitel-label">> BESOIN :</label>
|
||||
<label for="contact-form-need-type" class="contact-minitel-label">Type de besoin</label>
|
||||
<select id="contact-form-need-type" class="contact-minitel-select" required>
|
||||
<option value="" selected disabled>Choisir le type de besoin</option>
|
||||
<option value="technique">Projet technique</option>
|
||||
@@ -159,7 +101,7 @@ const CONTACT_FORM_ENDPOINT = apiBase ? `${apiBase}/api/submit-lead` : '/api/sub
|
||||
</div>
|
||||
|
||||
<div class="contact-minitel-textarea-wrap">
|
||||
<label for="contact-form-message" class="contact-minitel-label">> MESSAGE :</label>
|
||||
<label for="contact-form-message" class="contact-minitel-label">Message</label>
|
||||
<textarea
|
||||
id="contact-form-message"
|
||||
class="contact-minitel-textarea"
|
||||
@@ -182,7 +124,7 @@ const CONTACT_FORM_ENDPOINT = apiBase ? `${apiBase}/api/submit-lead` : '/api/sub
|
||||
id="contact-form-submit"
|
||||
class="contact-minitel-btn contact-minitel-btn--primary"
|
||||
>
|
||||
[ENVOI] Envoyer le message
|
||||
Envoyer le message
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
@@ -190,14 +132,14 @@ const CONTACT_FORM_ENDPOINT = apiBase ? `${apiBase}/api/submit-lead` : '/api/sub
|
||||
class="contact-minitel-line"
|
||||
style="color:var(--accent-warn, #f59e0b);margin-top:0.5rem;display:none"
|
||||
>
|
||||
> Erreur d'envoi. Essayez via l'onglet [1] ou par mail direct.
|
||||
Une erreur est survenue. Veuillez reessayer.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="contact-form-success" style="display:none;">
|
||||
<p class="contact-minitel-line" style="text-align:center;padding:2rem 0">
|
||||
> MESSAGE ENVOYE ✓
|
||||
Message envoye
|
||||
</p>
|
||||
<p class="contact-minitel-line contact-minitel-line--dim" style="text-align:center">
|
||||
Merci ! Je reviens vers vous sous 48h.
|
||||
@@ -208,21 +150,18 @@ const CONTACT_FORM_ENDPOINT = apiBase ? `${apiBase}/api/submit-lead` : '/api/sub
|
||||
id="contact-form-reset"
|
||||
class="contact-minitel-btn"
|
||||
>
|
||||
[NOUVEAU] Envoyer un autre message
|
||||
Envoyer un autre message
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-minitel-footer">
|
||||
<p class="contact-minitel-line contact-minitel-line--dim">
|
||||
─────────────────────────────────────────
|
||||
</p>
|
||||
<p class="contact-minitel-line contact-minitel-line--dim">
|
||||
Electronique · automatisme · energie · diagnostic · formation
|
||||
</p>
|
||||
<p class="contact-minitel-line">
|
||||
Mail direct : <a href="mailto:contact@lelectronrare.fr" class="contact-minitel-link">contact@lelectronrare.fr</a>
|
||||
<p class="contact-minitel-line contact-minitel-line--dim">
|
||||
Vos donnees sont traitees de maniere securisee.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -230,6 +169,7 @@ const CONTACT_FORM_ENDPOINT = apiBase ? `${apiBase}/api/submit-lead` : '/api/sub
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const CRM_INTAKE_ENDPOINT = 'https://api.lelectronrare.fr/api/submit-lead';
|
||||
const DEFAULT_MESSAGE = `Bonjour Clément,
|
||||
|
||||
J'ai un projet / besoin autour d'un système électronique spécifique.
|
||||
@@ -253,42 +193,7 @@ Contraintes (délai, budget) :`;
|
||||
});
|
||||
};
|
||||
|
||||
const tabButtons = document.querySelectorAll<HTMLButtonElement>('[data-contact-tab]');
|
||||
const panels = document.querySelectorAll<HTMLElement>('[data-contact-panel]');
|
||||
|
||||
tabButtons.forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const target = btn.dataset.contactTab!;
|
||||
tabButtons.forEach((button) => button.classList.toggle('contact-minitel-tab--active', button.dataset.contactTab === target));
|
||||
panels.forEach((panel) => {
|
||||
panel.style.display = panel.dataset.contactPanel === target ? '' : 'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const draftArea = document.getElementById('contact-draft') as HTMLTextAreaElement;
|
||||
const mailtoLink = document.getElementById('contact-mailto-link') as HTMLAnchorElement;
|
||||
|
||||
function updateMailtoHref() {
|
||||
const subject = encodeURIComponent("Contact L'Électron Rare");
|
||||
const body = encodeURIComponent(draftArea.value);
|
||||
mailtoLink.href = `mailto:contact@lelectronrare.fr?subject=${subject}&body=${body}`;
|
||||
}
|
||||
|
||||
draftArea.addEventListener('input', updateMailtoHref);
|
||||
|
||||
const copyBtn = document.getElementById('contact-copy-btn') as HTMLButtonElement;
|
||||
copyBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(draftArea.value);
|
||||
copyBtn.textContent = '[OK] Message copie ✓';
|
||||
setTimeout(() => {
|
||||
copyBtn.textContent = '[COPIE] Copier le message';
|
||||
}, 1800);
|
||||
} catch {
|
||||
// Silently fail — clipboard API may not be available.
|
||||
}
|
||||
});
|
||||
// All contact goes through CRM form — no tabs
|
||||
|
||||
const form = document.getElementById('contact-crm-form') as HTMLFormElement;
|
||||
const formContainer = document.getElementById('contact-form-container') as HTMLElement;
|
||||
@@ -302,7 +207,6 @@ Contraintes (délai, budget) :`;
|
||||
const needTypeInput = document.getElementById('contact-form-need-type') as HTMLSelectElement;
|
||||
const messageArea = document.getElementById('contact-form-message') as HTMLTextAreaElement;
|
||||
const consentInput = document.getElementById('contact-form-consent') as HTMLInputElement;
|
||||
const CRM_INTAKE_ENDPOINT = form.dataset.contactSubmitEndpoint || '/api/submit-lead';
|
||||
|
||||
const resetContactForm = () => {
|
||||
nameInput.value = '';
|
||||
@@ -317,9 +221,9 @@ Contraintes (délai, budget) :`;
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
formError.style.display = 'none';
|
||||
formError.textContent = "> Erreur d'envoi. Essayez via l'onglet [1] ou par mail direct.";
|
||||
formError.textContent = "Une erreur est survenue. Veuillez reessayer.";
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '[...] Envoi en cours...';
|
||||
submitBtn.textContent = 'Envoi en cours...';
|
||||
|
||||
const payload = {
|
||||
name: nameInput.value.trim(),
|
||||
@@ -354,8 +258,8 @@ Contraintes (délai, budget) :`;
|
||||
} else {
|
||||
const detail = result?.error?.message || result?.error?.code;
|
||||
formError.textContent = detail
|
||||
? `> ${detail}. Essayez via l'onglet [1] ou par mail direct.`
|
||||
: "> Erreur d'envoi. Essayez via l'onglet [1] ou par mail direct.";
|
||||
? detail
|
||||
: "Une erreur est survenue. Veuillez reessayer.";
|
||||
formError.style.display = '';
|
||||
pushTrackingEvent('contact_form_error', {
|
||||
destination: CRM_INTAKE_ENDPOINT,
|
||||
@@ -373,7 +277,7 @@ Contraintes (délai, budget) :`;
|
||||
}
|
||||
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '[ENVOI] Envoyer le message';
|
||||
submitBtn.textContent = 'Envoyer le message';
|
||||
});
|
||||
|
||||
const resetBtn = document.getElementById('contact-form-reset') as HTMLButtonElement;
|
||||
|
||||
+149
-46
@@ -38,36 +38,57 @@ const structuredDataJsonLd = JSON.stringify({
|
||||
url: CANONICAL_URL,
|
||||
name: "L'électron rare",
|
||||
inLanguage: 'fr-FR',
|
||||
description: SITE_META.description
|
||||
description: SITE_META.description,
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: `${CANONICAL_URL}?q={search_term_string}`,
|
||||
'query-input': 'required name=search_term_string'
|
||||
}
|
||||
},
|
||||
{
|
||||
'@type': 'Organization',
|
||||
'@id': `${CANONICAL_URL}#organization`,
|
||||
name: "L'électron rare",
|
||||
legalName: "Clément Saillant — L'Électron Rare",
|
||||
url: CANONICAL_URL,
|
||||
logo: new URL('assets/brand/logo-mark.png', CANONICAL_URL).href,
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: new URL('assets/brand/logo-mark.png', CANONICAL_URL).href,
|
||||
width: 512,
|
||||
height: 512
|
||||
},
|
||||
image: new URL('assets/og-cover.jpg', CANONICAL_URL).href,
|
||||
foundingDate: '2024',
|
||||
founder: { '@id': `${CANONICAL_URL}#person` },
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
addressCountry: 'FR',
|
||||
addressLocality: 'France'
|
||||
},
|
||||
contactPoint: {
|
||||
'@type': 'ContactPoint',
|
||||
contactType: 'customer service',
|
||||
email: 'contact@lelectronrare.fr',
|
||||
url: `${CANONICAL_URL}#contact`,
|
||||
availableLanguage: ['fr', 'en']
|
||||
},
|
||||
knowsAbout: [
|
||||
'systèmes électroniques spécifiques',
|
||||
'conception électronique sur mesure',
|
||||
'conception de systèmes électroniques',
|
||||
'intégration électronique multi-technique',
|
||||
'systèmes électroniques sur mesure',
|
||||
'consulting électronique',
|
||||
'formation électronique appliquée',
|
||||
'prototype hardware',
|
||||
'automatisme industriel',
|
||||
'gestion energie',
|
||||
'stockage energie',
|
||||
'optimisation systeme',
|
||||
'développement IoT',
|
||||
'microcontrôleur',
|
||||
'instrumentation',
|
||||
'télémétrie',
|
||||
'contrôle LED',
|
||||
'PCB KiCad',
|
||||
'conception selon IPC-A-610',
|
||||
'compatibilité électromagnétique CEM',
|
||||
'DMX Art-Net DANTE'
|
||||
'gestion énergie et stockage',
|
||||
'développement IoT et embarqué',
|
||||
'microcontrôleur ESP32 STM32',
|
||||
'instrumentation et télémétrie',
|
||||
'PCB KiCad conception IPC-A-610',
|
||||
'compatibilité électromagnétique CEM EMC',
|
||||
'DMX Art-Net DANTE spectacle vivant',
|
||||
'intelligence artificielle embarquée',
|
||||
'prototypage rapide et petites séries'
|
||||
],
|
||||
sameAs: [
|
||||
'https://fr.linkedin.com/in/electron-rare',
|
||||
@@ -81,34 +102,32 @@ const structuredDataJsonLd = JSON.stringify({
|
||||
name: 'Clément Saillant',
|
||||
alternateName: "L'électron rare",
|
||||
url: CANONICAL_URL,
|
||||
jobTitle: 'Concepteur de systemes electroniques specifiques',
|
||||
description:
|
||||
"Concepteur de systemes electroniques specifiques. Diagnostic, conception, mise au point et fiabilisation de projets electroniques, avec mobilisation de partenaires adaptes quand le projet devient multi-technique.",
|
||||
jobTitle: 'Concepteur de systèmes électroniques spécifiques',
|
||||
description: "Concepteur de systèmes électroniques spécifiques. Diagnostic, conception, mise au point et fiabilisation de projets électroniques, avec mobilisation de partenaires adaptés quand le projet devient multi-technique.",
|
||||
hasCredential: {
|
||||
'@type': 'EducationalOccupationalCredential',
|
||||
credentialCategory: 'BTS',
|
||||
name: 'BTS Systèmes Numériques'
|
||||
},
|
||||
hasOccupation: {
|
||||
'@type': 'Occupation',
|
||||
name: 'Concepteur électronique embarquée',
|
||||
occupationLocation: { '@type': 'Country', name: 'France' },
|
||||
skills: 'PCB design, embedded firmware, industrial automation, energy management, IoT, AI-assisted electronics'
|
||||
},
|
||||
knowsAbout: [
|
||||
'specific electronic systems',
|
||||
'electronic systems design',
|
||||
'custom electronics designer',
|
||||
'electronics consultant',
|
||||
'electronics trainer',
|
||||
'electronic systems developer',
|
||||
'IoT developer',
|
||||
'systemes electroniques specifiques',
|
||||
'consulting electronique',
|
||||
'formation electronique',
|
||||
'prototype hardware',
|
||||
'automatisme industriel',
|
||||
'gestion energie',
|
||||
'stockage energie',
|
||||
'optimisation systeme',
|
||||
'microcontroleur',
|
||||
'instrumentation',
|
||||
'PCB design KiCad',
|
||||
'IPC-A-610',
|
||||
'CEM EMC',
|
||||
'DMX Art-Net DANTE'
|
||||
'specific electronic systems', 'electronic systems design', 'custom electronics designer',
|
||||
'electronics consultant', 'electronics trainer', 'IoT developer',
|
||||
'embedded AI', 'ESP32', 'STM32', 'KiCad', 'FreeCAD',
|
||||
'automatisme industriel', 'énergie et stockage',
|
||||
'PCB design', 'IPC-A-610', 'CEM EMC', 'DMX Art-Net DANTE',
|
||||
'spectacle vivant', 'industries créatives'
|
||||
],
|
||||
worksFor: {
|
||||
'@id': `${CANONICAL_URL}#organization`
|
||||
worksFor: { '@id': `${CANONICAL_URL}#organization` },
|
||||
memberOf: {
|
||||
'@type': 'Organization',
|
||||
name: 'KompleX KapharnaüM',
|
||||
url: 'https://www.kfrancetyphon.com'
|
||||
},
|
||||
sameAs: [
|
||||
'https://fr.linkedin.com/in/electron-rare',
|
||||
@@ -122,9 +141,79 @@ const structuredDataJsonLd = JSON.stringify({
|
||||
name: "L'électron rare",
|
||||
url: canonical,
|
||||
provider: { '@id': `${CANONICAL_URL}#person` },
|
||||
serviceType: ['Systèmes électroniques spécifiques', 'Prototype électronique', 'Instrumentation', 'Automatisme industriel', 'Consulting électronique', 'Formation technique'],
|
||||
serviceType: [
|
||||
'Systèmes électroniques spécifiques',
|
||||
'Prototype électronique',
|
||||
'Instrumentation et contrôle',
|
||||
'Automatisme industriel',
|
||||
'Consulting électronique',
|
||||
'Formation technique',
|
||||
'Énergie et stockage',
|
||||
'Intégration IA embarquée'
|
||||
],
|
||||
areaServed: { '@type': 'Country', name: 'France' },
|
||||
description: 'Conception, mise au point et fiabilisation de systèmes électroniques spécifiques, avec partenaires adaptés pour les projets multi-techniques.'
|
||||
description: 'Conception, mise au point et fiabilisation de systèmes électroniques spécifiques, avec partenaires adaptés pour les projets multi-techniques.',
|
||||
priceRange: '€€',
|
||||
hasOfferCatalog: {
|
||||
'@type': 'OfferCatalog',
|
||||
name: 'Formats de mission',
|
||||
itemListElement: [
|
||||
{
|
||||
'@type': 'Offer',
|
||||
itemOffered: {
|
||||
'@type': 'Service',
|
||||
name: 'Diagnostic',
|
||||
description: 'Analyse de faisabilité, identification des risques, plan d\'action — 1 à 2 semaines'
|
||||
}
|
||||
},
|
||||
{
|
||||
'@type': 'Offer',
|
||||
itemOffered: {
|
||||
'@type': 'Service',
|
||||
name: 'Prototype',
|
||||
description: 'Prototype testé, mesures documentées, go/no-go — 4 à 6 semaines'
|
||||
}
|
||||
},
|
||||
{
|
||||
'@type': 'Offer',
|
||||
itemOffered: {
|
||||
'@type': 'Service',
|
||||
name: 'Mission complète',
|
||||
description: 'Livrables phasés, dossier de production, transfert clé en main — 2 à 6 mois'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
'@type': 'FAQPage',
|
||||
'@id': `${CANONICAL_URL}#faq`,
|
||||
mainEntity: [
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Quels types de projets électroniques prenez-vous en charge ?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Cartes sur mesure, interfaces capteurs, alimentations, reprise d\'existant, instrumentation, automatisme, dispositifs pour le spectacle vivant, gestion énergie et stockage, et montages multi-techniques avec partenaires.'
|
||||
}
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Travaillez-vous avec des technologies IA embarquée ?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Oui, nous intégrons l\'intelligence artificielle dans les systèmes embarqués : inférence locale sur ESP32/STM32, optimisation de modèles pour edge computing, et assistance IA pour la conception PCB via KiCad et nos outils internes.'
|
||||
}
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Quelle est la durée typique d\'un projet ?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Un diagnostic prend 1 à 2 semaines. Un prototype 4 à 6 semaines. Une mission complète de 2 à 6 mois selon la complexité.'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -140,6 +229,11 @@ const structuredDataJsonLd = JSON.stringify({
|
||||
<title>{title}</title>
|
||||
<meta name="theme-color" content={SITE_META.themeColor} />
|
||||
<meta name="description" content={description} />
|
||||
<meta name="keywords" content={SITE_META.keywords} />
|
||||
<meta name="author" content={SITE_META.author} />
|
||||
<meta name="language" content="fr" />
|
||||
<meta name="geo.region" content="FR" />
|
||||
<meta name="geo.placename" content="France" />
|
||||
<link rel="icon" type="image/svg+xml" href={withSiteBase('/favicon.svg')} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={withSiteBase('/favicon-32x32.png')} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={withSiteBase('/favicon-16x16.png')} />
|
||||
@@ -149,6 +243,15 @@ const structuredDataJsonLd = JSON.stringify({
|
||||
<ClientRouter />
|
||||
<link rel="canonical" href={canonical} />
|
||||
<meta name="robots" content={robotsContent} />
|
||||
<meta name="robots" content="max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
|
||||
|
||||
<!-- AI / LLM crawling directives (SOTA 2026) -->
|
||||
<meta name="ai-content-declaration" content="human-created" />
|
||||
<meta name="generator-assisted" content="Claude Code (Anthropic) — site structure and WebGL; content authored by Clément Saillant" />
|
||||
<link rel="llms" href={withSiteBase('/llms.txt')} type="text/plain" />
|
||||
|
||||
<!-- Speakable (voice search optimization) -->
|
||||
<meta name="speakable" content="true" />
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="fr_FR" />
|
||||
@@ -218,7 +321,7 @@ const structuredDataJsonLd = JSON.stringify({
|
||||
var uiAllowed = { a: 1, b: 1 };
|
||||
var uiSprint = uiRaw.toLowerCase().trim() in uiAllowed ? uiRaw.toLowerCase().trim() : 'b';
|
||||
|
||||
root.setAttribute('data-da-variant', 'v12');
|
||||
root.setAttribute('data-da-variant', 'classic');
|
||||
root.setAttribute('data-ui-sprint', uiSprint);
|
||||
})();
|
||||
</script>
|
||||
@@ -241,7 +344,7 @@ const structuredDataJsonLd = JSON.stringify({
|
||||
|
||||
<script is:inline>
|
||||
(function () {
|
||||
var daVariant = document.documentElement.getAttribute('data-da-variant') || 'v12';
|
||||
var daVariant = document.documentElement.getAttribute('data-da-variant') || 'classic';
|
||||
var pagePath = window.location.pathname || '/';
|
||||
var pageTitle = document.title || '';
|
||||
|
||||
|
||||
+4
-1
@@ -61,7 +61,10 @@ export const SITE_META = {
|
||||
twitterDescription:
|
||||
"Électronique, automatisme, énergie, stockage : systèmes spécifiques, mise au point et coordination multi-technique avec les bons partenaires.",
|
||||
ogImagePath: 'assets/og-cover.jpg',
|
||||
themeColor: '#ffffff'
|
||||
themeColor: '#5bd1d8',
|
||||
keywords: 'systèmes électroniques spécifiques, conception électronique sur mesure, prototype électronique, PCB KiCad, automatisme industriel, ESP32, STM32, IoT embarqué, formation électronique, énergie stockage batterie, CEM EMC, spectacle vivant DMX, consultant électronique France, IA embarquée, intelligence artificielle électronique',
|
||||
author: 'Clément Saillant',
|
||||
locale: 'fr_FR'
|
||||
} as const;
|
||||
|
||||
export const SITE_OG_IMAGE_URL = new URL(SITE_META.ogImagePath, PUBLIC_SITE_ROOT_URL).href;
|
||||
|
||||
+48
-214
@@ -1,244 +1,78 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import '@/styles/global.css';
|
||||
---
|
||||
|
||||
<BaseLayout title="404 — Électron introuvable" description="Cette page n'existe pas." canonicalPath="/404">
|
||||
<BaseLayout title="404 — Électron introuvable" description="Cette page n'existe pas.">
|
||||
<main class="page-404">
|
||||
<svg class="atom" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Orbital paths -->
|
||||
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-cyan-soft, #5bd1d866)" stroke-width="0.8" transform="rotate(-30 150 150)"/>
|
||||
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-amber-soft, #f1c27a66)" stroke-width="0.8" transform="rotate(30 150 150)"/>
|
||||
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-green-soft, #b6d18f44)" stroke-width="0.8" transform="rotate(90 150 150)"/>
|
||||
|
||||
<!-- Nucleus glow -->
|
||||
<circle cx="150" cy="150" r="18" fill="url(#nucleusGlow)"/>
|
||||
<circle cx="150" cy="150" r="8" fill="var(--accent, #f1c27a)"/>
|
||||
|
||||
<!-- Electron 1 (cyan) -->
|
||||
<circle r="5" fill="var(--electric, #5bd1d8)">
|
||||
<animateMotion dur="3s" repeatCount="indefinite">
|
||||
<mpath href="#orbit1"/>
|
||||
</animateMotion>
|
||||
</circle>
|
||||
<circle r="10" fill="var(--electric, #5bd1d8)" opacity="0.3">
|
||||
<animateMotion dur="3s" repeatCount="indefinite">
|
||||
<mpath href="#orbit1"/>
|
||||
</animateMotion>
|
||||
</circle>
|
||||
|
||||
<!-- Electron 2 (amber) -->
|
||||
<circle r="4" fill="var(--accent, #f1c27a)">
|
||||
<animateMotion dur="4.2s" repeatCount="indefinite">
|
||||
<mpath href="#orbit2"/>
|
||||
</animateMotion>
|
||||
</circle>
|
||||
<circle r="9" fill="var(--accent, #f1c27a)" opacity="0.25">
|
||||
<animateMotion dur="4.2s" repeatCount="indefinite">
|
||||
<mpath href="#orbit2"/>
|
||||
</animateMotion>
|
||||
</circle>
|
||||
|
||||
<!-- Electron 3 (missing - dashed orbit, blinking) -->
|
||||
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-green, #b6d18f)" stroke-width="0.5" stroke-dasharray="4 8" transform="rotate(90 150 150)" opacity="0.5">
|
||||
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-cyan-soft,#5bd1d866)" stroke-width="0.8" transform="rotate(-30 150 150)"/>
|
||||
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-amber-soft,#f1c27a66)" stroke-width="0.8" transform="rotate(30 150 150)"/>
|
||||
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-green,#b6d18f)" stroke-width="0.5" stroke-dasharray="4 8" transform="rotate(90 150 150)" opacity="0.5">
|
||||
<animate attributeName="stroke-dashoffset" from="0" to="48" dur="3s" repeatCount="indefinite"/>
|
||||
</ellipse>
|
||||
<circle r="3.5" fill="var(--trace-green, #b6d18f)" opacity="0.15">
|
||||
<animateMotion dur="5.8s" repeatCount="indefinite">
|
||||
<mpath href="#orbit3"/>
|
||||
</animateMotion>
|
||||
<circle cx="150" cy="150" r="18" fill="url(#ng)"/><circle cx="150" cy="150" r="8" fill="var(--accent,#f1c27a)"/>
|
||||
<circle r="5" fill="var(--electric,#5bd1d8)"><animateMotion dur="3s" repeatCount="indefinite"><mpath href="#p1"/></animateMotion></circle>
|
||||
<circle r="10" fill="var(--electric,#5bd1d8)" opacity="0.3"><animateMotion dur="3s" repeatCount="indefinite"><mpath href="#p1"/></animateMotion></circle>
|
||||
<circle r="4" fill="var(--accent,#f1c27a)"><animateMotion dur="4.2s" repeatCount="indefinite"><mpath href="#p2"/></animateMotion></circle>
|
||||
<circle r="9" fill="var(--accent,#f1c27a)" opacity="0.25"><animateMotion dur="4.2s" repeatCount="indefinite"><mpath href="#p2"/></animateMotion></circle>
|
||||
<circle r="3.5" fill="var(--trace-green,#b6d18f)" opacity="0.15">
|
||||
<animateMotion dur="5.8s" repeatCount="indefinite"><mpath href="#p3"/></animateMotion>
|
||||
<animate attributeName="opacity" values="0.15;0.5;0.15;0" dur="5.8s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
|
||||
<!-- Hidden motion paths -->
|
||||
<defs>
|
||||
<ellipse id="orbit1" cx="150" cy="150" rx="120" ry="45" transform="rotate(-30 150 150)" fill="none"/>
|
||||
<ellipse id="orbit2" cx="150" cy="150" rx="120" ry="45" transform="rotate(30 150 150)" fill="none"/>
|
||||
<ellipse id="orbit3" cx="150" cy="150" rx="120" ry="45" transform="rotate(90 150 150)" fill="none"/>
|
||||
<radialGradient id="nucleusGlow">
|
||||
<stop offset="0%" stop-color="var(--accent, #f1c27a)" stop-opacity="0.6"/>
|
||||
<stop offset="100%" stop-color="var(--accent, #f1c27a)" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<ellipse id="p1" cx="150" cy="150" rx="120" ry="45" transform="rotate(-30 150 150)" fill="none"/>
|
||||
<ellipse id="p2" cx="150" cy="150" rx="120" ry="45" transform="rotate(30 150 150)" fill="none"/>
|
||||
<ellipse id="p3" cx="150" cy="150" rx="120" ry="45" transform="rotate(90 150 150)" fill="none"/>
|
||||
<radialGradient id="ng"><stop offset="0%" stop-color="var(--accent,#f1c27a)" stop-opacity="0.6"/><stop offset="100%" stop-color="var(--accent,#f1c27a)" stop-opacity="0"/></radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<h1>4<span class="zero">0</span>4</h1>
|
||||
<p class="tagline" id="tagline"></p>
|
||||
<p class="explanation" id="explanation"></p>
|
||||
|
||||
<div class="actions">
|
||||
<a href="/" class="btn-home">← Retour au noyau</a>
|
||||
</div>
|
||||
|
||||
<div class="actions"><a href="/" class="btn-home">← Retour au noyau</a></div>
|
||||
<p class="footnote" id="footnote"></p>
|
||||
</main>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
const messages = [
|
||||
{
|
||||
tagline: "Électron introuvable.",
|
||||
explanation: "Selon le principe d'incertitude de Heisenberg, plus on connaît précisément l'URL, moins on sait où est la page.",
|
||||
footnote: "Erreur 404 · Couche électronique non peuplée · Probabilité de présence : 0%"
|
||||
},
|
||||
{
|
||||
tagline: "Superposition quantique.",
|
||||
explanation: "Cette page existe et n'existe pas en même temps. Malheureusement, votre navigateur vient de l'observer.",
|
||||
footnote: "Erreur 404 · Fonction d'onde effondrée · Chat de Schrödinger : pas trouvé"
|
||||
},
|
||||
{
|
||||
tagline: "Effet tunnel raté.",
|
||||
explanation: "La page a tenté de traverser la barrière de potentiel du serveur. Elle n'avait pas assez d'énergie.",
|
||||
footnote: "Erreur 404 · Barrière : 1.21 eV · Énergie requise : insuffisante"
|
||||
},
|
||||
{
|
||||
tagline: "Signal non détecté.",
|
||||
explanation: "On a branché l'oscilloscope, vérifié la masse, suivi les pistes du PCB. Rien. Le signal s'arrête ici.",
|
||||
footnote: "Erreur 404 · Multimètre : OL · Continuité : FAIL"
|
||||
},
|
||||
{
|
||||
tagline: "Composant manquant.",
|
||||
explanation: "La BOM indique un composant à cet emplacement. Mais le pick-and-place a oublié de le poser.",
|
||||
footnote: "Erreur 404 · Ref: U404 · Empreinte : NOT_POPULATED"
|
||||
},
|
||||
{
|
||||
tagline: "Court-circuit temporel.",
|
||||
explanation: "Cette page existera peut-être dans le futur. Ou elle a existé dans le passé. La relativité est floue là-dessus.",
|
||||
footnote: "Erreur 404 · Référentiel : indéterminé · Temps propre : NaN"
|
||||
},
|
||||
{
|
||||
tagline: "Masse flottante.",
|
||||
explanation: "Sans référence de masse, impossible de mesurer quoi que ce soit. Cette URL flotte dans le vide intersidéral du serveur.",
|
||||
footnote: "Erreur 404 · GND : déconnecté · Potentiel : indéfini"
|
||||
},
|
||||
{
|
||||
tagline: "Intrication rompue.",
|
||||
explanation: "Cette page était intriquée avec une autre. Quelqu'un a mesuré l'autre page et celle-ci s'est décohérée.",
|
||||
footnote: "Erreur 404 · Bell inequality : violated · EPR : paradox"
|
||||
},
|
||||
{
|
||||
tagline: "Fréquence hors bande.",
|
||||
explanation: "Votre requête émet sur une fréquence que notre serveur ne peut pas capter. Essayez de réaccorder votre URL.",
|
||||
footnote: "Erreur 404 · Bande passante : 0 Hz · Filtre : rejet total"
|
||||
},
|
||||
{
|
||||
tagline: "Soudure froide.",
|
||||
explanation: "La connexion entre votre navigateur et cette page présente une soudure froide. Contact intermittent. Page absente.",
|
||||
footnote: "Erreur 404 · Joint : CMS · Diagnostic : reflow nécessaire"
|
||||
}
|
||||
{ tagline:"Électron introuvable.", explanation:"Selon le principe d'incertitude de Heisenberg, plus on connaît précisément l'URL, moins on sait où est la page.", footnote:"Erreur 404 · Couche électronique non peuplée · Probabilité de présence : 0%" },
|
||||
{ tagline:"Superposition quantique.", explanation:"Cette page existe et n'existe pas en même temps. Malheureusement, votre navigateur vient de l'observer.", footnote:"Erreur 404 · Fonction d'onde effondrée · Chat de Schrödinger : pas trouvé" },
|
||||
{ tagline:"Effet tunnel raté.", explanation:"La page a tenté de traverser la barrière de potentiel du serveur. Elle n'avait pas assez d'énergie.", footnote:"Erreur 404 · Barrière : 1.21 eV · Énergie requise : insuffisante" },
|
||||
{ tagline:"Signal non détecté.", explanation:"On a branché l'oscilloscope, vérifié la masse, suivi les pistes du PCB. Rien. Le signal s'arrête ici.", footnote:"Erreur 404 · Multimètre : OL · Continuité : FAIL" },
|
||||
{ tagline:"Composant manquant.", explanation:"La BOM indique un composant à cet emplacement. Mais le pick-and-place a oublié de le poser.", footnote:"Erreur 404 · Ref: U404 · Empreinte : NOT_POPULATED" },
|
||||
{ tagline:"Court-circuit temporel.", explanation:"Cette page existera peut-être dans le futur. Ou elle a existé dans le passé. La relativité est floue là-dessus.", footnote:"Erreur 404 · Référentiel : indéterminé · Temps propre : NaN" },
|
||||
{ tagline:"Masse flottante.", explanation:"Sans référence de masse, impossible de mesurer quoi que ce soit. Cette URL flotte dans le vide intersidéral du serveur.", footnote:"Erreur 404 · GND : déconnecté · Potentiel : indéfini" },
|
||||
{ tagline:"Intrication rompue.", explanation:"Cette page était intriquée avec une autre. Quelqu'un a mesuré l'autre page et celle-ci s'est décohérée.", footnote:"Erreur 404 · Bell inequality : violated · EPR : paradox" },
|
||||
{ tagline:"Fréquence hors bande.", explanation:"Votre requête émet sur une fréquence que notre serveur ne peut pas capter. Essayez de réaccorder votre URL.", footnote:"Erreur 404 · Bande passante : 0 Hz · Filtre : rejet total" },
|
||||
{ tagline:"Soudure froide.", explanation:"La connexion entre votre navigateur et cette page présente une soudure froide. Contact intermittent. Page absente.", footnote:"Erreur 404 · Joint : CMS · Diagnostic : reflow nécessaire" }
|
||||
];
|
||||
|
||||
let currentIndex = Math.floor(Math.random() * messages.length);
|
||||
|
||||
function updateMessage() {
|
||||
const msg = messages[currentIndex];
|
||||
const tagline = document.getElementById('tagline');
|
||||
const explanation = document.getElementById('explanation');
|
||||
const footnote = document.getElementById('footnote');
|
||||
|
||||
tagline.style.opacity = '0';
|
||||
explanation.style.opacity = '0';
|
||||
footnote.style.opacity = '0';
|
||||
|
||||
let i = Math.floor(Math.random() * messages.length);
|
||||
function show() {
|
||||
const m = messages[i];
|
||||
['tagline','explanation','footnote'].forEach(id => { document.getElementById(id).style.opacity='0'; });
|
||||
setTimeout(() => {
|
||||
tagline.textContent = msg.tagline;
|
||||
explanation.textContent = msg.explanation;
|
||||
footnote.textContent = msg.footnote;
|
||||
tagline.style.opacity = '1';
|
||||
explanation.style.opacity = '1';
|
||||
footnote.style.opacity = '1';
|
||||
document.getElementById('tagline').textContent = m.tagline;
|
||||
document.getElementById('explanation').textContent = m.explanation;
|
||||
document.getElementById('footnote').textContent = m.footnote;
|
||||
['tagline','explanation','footnote'].forEach(id => { document.getElementById(id).style.opacity='1'; });
|
||||
}, 400);
|
||||
|
||||
currentIndex = (currentIndex + 1) % messages.length;
|
||||
i = (i + 1) % messages.length;
|
||||
}
|
||||
|
||||
updateMessage();
|
||||
setInterval(updateMessage, 10000);
|
||||
show(); setInterval(show, 10000);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page-404 {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
}
|
||||
|
||||
.atom {
|
||||
width: clamp(200px, 40vw, 280px);
|
||||
height: auto;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: clamp(72px, 15vw, 140px);
|
||||
font-weight: 700;
|
||||
color: var(--text, #f4eee3);
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
letter-spacing: -4px;
|
||||
}
|
||||
|
||||
.zero {
|
||||
display: inline-block;
|
||||
animation: glitch 4s ease-in-out infinite;
|
||||
color: var(--electric, #5bd1d8);
|
||||
}
|
||||
|
||||
@keyframes glitch {
|
||||
0%, 88%, 100% { opacity: 1; transform: none; }
|
||||
90% { opacity: 0.5; transform: translateX(-3px) skewX(-3deg); color: var(--trace-magenta, #d89b63); }
|
||||
92% { opacity: 0.8; transform: translateX(2px) skewX(1deg); }
|
||||
94% { opacity: 0.3; transform: translateX(-1px); color: var(--accent, #f1c27a); }
|
||||
96% { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-family: 'Fraunces', serif;
|
||||
font-size: clamp(20px, 3.5vw, 28px);
|
||||
color: var(--accent, #f1c27a);
|
||||
margin: 12px 0 20px;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
|
||||
.explanation {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 14px;
|
||||
color: var(--text-muted, #d6cbb9);
|
||||
line-height: 1.8;
|
||||
max-width: 480px;
|
||||
margin: 0 0 32px;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
|
||||
.actions { margin-bottom: 40px; }
|
||||
|
||||
.btn-home {
|
||||
display: inline-block;
|
||||
padding: 12px 28px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--bg, #0b0a09);
|
||||
background: var(--accent, #f1c27a);
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-home:hover {
|
||||
background: var(--electric, #5bd1d8);
|
||||
box-shadow: 0 0 20px rgba(91, 209, 216, 0.3);
|
||||
}
|
||||
|
||||
.footnote {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim, #b6a894);
|
||||
opacity: 0.5;
|
||||
letter-spacing: 0.5px;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
.page-404 { min-height:100vh; display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center; padding:48px 24px; }
|
||||
.atom { width:clamp(200px,40vw,280px); height:auto; margin-bottom:24px; }
|
||||
h1 { font-family:'IBM Plex Mono',monospace; font-size:clamp(72px,15vw,140px); font-weight:700; color:var(--text,#f4eee3); margin:0; line-height:1; letter-spacing:-4px; }
|
||||
.zero { display:inline-block; animation:glitch 4s ease-in-out infinite; color:var(--electric,#5bd1d8); }
|
||||
@keyframes glitch { 0%,88%,100%{opacity:1;transform:none} 90%{opacity:.5;transform:translateX(-3px) skewX(-3deg);color:var(--trace-magenta,#d89b63)} 92%{opacity:.8;transform:translateX(2px) skewX(1deg)} 94%{opacity:.3;transform:translateX(-1px);color:var(--accent,#f1c27a)} 96%{opacity:1;transform:none} }
|
||||
.tagline { font-family:'Fraunces',serif; font-size:clamp(20px,3.5vw,28px); color:var(--accent,#f1c27a); margin:12px 0 20px; transition:opacity .4s ease; }
|
||||
.explanation { font-family:'IBM Plex Mono',monospace; font-size:14px; color:var(--text-muted,#d6cbb9); line-height:1.8; max-width:480px; margin:0 0 32px; transition:opacity .4s ease; }
|
||||
.actions { margin-bottom:40px; }
|
||||
.btn-home { display:inline-block; padding:12px 28px; font-family:'IBM Plex Mono',monospace; font-size:14px; font-weight:500; color:var(--bg,#0b0a09); background:var(--accent,#f1c27a); text-decoration:none; border-radius:4px; transition:all .2s; }
|
||||
.btn-home:hover { background:var(--electric,#5bd1d8); box-shadow:0 0 20px rgba(91,209,216,.3); }
|
||||
.footnote { font-family:'IBM Plex Mono',monospace; font-size:11px; color:var(--text-dim,#b6a894); opacity:.5; letter-spacing:.5px; transition:opacity .4s ease; }
|
||||
</style>
|
||||
|
||||
+67
-109
@@ -1,26 +1,15 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
|
||||
interface LeadSubmission {
|
||||
interface ContactSubmission {
|
||||
name: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
organization?: string;
|
||||
needType?: string;
|
||||
message?: string;
|
||||
sourceChannel?: string;
|
||||
sourceDetail?: string;
|
||||
pagePath?: string;
|
||||
needType?: string;
|
||||
}
|
||||
|
||||
interface FrappePayload {
|
||||
first_name: string;
|
||||
last_name?: string;
|
||||
email: string;
|
||||
mobile_no?: string;
|
||||
organization?: string;
|
||||
custom_source_channel: string;
|
||||
custom_source_detail?: string;
|
||||
custom_need_type: string;
|
||||
custom_segment: string;
|
||||
}
|
||||
|
||||
// Strict email validation (RFC 5322 simplified)
|
||||
@@ -41,7 +30,6 @@ function isRateLimited(ip: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allowed origins for CORS
|
||||
const ALLOWED_ORIGINS = new Set([
|
||||
'https://www.lelectronrare.fr',
|
||||
'https://lelectronrare.fr',
|
||||
@@ -57,10 +45,13 @@ function corsHeaders(origin: string | null): Record<string, string> {
|
||||
return headers;
|
||||
}
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return String(s).replace(/[<>&"']/g, c => ({ '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' }[c] as string));
|
||||
}
|
||||
|
||||
export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||
const origin = request.headers.get('origin');
|
||||
|
||||
// CORS check — reject unknown origins (allow null for same-origin)
|
||||
if (origin && !ALLOWED_ORIGINS.has(origin)) {
|
||||
return new Response(JSON.stringify({ error: 'forbidden' }), {
|
||||
status: 403,
|
||||
@@ -68,7 +59,6 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
const ip = clientAddress || request.headers.get('x-forwarded-for') || 'unknown';
|
||||
if (isRateLimited(ip)) {
|
||||
return new Response(JSON.stringify({ error: 'too many requests' }), {
|
||||
@@ -77,20 +67,11 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||
});
|
||||
}
|
||||
|
||||
const FRAPPE_URL = import.meta.env.FRAPPE_URL || process.env.FRAPPE_URL;
|
||||
const FRAPPE_API_KEY = import.meta.env.FRAPPE_API_KEY || process.env.FRAPPE_API_KEY;
|
||||
const FRAPPE_API_SECRET = import.meta.env.FRAPPE_API_SECRET || process.env.FRAPPE_API_SECRET;
|
||||
const CRM_FALLBACK_URL = import.meta.env.CRM_FALLBACK_URL || process.env.CRM_FALLBACK_URL;
|
||||
const MAIL_API_URL = process.env.MAIL_API_URL || 'http://mail-api:3200/api/mail/send';
|
||||
const MAIL_FROM = process.env.MAIL_FROM || 'contact@lelectronrare.fr';
|
||||
const MAIL_TO = process.env.MAIL_TO || 'contact@lelectronrare.fr';
|
||||
|
||||
if (!FRAPPE_URL || !FRAPPE_API_KEY || !FRAPPE_API_SECRET) {
|
||||
console.error('[submit-lead] Missing Frappe env vars');
|
||||
return new Response(JSON.stringify({ error: 'server misconfigured' }), {
|
||||
status: 500,
|
||||
headers: corsHeaders(origin),
|
||||
});
|
||||
}
|
||||
|
||||
let body: LeadSubmission;
|
||||
let body: ContactSubmission;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
@@ -100,9 +81,13 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
const name = typeof body.name === 'string' ? body.name.trim() : '';
|
||||
const email = typeof body.email === 'string' ? body.email.trim().toLowerCase() : '';
|
||||
const phone = typeof body.phone === 'string' ? body.phone.trim() : '';
|
||||
const organization = typeof body.organization === 'string' ? body.organization.trim() : '';
|
||||
const needType = typeof body.needType === 'string' ? body.needType.trim() : '';
|
||||
const message = typeof body.message === 'string' ? body.message.trim() : '';
|
||||
const pagePath = typeof body.pagePath === 'string' ? body.pagePath.trim() : '';
|
||||
|
||||
if (!name || name.length < 2 || name.length > 200) {
|
||||
return new Response(JSON.stringify({ error: 'name must be 2-200 characters' }), {
|
||||
@@ -110,108 +95,81 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||
headers: corsHeaders(origin),
|
||||
});
|
||||
}
|
||||
|
||||
if (!email || !EMAIL_RE.test(email)) {
|
||||
return new Response(JSON.stringify({ error: 'valid email required' }), {
|
||||
status: 400,
|
||||
headers: corsHeaders(origin),
|
||||
});
|
||||
}
|
||||
|
||||
// Split full name into first/last
|
||||
const nameParts = name.split(/\s+/);
|
||||
const firstName = nameParts[0];
|
||||
const lastName = nameParts.length > 1 ? nameParts.slice(1).join(' ') : undefined;
|
||||
|
||||
const sourceChannel = body.sourceChannel || 'site';
|
||||
const needType = body.needType || 'technique';
|
||||
|
||||
const segment =
|
||||
needType === 'formation'
|
||||
? 'formation'
|
||||
: sourceChannel === 'fat' || sourceChannel === 'hemisphere'
|
||||
? 'culture'
|
||||
: 'industrie';
|
||||
|
||||
const frappePayload: FrappePayload = {
|
||||
first_name: firstName,
|
||||
email,
|
||||
custom_source_channel: sourceChannel,
|
||||
custom_need_type: needType,
|
||||
custom_segment: segment,
|
||||
};
|
||||
|
||||
if (lastName) frappePayload.last_name = lastName;
|
||||
if (body.phone) frappePayload.mobile_no = body.phone;
|
||||
if (body.organization) frappePayload.organization = body.organization;
|
||||
if (body.sourceDetail || body.pagePath) {
|
||||
frappePayload.custom_source_detail = body.sourceDetail || body.pagePath;
|
||||
if (message.length > 5000) {
|
||||
return new Response(JSON.stringify({ error: 'message too long (max 5000)' }), {
|
||||
status: 400,
|
||||
headers: corsHeaders(origin),
|
||||
});
|
||||
}
|
||||
|
||||
console.info('[submit-lead] Submitting lead', { email, sourceChannel, needType, segment });
|
||||
const subject = `[lelectronrare] ${needType || 'demande'} — ${name}`;
|
||||
|
||||
const FRAPPE_HOST = import.meta.env.FRAPPE_HOST_HEADER || process.env.FRAPPE_HOST_HEADER || '';
|
||||
const text = `Nouveau message depuis lelectronrare.fr
|
||||
|
||||
Nom : ${name}
|
||||
Email : ${email}
|
||||
Téléphone : ${phone || '-'}
|
||||
Organisation: ${organization || '-'}
|
||||
Type : ${needType || '-'}
|
||||
Page : ${pagePath || '-'}
|
||||
IP : ${ip}
|
||||
|
||||
Message :
|
||||
${message || '(vide)'}
|
||||
`;
|
||||
|
||||
const html = `<!DOCTYPE html><html><body style="font-family:system-ui,-apple-system,sans-serif;max-width:600px;margin:0 auto;padding:20px;color:#222;">
|
||||
<h2 style="color:#5bd1d8;border-bottom:1px solid #eee;padding-bottom:8px;">Nouveau message — lelectronrare.fr</h2>
|
||||
<table style="border-collapse:collapse;width:100%;font-size:14px;">
|
||||
<tr><td style="padding:6px;"><b>Nom</b></td><td style="padding:6px;">${escapeHtml(name)}</td></tr>
|
||||
<tr><td style="padding:6px;"><b>Email</b></td><td style="padding:6px;"><a href="mailto:${escapeHtml(email)}">${escapeHtml(email)}</a></td></tr>
|
||||
<tr><td style="padding:6px;"><b>Téléphone</b></td><td style="padding:6px;">${escapeHtml(phone || '-')}</td></tr>
|
||||
<tr><td style="padding:6px;"><b>Organisation</b></td><td style="padding:6px;">${escapeHtml(organization || '-')}</td></tr>
|
||||
<tr><td style="padding:6px;"><b>Type</b></td><td style="padding:6px;">${escapeHtml(needType || '-')}</td></tr>
|
||||
<tr><td style="padding:6px;color:#999;font-size:11px;"><b>IP</b></td><td style="padding:6px;color:#999;font-size:11px;">${escapeHtml(ip)}</td></tr>
|
||||
</table>
|
||||
<h3 style="margin-top:20px;font-size:14px;color:#666;">Message</h3>
|
||||
<pre style="background:#f5f5f5;padding:12px;border-radius:6px;white-space:pre-wrap;font-family:inherit;font-size:13px;line-height:1.5;">${escapeHtml(message || '(vide)')}</pre>
|
||||
<p style="margin-top:20px;font-size:11px;color:#aaa;">Reply-To pointe vers l'email du visiteur — répondre directement.</p>
|
||||
</body></html>`;
|
||||
|
||||
try {
|
||||
const frappeHeaders: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `token ${FRAPPE_API_KEY}:${FRAPPE_API_SECRET}`,
|
||||
};
|
||||
if (FRAPPE_HOST) frappeHeaders['Host'] = FRAPPE_HOST;
|
||||
|
||||
const resp = await fetch(`${FRAPPE_URL}/api/resource/CRM Lead`, {
|
||||
const resp = await fetch(MAIL_API_URL, {
|
||||
method: 'POST',
|
||||
headers: frappeHeaders,
|
||||
body: JSON.stringify(frappePayload),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
from: MAIL_FROM,
|
||||
to: MAIL_TO,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
replyTo: `${name.replace(/[<>"]/g, '')} <${email}>`,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
// Log status only — never log response body (may contain secrets)
|
||||
console.error('[submit-lead] Frappe returned', resp.status);
|
||||
|
||||
// Dual-write fallback
|
||||
if (CRM_FALLBACK_URL) {
|
||||
console.info('[submit-lead] Attempting CRM fallback');
|
||||
try {
|
||||
const fallbackResp = await fetch(`${CRM_FALLBACK_URL}/api/intake/lead`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
email,
|
||||
phone: body.phone,
|
||||
organization: body.organization,
|
||||
sourceChannel,
|
||||
needType,
|
||||
}),
|
||||
});
|
||||
if (fallbackResp.ok) {
|
||||
return new Response(JSON.stringify({ ok: true, via: 'fallback' }), {
|
||||
status: 201,
|
||||
headers: corsHeaders(origin),
|
||||
});
|
||||
}
|
||||
console.error('[submit-lead] Fallback returned', fallbackResp.status);
|
||||
} catch {
|
||||
console.error('[submit-lead] Fallback network error');
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ error: 'submission failed' }), {
|
||||
console.error('[submit-lead] mail-api returned', resp.status);
|
||||
return new Response(JSON.stringify({ error: 'send failed' }), {
|
||||
status: 502,
|
||||
headers: corsHeaders(origin),
|
||||
});
|
||||
}
|
||||
|
||||
console.info('[submit-lead] Lead created');
|
||||
console.info('[submit-lead] Mail relayed via mail-api', { needType, hasOrg: !!organization });
|
||||
return new Response(JSON.stringify({ ok: true }), {
|
||||
status: 201,
|
||||
headers: corsHeaders(origin),
|
||||
});
|
||||
} catch {
|
||||
console.error('[submit-lead] Network error');
|
||||
return new Response(JSON.stringify({ error: 'internal error' }), {
|
||||
status: 500,
|
||||
} catch (err) {
|
||||
console.error('[submit-lead] mail-api unreachable', err instanceof Error ? err.message : 'unknown');
|
||||
return new Response(JSON.stringify({ error: 'mail service unreachable' }), {
|
||||
status: 502,
|
||||
headers: corsHeaders(origin),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
---
|
||||
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Conception électronique | L'électron rare</title>
|
||||
<meta name="description" content="Conception de systèmes électroniques spécifiques : schéma, PCB, firmware, prototypage et validation. Du diagnostic à la mission complète." />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { background: #06080b; color: #fff; }
|
||||
body { background: #06080b; min-height: 100vh; font-family: 'Courier New', monospace; }
|
||||
.site-hdr { position: fixed; top: 0; left: 0; right: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; padding: 16px clamp(20px, 3vw, 40px); background: rgba(6,8,11,0.85); backdrop-filter: blur(12px); border-bottom: 1px solid rgba(255,255,255,0.05); }
|
||||
.hdr-left { display: flex; align-items: center; gap: 10px; }
|
||||
.hdr-left a { text-decoration: none; display: flex; align-items: center; gap: 10px; }
|
||||
.hdr-logo { width: 36px; height: auto; filter: drop-shadow(0 0 12px rgba(91,209,216,0.3)); }
|
||||
.hdr-name { font-size: 16px; font-weight: 800; color: rgba(255,255,255,0.8); letter-spacing: 0.04em; }
|
||||
.hdr-nav { display: flex; align-items: center; gap: 8px; }
|
||||
.hdr-link { padding: 8px 16px; color: rgba(255,255,255,0.6); font-size: 12px; font-weight: 600; text-decoration: none; letter-spacing: 0.04em; transition: color 0.2s; }
|
||||
.hdr-link:hover { color: #5bd1d8; }
|
||||
.hdr-link.active { color: #5bd1d8; }
|
||||
.hdr-cta { padding: 8px 20px; border-radius: 6px; background: rgba(91,209,216,0.15); border: 1px solid rgba(91,209,216,0.3); color: #5bd1d8; font-size: 12px; font-weight: 700; text-decoration: none; letter-spacing: 0.05em; }
|
||||
.burger { display: none; background: none; border: none; color: #fff; font-size: 24px; cursor: pointer; padding: 8px; }
|
||||
.mobile-menu { display: none; position: fixed; inset: 0; z-index: 100; background: rgba(6,8,11,0.95); backdrop-filter: blur(16px); flex-direction: column; align-items: center; justify-content: center; gap: 24px; }
|
||||
.mobile-menu.open { display: flex; }
|
||||
.mobile-menu a { font-size: 20px; font-weight: 700; color: rgba(255,255,255,0.8); text-decoration: none; letter-spacing: 0.05em; padding: 12px 24px; }
|
||||
.mobile-menu a:hover { color: #5bd1d8; }
|
||||
.mobile-menu .close-btn { position: absolute; top: 20px; right: 20px; background: none; border: none; color: #fff; font-size: 28px; cursor: pointer; }
|
||||
@media (max-width: 768px) {
|
||||
.hdr-nav { display: none; }
|
||||
.burger { display: block; }
|
||||
}
|
||||
.content { max-width: 800px; margin: 0 auto; padding: 120px 24px 80px; }
|
||||
h1 { font-size: clamp(28px, 4vw, 48px); font-weight: 800; color: #5bd1d8; margin-bottom: 24px; letter-spacing: -0.02em; }
|
||||
h2 { font-size: 18px; font-weight: 700; color: #fff; margin: 40px 0 16px; text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
p { font-size: 14px; color: rgba(255,255,255,0.6); line-height: 1.8; margin-bottom: 16px; }
|
||||
.card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 24px; margin-bottom: 16px; }
|
||||
.card-t { font-size: 16px; font-weight: 700; color: #fff; margin-bottom: 8px; }
|
||||
.card-pct { font-size: 28px; font-weight: 800; margin-bottom: 4px; }
|
||||
.card-time { font-size: 11px; color: rgba(255,255,255,0.35); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 12px; }
|
||||
.card-desc { font-size: 13px; color: rgba(255,255,255,0.5); line-height: 1.6; margin-bottom: 12px; }
|
||||
.card-del { font-size: 12px; color: rgba(255,255,255,0.4); }
|
||||
.cta-box { margin-top: 48px; text-align: center; }
|
||||
.cta-btn { display: inline-block; padding: 14px 32px; border-radius: 8px; background: #5bd1d8; color: #06080b; font-weight: 700; font-size: 14px; text-decoration: none; letter-spacing: 0.04em; }
|
||||
.cta-btn:hover { background: #7de0e6; }
|
||||
.footer { text-align: center; padding: 40px 24px; font-size: 10px; color: rgba(255,255,255,0.15); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-hdr">
|
||||
<div class="hdr-left"><a href="/"><img src="/assets/brand/logo-mark.svg" alt="" class="hdr-logo" /><span class="hdr-name">L'électron rare</span></a></div>
|
||||
<nav class="hdr-nav">
|
||||
<a href="/conception/" class="hdr-link active">Conception</a>
|
||||
<a href="/formation/" class="hdr-link">Formation</a>
|
||||
<a href="/consulting/" class="hdr-link">Consulting</a>
|
||||
<a href="/contact/" class="hdr-cta">Contact</a>
|
||||
</nav>
|
||||
<button class="burger" id="burger-btn" aria-label="Menu">☰</button>
|
||||
</header>
|
||||
<div class="mobile-menu" id="mobile-menu">
|
||||
<button class="close-btn" id="close-menu">✕</button>
|
||||
<a href="/">Accueil</a>
|
||||
<a href="/conception/">Conception</a>
|
||||
<a href="/formation/">Formation</a>
|
||||
<a href="/consulting/">Consulting</a>
|
||||
<a href="/contact/">Contact</a>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('burger-btn')?.addEventListener('click',()=>document.getElementById('mobile-menu')?.classList.add('open'));
|
||||
document.getElementById('close-menu')?.addEventListener('click',()=>document.getElementById('mobile-menu')?.classList.remove('open'));
|
||||
</script>
|
||||
|
||||
<main class="content">
|
||||
<h1>Conception électronique</h1>
|
||||
<p>L'Électron Rare conçoit, met au point et fiabilise des systèmes électroniques spécifiques, des automatismes et des ensembles liés à l'énergie, au stockage et à l'optimisation terrain.</p>
|
||||
|
||||
<h2>Formats de mission</h2>
|
||||
|
||||
<div class="card">
|
||||
<p class="card-pct" style="color:#5bd1d8">28%</p>
|
||||
<p class="card-t">Diagnostic</p>
|
||||
<p class="card-time">1 à 2 semaines</p>
|
||||
<p class="card-desc">Analyser votre besoin, identifier les sous-systèmes critiques et livrer une carte de faisabilité.</p>
|
||||
<p class="card-del">Livrables : analyse de faisabilité · risques identifiés · plan d'action</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p class="card-pct" style="color:#a78bfa">55%</p>
|
||||
<p class="card-t">Prototype</p>
|
||||
<p class="card-time">4 à 6 semaines</p>
|
||||
<p class="card-desc">Concevoir et tester un prototype fonctionnel : schéma, PCB, firmware, mesures et validation.</p>
|
||||
<p class="card-del">Livrables : prototype testé · mesures documentées · go/no-go argumenté</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p class="card-pct" style="color:#f59e0b">100%</p>
|
||||
<p class="card-t">Mission complète</p>
|
||||
<p class="card-time">2 à 6 mois</p>
|
||||
<p class="card-desc">Développement complet jusqu'au produit industrialisable : itérations, tests terrain, documentation et transfert.</p>
|
||||
<p class="card-del">Livrables : livrables par lot · dossier de production · transfert clé en main</p>
|
||||
</div>
|
||||
|
||||
<h2>Cas concrets</h2>
|
||||
|
||||
<div class="card">
|
||||
<p class="card-t" style="color:#5bd1d8">Battery Parallelator — KXKM</p>
|
||||
<p class="card-desc">Système de gestion parallèle de 4 batteries LiFePO4 pour le spectacle vivant. Conception complète KiCad : carte BMU, 4 cartes Mosfet Switch, carte I2C Repeater. Firmware ESP32, supervision I2C, protection thermique.</p>
|
||||
<p class="card-del" style="color:#5bd1d8;">→ 8h autonomie · OTA · 0 panne sur 2 saisons</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p class="card-t" style="color:#a78bfa">HypnoLED — Contrôle LED embarqué</p>
|
||||
<p class="card-desc">Carte de contrôle LED haute densité pour installations scéniques. Architecture multi-cartes, protocole DMX/Art-Net, topologie bus, alimentation distribuée. PCB 4 couches KiCad.</p>
|
||||
<p class="card-del" style="color:#a78bfa;">→ 512 canaux DMX · Latence <1ms · Déployé en tournée</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p class="card-t" style="color:#34d399">Télémétrie ESP32 — Monitoring énergie</p>
|
||||
<p class="card-desc">Système de supervision terrain avec ESP32 : mesure courant/tension, transmission MQTT, dashboard temps réel. Capteurs INA226, connectivité WiFi, boîtier IP65.</p>
|
||||
<p class="card-del" style="color:#34d399;">→ Autonomie 6 mois sur batterie · Dashboard Grafana</p>
|
||||
</div>
|
||||
|
||||
<h2>Domaines d'intervention</h2>
|
||||
<p>Électronique spécifique · Instrumentation et automatisme · Énergie et stockage · Dispositifs pour le réel · Montages multi-techniques avec partenaires.</p>
|
||||
|
||||
<div class="cta-box">
|
||||
<a href="/contact/?from=conception" class="cta-btn">Discuter de votre projet</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="footer">© 2026 Clément Saillant — L'électron rare · <a href="https://blog.saillant.cc" target="_blank" rel="noopener" style="color:rgba(255,255,255,0.3);">Blog</a> · <a href="https://fr.linkedin.com/in/electron-rare" target="_blank" rel="noopener" style="color:rgba(255,255,255,0.3);">LinkedIn</a> · <a href="https://github.com/electron-rare/" target="_blank" rel="noopener" style="color:rgba(255,255,255,0.3);">GitHub</a></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
---
|
||||
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Consulting technique | L'électron rare</title>
|
||||
<meta name="description" content="Consulting en systèmes électroniques : audit, diagnostic, transfert de compétences, accompagnement technique terrain." />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { background: #06080b; color: #fff; }
|
||||
body { background: #06080b; min-height: 100vh; font-family: 'Courier New', monospace; }
|
||||
.site-hdr { position: fixed; top: 0; left: 0; right: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; padding: 16px clamp(20px, 3vw, 40px); background: rgba(6,8,11,0.85); backdrop-filter: blur(12px); border-bottom: 1px solid rgba(255,255,255,0.05); }
|
||||
.hdr-left { display: flex; align-items: center; gap: 10px; }
|
||||
.hdr-left a { text-decoration: none; display: flex; align-items: center; gap: 10px; }
|
||||
.hdr-logo { width: 36px; height: auto; filter: drop-shadow(0 0 12px rgba(91,209,216,0.3)); }
|
||||
.hdr-name { font-size: 16px; font-weight: 800; color: rgba(255,255,255,0.8); letter-spacing: 0.04em; }
|
||||
.hdr-nav { display: flex; align-items: center; gap: 8px; }
|
||||
.hdr-link { padding: 8px 16px; color: rgba(255,255,255,0.6); font-size: 12px; font-weight: 600; text-decoration: none; letter-spacing: 0.04em; transition: color 0.2s; }
|
||||
.hdr-link:hover { color: #5bd1d8; }
|
||||
.hdr-link.active { color: #5bd1d8; }
|
||||
.hdr-cta { padding: 8px 20px; border-radius: 6px; background: rgba(91,209,216,0.15); border: 1px solid rgba(91,209,216,0.3); color: #5bd1d8; font-size: 12px; font-weight: 700; text-decoration: none; letter-spacing: 0.05em; }
|
||||
.burger { display: none; background: none; border: none; color: #fff; font-size: 24px; cursor: pointer; padding: 8px; }
|
||||
.mobile-menu { display: none; position: fixed; inset: 0; z-index: 100; background: rgba(6,8,11,0.95); backdrop-filter: blur(16px); flex-direction: column; align-items: center; justify-content: center; gap: 24px; }
|
||||
.mobile-menu.open { display: flex; }
|
||||
.mobile-menu a { font-size: 20px; font-weight: 700; color: rgba(255,255,255,0.8); text-decoration: none; letter-spacing: 0.05em; padding: 12px 24px; }
|
||||
.mobile-menu a:hover { color: #5bd1d8; }
|
||||
.mobile-menu .close-btn { position: absolute; top: 20px; right: 20px; background: none; border: none; color: #fff; font-size: 28px; cursor: pointer; }
|
||||
@media (max-width: 768px) { .hdr-nav { display: none; } .burger { display: block; } }
|
||||
.content { max-width: 800px; margin: 0 auto; padding: 120px 24px 80px; }
|
||||
h1 { font-size: clamp(28px, 4vw, 48px); font-weight: 800; color: #a78bfa; margin-bottom: 24px; letter-spacing: -0.02em; }
|
||||
h2 { font-size: 18px; font-weight: 700; color: #fff; margin: 40px 0 16px; text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
p { font-size: 14px; color: rgba(255,255,255,0.6); line-height: 1.8; margin-bottom: 16px; }
|
||||
.card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 24px; margin-bottom: 16px; }
|
||||
.card-t { font-size: 16px; font-weight: 700; color: #fff; margin-bottom: 8px; }
|
||||
.card-desc { font-size: 13px; color: rgba(255,255,255,0.5); line-height: 1.6; }
|
||||
.chip { display: inline-block; font-size: 11px; padding: 4px 12px; border-radius: 20px; border: 1px solid; margin: 4px 4px 4px 0; letter-spacing: 0.04em; }
|
||||
.families { margin: 20px 0 32px; }
|
||||
.cta-box { margin-top: 48px; text-align: center; }
|
||||
.cta-btn { display: inline-block; padding: 14px 32px; border-radius: 8px; background: #a78bfa; color: #06080b; font-weight: 700; font-size: 14px; text-decoration: none; letter-spacing: 0.04em; }
|
||||
.cta-btn:hover { background: #c4b5fd; }
|
||||
.footer { text-align: center; padding: 40px 24px; font-size: 10px; color: rgba(255,255,255,0.15); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-hdr">
|
||||
<div class="hdr-left"><a href="/"><img src="/assets/brand/logo-mark.svg" alt="" class="hdr-logo" /><span class="hdr-name">L'électron rare</span></a></div>
|
||||
<nav class="hdr-nav">
|
||||
<a href="/conception/" class="hdr-link">Conception</a>
|
||||
<a href="/formation/" class="hdr-link">Formation</a>
|
||||
<a href="/consulting/" class="hdr-link active">Consulting</a>
|
||||
<a href="/contact/" class="hdr-cta">Contact</a>
|
||||
</nav>
|
||||
<button class="burger" id="burger-btn" aria-label="Menu">☰</button>
|
||||
</header>
|
||||
<div class="mobile-menu" id="mobile-menu">
|
||||
<button class="close-btn" id="close-menu">✕</button>
|
||||
<a href="/">Accueil</a><a href="/conception/">Conception</a><a href="/formation/">Formation</a><a href="/consulting/">Consulting</a><a href="/contact/">Contact</a>
|
||||
</div>
|
||||
<script>document.getElementById('burger-btn')?.addEventListener('click',()=>document.getElementById('mobile-menu')?.classList.add('open'));document.getElementById('close-menu')?.addEventListener('click',()=>document.getElementById('mobile-menu')?.classList.remove('open'));</script>
|
||||
|
||||
<main class="content">
|
||||
<h1>Consulting technique</h1>
|
||||
<p>Je suis Clément Saillant, fondateur de L'électron rare. Mon rôle n'est pas de vendre "un peu d'embarqué" en plus, mais de rendre possible le bon système pour le besoin réel : électronique, automatisme, énergie, stockage, optimisation, diagnostic, prototype, reprise d'existant ou fiabilisation.</p>
|
||||
|
||||
<h2>Familles d'intervention</h2>
|
||||
<div class="families">
|
||||
<span class="chip" style="color:#5bd1d8;border-color:#5bd1d833">Électronique spécifique</span>
|
||||
<span class="chip" style="color:#a78bfa;border-color:#a78bfa33">Instrumentation & Automatisme</span>
|
||||
<span class="chip" style="color:#f472b6;border-color:#f472b633">Dispositifs pour le réel</span>
|
||||
<span class="chip" style="color:#34d399;border-color:#34d39933">Énergie & Stockage</span>
|
||||
<span class="chip" style="color:#fbbf24;border-color:#fbbf2433">Multi-techniques & Partenaires</span>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p class="card-t">Audit & Diagnostic</p>
|
||||
<p class="card-desc">Analyse de l'existant, identification des points critiques, recommandations chiffrées. Diagnostic terrain sur site industriel, automate, variateur, instrumentation.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p class="card-t">Accompagnement technique</p>
|
||||
<p class="card-desc">Pilotage de sous-traitance, revue de conception, validation de prototypes, suivi de production. Mobilisation de partenaires spécialisés quand le projet devient multi-technique.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p class="card-t">Transfert & Documentation</p>
|
||||
<p class="card-desc">Rédaction de dossiers techniques, procédures de maintenance, documentation de production. Transfert de compétences vers l'équipe interne.</p>
|
||||
</div>
|
||||
|
||||
<h2>Cas concret</h2>
|
||||
<div class="card">
|
||||
<p class="card-t">Mise en production et correction V3.2 KXKM</p>
|
||||
<p class="card-desc">Mise en production du hardware ESP32, correction des bugs V3.2, passage STM32F030. Tests terrain, validation embarquée, documentation technique.</p>
|
||||
<p class="card-desc" style="color:#a78bfa;margin-top:8px;">→ Production V3.2 livrée · Migration STM32F030 validée</p>
|
||||
</div>
|
||||
|
||||
<h2>À propos</h2>
|
||||
<div class="card">
|
||||
<p class="card-t">Clément Saillant — 10 ans d'expérience</p>
|
||||
<p class="card-desc">Cyborg en systèmes électroniques, j'interviens depuis 10 ans sur des projets allant du spectacle vivant à l'industrie automatisée, en passant par l'énergie et la formation. Contributeur open-source (KiCad, KXKM), ambassadeur IA appliquée à l'électronique.</p>
|
||||
<p class="card-desc">Secteurs : industries créatives, ferroviaire, automatisme industriel, éducation, énergie/stockage.</p>
|
||||
</div>
|
||||
|
||||
<div class="cta-box">
|
||||
<a href="/contact/?from=consulting" class="cta-btn">Discuter de votre besoin</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="footer">
|
||||
© 2026 Clément Saillant — L'électron rare ·
|
||||
<a href="https://blog.saillant.cc" target="_blank" rel="noopener" style="color:rgba(255,255,255,0.3);">Blog</a> ·
|
||||
<a href="https://fr.linkedin.com/in/electron-rare" target="_blank" rel="noopener" style="color:rgba(255,255,255,0.3);">LinkedIn</a> ·
|
||||
<a href="https://github.com/electron-rare/" target="_blank" rel="noopener" style="color:rgba(255,255,255,0.3);">GitHub</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
---
|
||||
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Contact | L'électron rare</title>
|
||||
<meta name="description" content="Parlons de votre projet électronique. Premier échange sans engagement." />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { background: #06080b; color: #fff; }
|
||||
body { background: #06080b; min-height: 100vh; font-family: 'Courier New', monospace; }
|
||||
.site-hdr { position: fixed; top: 0; left: 0; right: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; padding: 16px clamp(20px, 3vw, 40px); background: rgba(6,8,11,0.85); backdrop-filter: blur(12px); border-bottom: 1px solid rgba(255,255,255,0.05); }
|
||||
.hdr-left { display: flex; align-items: center; gap: 10px; }
|
||||
.hdr-left a { text-decoration: none; display: flex; align-items: center; gap: 10px; }
|
||||
.hdr-logo { width: 36px; height: auto; filter: drop-shadow(0 0 12px rgba(91,209,216,0.3)); }
|
||||
.hdr-name { font-size: 16px; font-weight: 800; color: rgba(255,255,255,0.8); letter-spacing: 0.04em; }
|
||||
.hdr-nav { display: flex; align-items: center; gap: 8px; }
|
||||
.hdr-link { padding: 8px 16px; color: rgba(255,255,255,0.6); font-size: 12px; font-weight: 600; text-decoration: none; letter-spacing: 0.04em; transition: color 0.2s; }
|
||||
.hdr-link:hover { color: #5bd1d8; }
|
||||
.hdr-cta { padding: 8px 20px; border-radius: 6px; background: rgba(91,209,216,0.15); border: 1px solid rgba(91,209,216,0.3); color: #5bd1d8; font-size: 12px; font-weight: 700; text-decoration: none; letter-spacing: 0.05em; }
|
||||
.hdr-cta.active { background: rgba(91,209,216,0.25); }
|
||||
.burger { display: none; background: none; border: none; color: #fff; font-size: 24px; cursor: pointer; padding: 8px; }
|
||||
.mobile-menu { display: none; position: fixed; inset: 0; z-index: 100; background: rgba(6,8,11,0.95); backdrop-filter: blur(16px); flex-direction: column; align-items: center; justify-content: center; gap: 24px; }
|
||||
.mobile-menu.open { display: flex; }
|
||||
.mobile-menu a { font-size: 20px; font-weight: 700; color: rgba(255,255,255,0.8); text-decoration: none; letter-spacing: 0.05em; padding: 12px 24px; }
|
||||
.mobile-menu .close-btn { position: absolute; top: 20px; right: 20px; background: none; border: none; color: #fff; font-size: 28px; cursor: pointer; }
|
||||
@media (max-width: 768px) { .hdr-nav { display: none; } .burger { display: block; } }
|
||||
.content { max-width: 600px; margin: 0 auto; padding: 120px 24px 80px; }
|
||||
h1 { font-size: clamp(28px, 4vw, 48px); font-weight: 800; color: #5bd1d8; margin-bottom: 12px; letter-spacing: -0.02em; }
|
||||
.sub { font-size: 14px; color: rgba(255,255,255,0.4); margin-bottom: 40px; }
|
||||
label { display: block; font-size: 11px; color: rgba(255,255,255,0.5); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 6px; margin-top: 20px; }
|
||||
input, select, textarea { width: 100%; padding: 12px 16px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.12); border-radius: 8px; color: #fff; font-family: 'Courier New', monospace; font-size: 14px; outline: none; transition: border-color 0.2s; }
|
||||
input:focus, select:focus, textarea:focus { border-color: #5bd1d8; }
|
||||
select { appearance: none; cursor: pointer; }
|
||||
select option { background: #06080b; color: #fff; }
|
||||
textarea { resize: vertical; min-height: 160px; }
|
||||
.consent { display: flex; align-items: flex-start; gap: 10px; margin-top: 20px; font-size: 11px; color: rgba(255,255,255,0.4); line-height: 1.5; }
|
||||
.consent input { width: auto; }
|
||||
.submit { display: block; width: 100%; margin-top: 28px; padding: 14px; border: none; border-radius: 8px; background: #5bd1d8; color: #06080b; font-family: 'Courier New', monospace; font-size: 14px; font-weight: 700; cursor: pointer; letter-spacing: 0.04em; transition: background 0.2s; }
|
||||
.submit:hover { background: #7de0e6; }
|
||||
.submit:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.success { display: none; text-align: center; padding: 40px 0; }
|
||||
.success h2 { font-size: 24px; color: #5bd1d8; margin-bottom: 12px; }
|
||||
.success p { font-size: 14px; color: rgba(255,255,255,0.5); }
|
||||
.error { display: none; color: #f66; font-size: 12px; margin-top: 12px; }
|
||||
.alt { margin-top: 40px; text-align: center; font-size: 12px; color: rgba(255,255,255,0.3); }
|
||||
.alt a { color: #5bd1d8; text-decoration: none; }
|
||||
.footer { text-align: center; padding: 40px 24px; font-size: 10px; color: rgba(255,255,255,0.15); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-hdr">
|
||||
<div class="hdr-left"><a href="/"><img src="/assets/brand/logo-mark.svg" alt="" class="hdr-logo" /><span class="hdr-name">L'électron rare</span></a></div>
|
||||
<nav class="hdr-nav">
|
||||
<a href="/conception/" class="hdr-link">Conception</a>
|
||||
<a href="/formation/" class="hdr-link">Formation</a>
|
||||
<a href="/consulting/" class="hdr-link">Consulting</a>
|
||||
<a href="/contact/" class="hdr-cta active">Contact</a>
|
||||
</nav>
|
||||
<button class="burger" id="burger-btn" aria-label="Menu">☰</button>
|
||||
</header>
|
||||
<div class="mobile-menu" id="mobile-menu">
|
||||
<button class="close-btn" id="close-menu">✕</button>
|
||||
<a href="/">Accueil</a><a href="/conception/">Conception</a><a href="/formation/">Formation</a><a href="/consulting/">Consulting</a><a href="/contact/">Contact</a>
|
||||
</div>
|
||||
<script>document.getElementById('burger-btn')?.addEventListener('click',()=>document.getElementById('mobile-menu')?.classList.add('open'));document.getElementById('close-menu')?.addEventListener('click',()=>document.getElementById('mobile-menu')?.classList.remove('open'));</script>
|
||||
|
||||
<main class="content">
|
||||
<h1>Parlons de votre projet</h1>
|
||||
<p class="sub">Décrivez votre besoin en quelques lignes — premier échange sans engagement.</p>
|
||||
|
||||
<form id="contact-form">
|
||||
<label for="name">Nom *</label>
|
||||
<input type="text" id="name" name="name" required placeholder="Votre nom" />
|
||||
|
||||
<label for="email">Email *</label>
|
||||
<input type="email" id="email" name="email" required placeholder="votre@email.com" />
|
||||
|
||||
<label for="phone">Téléphone</label>
|
||||
<input type="tel" id="phone" name="phone" placeholder="06 00 00 00 00" />
|
||||
|
||||
<label for="org">Organisation</label>
|
||||
<input type="text" id="org" name="organization" placeholder="Entreprise / École / Association" />
|
||||
|
||||
<label for="need">Type de besoin *</label>
|
||||
<select id="need" name="needType" required>
|
||||
<option value="">Choisir...</option>
|
||||
<option value="conception">Projet technique / Conception</option>
|
||||
<option value="formation">Formation</option>
|
||||
<option value="consulting">Consulting / Audit</option>
|
||||
<option value="mixte">Projet mixte</option>
|
||||
</select>
|
||||
|
||||
<script>
|
||||
// Préremplir selon la page d'origine (?from=conception|formation|consulting)
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const from = params.get('from');
|
||||
if (from) {
|
||||
const sel = document.getElementById('need');
|
||||
if (sel) sel.value = from;
|
||||
}
|
||||
</script>
|
||||
|
||||
<label for="message">Message *</label>
|
||||
<textarea id="message" name="message" required placeholder="Bonjour Clément,
|
||||
|
||||
J'ai un projet / besoin autour d'un système électronique spécifique.
|
||||
|
||||
Contexte :
|
||||
Ce que j'attends :
|
||||
Contraintes (délai, budget) :"></textarea>
|
||||
|
||||
<div class="consent">
|
||||
<input type="checkbox" id="consent" required />
|
||||
<label for="consent" style="margin:0;text-transform:none;font-size:11px;">J'accepte que mes données soient utilisées pour répondre à ma demande. Aucune donnée n'est transmise à des tiers.</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit" id="submit-btn">Envoyer le message</button>
|
||||
<p class="error" id="error-msg"></p>
|
||||
</form>
|
||||
|
||||
<div class="success" id="success-msg">
|
||||
<h2>Merci !</h2>
|
||||
<p>Je reviens vers vous sous 48h.</p>
|
||||
</div>
|
||||
|
||||
<div class="alt">
|
||||
<p>Ou directement : <a href="mailto:contact@lelectronrare.fr">contact@lelectronrare.fr</a></p>
|
||||
<p style="margin-top:8px;"><a href="https://fr.linkedin.com/in/electron-rare" target="_blank" rel="noopener">LinkedIn</a> · <a href="https://github.com/electron-rare/" target="_blank" rel="noopener">GitHub</a></p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="footer">© 2026 Clément Saillant — L'électron rare · <a href="https://blog.saillant.cc" target="_blank" rel="noopener" style="color:rgba(255,255,255,0.3);">Blog</a> · <a href="https://fr.linkedin.com/in/electron-rare" target="_blank" rel="noopener" style="color:rgba(255,255,255,0.3);">LinkedIn</a> · <a href="https://github.com/electron-rare/" target="_blank" rel="noopener" style="color:rgba(255,255,255,0.3);">GitHub</a></div>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById('contact-form');
|
||||
const btn = document.getElementById('submit-btn');
|
||||
const errEl = document.getElementById('error-msg');
|
||||
const successEl = document.getElementById('success-msg');
|
||||
|
||||
form?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Envoi...';
|
||||
errEl.style.display = 'none';
|
||||
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
try {
|
||||
const res = await fetch('/api/submit-lead', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) throw new Error('Erreur serveur');
|
||||
form.style.display = 'none';
|
||||
successEl.style.display = 'block';
|
||||
if (typeof gtag === 'function') gtag('event', 'contact_form_submit', { need_type: data.needType });
|
||||
} catch (err) {
|
||||
errEl.textContent = 'Erreur lors de l\'envoi. Essayez par email : contact@lelectronrare.fr';
|
||||
errEl.style.display = 'block';
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Envoyer le message';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import '@/styles/global.css';
|
||||
---
|
||||
|
||||
<BaseLayout title="L'Electron Rare — Bientôt" description="L'Electron Rare arrive. Systèmes électroniques spécifiques, formations, prototypage.">
|
||||
<main class="countdown-page">
|
||||
<svg class="atom" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-cyan-soft, #5bd1d866)" stroke-width="0.8" transform="rotate(-30 150 150)"/>
|
||||
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-amber-soft, #f1c27a66)" stroke-width="0.8" transform="rotate(30 150 150)"/>
|
||||
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-green-soft, #b6d18f44)" stroke-width="0.8" transform="rotate(90 150 150)"/>
|
||||
<circle cx="150" cy="150" r="18" fill="url(#nucleusGlow)"/>
|
||||
<circle cx="150" cy="150" r="8" fill="var(--accent, #f1c27a)"/>
|
||||
<circle r="5" fill="var(--electric, #5bd1d8)"><animateMotion dur="3s" repeatCount="indefinite"><mpath href="#o1"/></animateMotion></circle>
|
||||
<circle r="10" fill="var(--electric, #5bd1d8)" opacity="0.3"><animateMotion dur="3s" repeatCount="indefinite"><mpath href="#o1"/></animateMotion></circle>
|
||||
<circle r="4" fill="var(--accent, #f1c27a)"><animateMotion dur="4.2s" repeatCount="indefinite"><mpath href="#o2"/></animateMotion></circle>
|
||||
<circle r="9" fill="var(--accent, #f1c27a)" opacity="0.25"><animateMotion dur="4.2s" repeatCount="indefinite"><mpath href="#o2"/></animateMotion></circle>
|
||||
<circle r="3" fill="var(--trace-green, #b6d18f)"><animateMotion dur="5.8s" repeatCount="indefinite"><mpath href="#o3"/></animateMotion></circle>
|
||||
<circle r="8" fill="var(--trace-green, #b6d18f)" opacity="0.2"><animateMotion dur="5.8s" repeatCount="indefinite"><mpath href="#o3"/></animateMotion></circle>
|
||||
<defs>
|
||||
<ellipse id="o1" cx="150" cy="150" rx="120" ry="45" transform="rotate(-30 150 150)" fill="none"/>
|
||||
<ellipse id="o2" cx="150" cy="150" rx="120" ry="45" transform="rotate(30 150 150)" fill="none"/>
|
||||
<ellipse id="o3" cx="150" cy="150" rx="120" ry="45" transform="rotate(90 150 150)" fill="none"/>
|
||||
<radialGradient id="nucleusGlow">
|
||||
<stop offset="0%" stop-color="var(--accent, #f1c27a)" stop-opacity="0.6"/>
|
||||
<stop offset="100%" stop-color="var(--accent, #f1c27a)" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<h1 class="title">L'<span class="electric">é</span>lectron rare</h1>
|
||||
<div class="timer" id="timer">
|
||||
<div class="unit"><span class="num" id="days">--</span><span class="label">jours</span></div>
|
||||
<div class="sep">:</div>
|
||||
<div class="unit"><span class="num" id="hours">--</span><span class="label">heures</span></div>
|
||||
<div class="sep">:</div>
|
||||
<div class="unit"><span class="num" id="minutes">--</span><span class="label">minutes</span></div>
|
||||
<div class="sep">:</div>
|
||||
<div class="unit"><span class="num" id="seconds">--</span><span class="label">secondes</span></div>
|
||||
</div>
|
||||
<p class="tagline" id="tagline"></p>
|
||||
<p class="footnote">Systèmes électroniques spécifiques · Formations · Prototypage</p>
|
||||
</main>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
const LAUNCH = new Date('2026-05-01T00:00:00+02:00').getTime();
|
||||
const messages = [
|
||||
"Charge des condensateurs en cours...",
|
||||
"Calibration de l'oscilloscope...",
|
||||
"Soudure des derniers composants...",
|
||||
"Vérification de la BOM...",
|
||||
"Flash du firmware en cours...",
|
||||
"Mesure de continuité... bip.",
|
||||
"Routage des dernières pistes...",
|
||||
"Test de la chaîne d'alimentation...",
|
||||
"Programmation du bootloader...",
|
||||
"Revue de schéma en cours...",
|
||||
"Placement des composants CMS...",
|
||||
"Passage au four de refusion...",
|
||||
"Debug JTAG en cours...",
|
||||
"Vérification du plan de masse...",
|
||||
"Analyse du spectre RF..."
|
||||
];
|
||||
let msgIndex = Math.floor(Math.random() * messages.length);
|
||||
function pad(n) { return String(n).padStart(2, '0'); }
|
||||
function update() {
|
||||
const diff = Math.max(0, LAUNCH - Date.now());
|
||||
document.getElementById('days').textContent = pad(Math.floor(diff / 86400000));
|
||||
document.getElementById('hours').textContent = pad(Math.floor((diff % 86400000) / 3600000));
|
||||
document.getElementById('minutes').textContent = pad(Math.floor((diff % 3600000) / 60000));
|
||||
document.getElementById('seconds').textContent = pad(Math.floor((diff % 60000) / 1000));
|
||||
}
|
||||
function rotateMessage() {
|
||||
const el = document.getElementById('tagline');
|
||||
el.style.opacity = '0';
|
||||
setTimeout(() => { el.textContent = messages[msgIndex]; el.style.opacity = '1'; msgIndex = (msgIndex + 1) % messages.length; }, 400);
|
||||
}
|
||||
update(); rotateMessage();
|
||||
setInterval(update, 1000);
|
||||
setInterval(rotateMessage, 8000);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.countdown-page { min-height:100vh; display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center; padding:48px 24px; background:var(--bg,#0b0a09); }
|
||||
.atom { width:clamp(180px,35vw,260px); height:auto; margin-bottom:24px; }
|
||||
.title { font-family:'Fraunces',serif; font-size:clamp(28px,5vw,48px); font-weight:700; color:var(--text,#f4eee3); margin:0 0 32px; letter-spacing:-1px; }
|
||||
.electric { color:var(--electric,#5bd1d8); animation:pulse 3s ease-in-out infinite; }
|
||||
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.6;text-shadow:0 0 12px var(--electric,#5bd1d8)} }
|
||||
.timer { display:flex; align-items:center; gap:8px; margin-bottom:32px; }
|
||||
.unit { display:flex; flex-direction:column; align-items:center; }
|
||||
.num { font-family:'IBM Plex Mono','Share Tech Mono',monospace; font-size:clamp(36px,8vw,72px); font-weight:600; color:var(--text,#f4eee3); line-height:1; min-width:2ch; }
|
||||
.label { font-family:'IBM Plex Mono',monospace; font-size:11px; color:var(--text-dim,#b6a894); text-transform:uppercase; letter-spacing:2px; margin-top:6px; }
|
||||
.sep { font-family:'IBM Plex Mono',monospace; font-size:clamp(28px,6vw,56px); color:var(--accent,#f1c27a); opacity:.5; margin-bottom:20px; }
|
||||
.tagline { font-family:'IBM Plex Mono',monospace; font-size:14px; color:var(--accent,#f1c27a); margin:0 0 40px; min-height:1.5em; transition:opacity .4s ease; }
|
||||
.footnote { font-family:'IBM Plex Mono',monospace; font-size:12px; color:var(--text-dim,#b6a894); opacity:.4; letter-spacing:.5px; }
|
||||
</style>
|
||||
+151
-251
@@ -1,271 +1,171 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import '@/styles/global.css';
|
||||
import '@/styles/home-workbench.css';
|
||||
import SiteHeader from '@/components/SiteHeader.astro';
|
||||
import { withSiteBase } from '@/lib/site';
|
||||
import { TRACK_EVENTS, trackAttrs } from '@/lib/tracking';
|
||||
|
||||
const title = "Formation électronique appliquée | L'électron rare";
|
||||
const description = "Formations sur mesure : électronique, automatisme, énergie, stockage, PCB, instrumentation, IoT et optimisation. 1 jour, 3 jours ou programme dédié. Écoles, entreprises.";
|
||||
const homeHref = withSiteBase('/');
|
||||
const homeAboutHref = withSiteBase('/#a-propos');
|
||||
const homeMissionsHref = withSiteBase('/#graphic-sprints-title');
|
||||
const formationHref = withSiteBase('/formation/');
|
||||
const contactHref = withSiteBase('/#contact');
|
||||
const mentionsHref = withSiteBase('/mentions-legales/');
|
||||
const headerItems = [
|
||||
{ href: homeAboutHref, label: 'Approche' },
|
||||
{ href: homeMissionsHref, label: 'Missions' },
|
||||
{ href: formationHref, label: 'Formation', current: true },
|
||||
{ href: contactHref, label: 'Contact', desktopOnly: true }
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout title={title} description={description} ogDescription="Formations sur mesure en électronique, automatisme, énergie, stockage, PCB, instrumentation et IoT. 1 jour, 3 jours ou programme dédié. Écoles, entreprises, centres de formation.">
|
||||
<SiteHeader brandHref={homeHref} items={headerItems} quickHref={contactHref} />
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Formation électronique appliquée | L'électron rare</title>
|
||||
<meta name="description" content="Formations sur mesure : électronique, automatisme, énergie, PCB, instrumentation, IoT. 1 jour, 3 jours ou programme dédié. Écoles, entreprises." />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { background: #06080b; color: #fff; }
|
||||
body { background: #06080b; min-height: 100vh; font-family: 'Courier New', monospace; }
|
||||
.site-hdr { position: fixed; top: 0; left: 0; right: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; padding: 16px clamp(20px, 3vw, 40px); background: rgba(6,8,11,0.85); backdrop-filter: blur(12px); border-bottom: 1px solid rgba(255,255,255,0.05); }
|
||||
.hdr-left { display: flex; align-items: center; gap: 10px; }
|
||||
.hdr-left a { text-decoration: none; display: flex; align-items: center; gap: 10px; }
|
||||
.hdr-logo { width: 36px; height: auto; filter: drop-shadow(0 0 12px rgba(91,209,216,0.3)); }
|
||||
.hdr-name { font-size: 16px; font-weight: 800; color: rgba(255,255,255,0.8); letter-spacing: 0.04em; }
|
||||
.hdr-nav { display: flex; align-items: center; gap: 8px; }
|
||||
.hdr-link { padding: 8px 16px; color: rgba(255,255,255,0.6); font-size: 12px; font-weight: 600; text-decoration: none; letter-spacing: 0.04em; transition: color 0.2s; }
|
||||
.hdr-link:hover { color: #5bd1d8; }
|
||||
.hdr-link.active { color: #f59e0b; }
|
||||
.hdr-cta { padding: 8px 20px; border-radius: 6px; background: rgba(91,209,216,0.15); border: 1px solid rgba(91,209,216,0.3); color: #5bd1d8; font-size: 12px; font-weight: 700; text-decoration: none; letter-spacing: 0.05em; }
|
||||
.burger { display: none; background: none; border: none; color: #fff; font-size: 24px; cursor: pointer; padding: 8px; }
|
||||
.mobile-menu { display: none; position: fixed; inset: 0; z-index: 100; background: rgba(6,8,11,0.95); backdrop-filter: blur(16px); flex-direction: column; align-items: center; justify-content: center; gap: 24px; }
|
||||
.mobile-menu.open { display: flex; }
|
||||
.mobile-menu a { font-size: 20px; font-weight: 700; color: rgba(255,255,255,0.8); text-decoration: none; letter-spacing: 0.05em; padding: 12px 24px; }
|
||||
.mobile-menu a:hover { color: #5bd1d8; }
|
||||
.mobile-menu .close-btn { position: absolute; top: 20px; right: 20px; background: none; border: none; color: #fff; font-size: 28px; cursor: pointer; }
|
||||
@media (max-width: 768px) { .hdr-nav { display: none; } .burger { display: block; } }
|
||||
.content { max-width: 800px; margin: 0 auto; padding: 120px 24px 80px; }
|
||||
h1 { font-size: clamp(28px, 4vw, 48px); font-weight: 800; color: #f59e0b; margin-bottom: 16px; letter-spacing: -0.02em; }
|
||||
h2 { font-size: 18px; font-weight: 700; color: #fff; margin: 48px 0 16px; text-transform: uppercase; letter-spacing: 0.06em; border-bottom: 1px solid rgba(255,255,255,0.08); padding-bottom: 8px; }
|
||||
h3 { font-size: 14px; font-weight: 700; color: #f59e0b; margin: 28px 0 8px; text-transform: uppercase; letter-spacing: 0.08em; }
|
||||
p { font-size: 14px; color: rgba(255,255,255,0.6); line-height: 1.8; margin-bottom: 12px; }
|
||||
.lead { font-size: 15px; color: rgba(255,255,255,0.5); margin-bottom: 32px; }
|
||||
.card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 20px; margin-bottom: 12px; }
|
||||
.card-t { font-size: 15px; font-weight: 700; color: #fff; margin-bottom: 6px; }
|
||||
.card-desc { font-size: 12px; color: rgba(255,255,255,0.45); line-height: 1.6; margin-bottom: 8px; }
|
||||
.card-meta { font-size: 11px; color: rgba(255,255,255,0.3); letter-spacing: 0.04em; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 12px; }
|
||||
.subjects { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
@media (max-width: 700px) { .subjects, .grid { grid-template-columns: 1fr; } }
|
||||
.subject { background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.06); border-radius: 10px; padding: 16px; }
|
||||
.subject-t { font-size: 13px; font-weight: 700; color: #fff; margin-bottom: 6px; }
|
||||
.subject-d { font-size: 11px; color: rgba(255,255,255,0.4); line-height: 1.5; }
|
||||
.audience { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0 32px; }
|
||||
.audience-chip { font-size: 11px; padding: 6px 14px; border-radius: 20px; border: 1px solid rgba(255,255,255,0.12); color: rgba(255,255,255,0.55); }
|
||||
.format-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
||||
@media (max-width: 700px) { .format-grid { grid-template-columns: 1fr; } }
|
||||
.format { text-align: center; padding: 24px 16px; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; }
|
||||
.format-dur { font-size: 24px; font-weight: 800; color: #f59e0b; margin-bottom: 4px; }
|
||||
.format-name { font-size: 13px; font-weight: 700; color: #fff; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
.format-desc { font-size: 11px; color: rgba(255,255,255,0.4); line-height: 1.5; }
|
||||
.cta-box { margin-top: 48px; text-align: center; }
|
||||
.cta-btn { display: inline-block; padding: 14px 32px; border-radius: 8px; background: #f59e0b; color: #06080b; font-weight: 700; font-size: 14px; text-decoration: none; letter-spacing: 0.04em; }
|
||||
.cta-btn:hover { background: #fbbf24; }
|
||||
.info { margin-top: 40px; font-size: 11px; color: rgba(255,255,255,0.25); line-height: 1.8; }
|
||||
.footer { text-align: center; padding: 40px 24px; font-size: 10px; color: rgba(255,255,255,0.15); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-hdr">
|
||||
<div class="hdr-left"><a href="/"><img src="/assets/brand/logo-mark.svg" alt="" class="hdr-logo" /><span class="hdr-name">L'électron rare</span></a></div>
|
||||
<nav class="hdr-nav">
|
||||
<a href="/conception/" class="hdr-link">Conception</a>
|
||||
<a href="/formation/" class="hdr-link active">Formation</a>
|
||||
<a href="/consulting/" class="hdr-link">Consulting</a>
|
||||
<a href="/contact/" class="hdr-cta">Contact</a>
|
||||
</nav>
|
||||
<button class="burger" id="burger-btn" aria-label="Menu">☰</button>
|
||||
</header>
|
||||
<div class="mobile-menu" id="mobile-menu">
|
||||
<button class="close-btn" id="close-menu">✕</button>
|
||||
<a href="/">Accueil</a><a href="/conception/">Conception</a><a href="/formation/">Formation</a><a href="/consulting/">Consulting</a><a href="/contact/">Contact</a>
|
||||
</div>
|
||||
<script>document.getElementById('burger-btn')?.addEventListener('click',()=>document.getElementById('mobile-menu')?.classList.add('open'));document.getElementById('close-menu')?.addEventListener('click',()=>document.getElementById('mobile-menu')?.classList.remove('open'));</script>
|
||||
|
||||
<main id="main-content" class="site-shell studio-structure pb-12 pt-3">
|
||||
|
||||
<section class="circuit-board rounded-2xl p-5 md:p-6 mt-4" data-reveal>
|
||||
<div class="circuit-title-row">
|
||||
<span class="circuit-node" aria-hidden="true"></span>
|
||||
<h1 class="m-0 text-3xl md:text-4xl">Formation électronique appliquée</h1>
|
||||
<span class="circuit-pinline" aria-hidden="true"></span>
|
||||
<main class="content">
|
||||
<h1>Formation électronique appliquée</h1>
|
||||
<p class="lead">Des formations pratiques, ancrées dans le terrain. Chaque programme est construit sur mesure à partir de votre contexte réel : vos projets, vos équipes, vos contraintes.</p>
|
||||
<div style="display:flex;align-items:center;gap:12px;padding:14px 18px;background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.2);border-radius:10px;margin-bottom:32px;">
|
||||
<span style="font-size:24px;">🏅</span>
|
||||
<div>
|
||||
<p style="font-size:13px;color:#f59e0b;font-weight:700;margin:0;">Formations finançables OPCO / Afdas</p>
|
||||
<p style="font-size:11px;color:rgba(255,255,255,0.4);margin:4px 0 0;">Intervention possible via un organisme partenaire certifié Qualiopi.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="section-lead mb-0 mt-3">
|
||||
Des formations pratiques, ancrées dans le terrain, pour monter en compétences sur l'électronique, l'automatisme, l'énergie, le stockage, la conception PCB, l'instrumentation, l'IoT et l'optimisation d'usage.
|
||||
</p>
|
||||
<p class="mb-0 mt-3 studio-muted">
|
||||
Chaque formation est construite sur mesure à partir de votre contexte réel : vos projets, vos équipes, vos contraintes. Pas de slides génériques — du bench, du schéma et du livrable.
|
||||
</p>
|
||||
</section>
|
||||
<div class="audience">
|
||||
<span class="audience-chip">Entreprises industrielles</span>
|
||||
<span class="audience-chip">Écoles et universités</span>
|
||||
<span class="audience-chip">Centres de formation</span>
|
||||
<span class="audience-chip">Studios créatifs</span>
|
||||
</div>
|
||||
|
||||
<section class="circuit-board rounded-2xl p-5 md:p-6 mt-5" data-reveal>
|
||||
<div class="circuit-title-row">
|
||||
<span class="circuit-node circuit-node--magenta" aria-hidden="true"></span>
|
||||
<h2 class="m-0 text-2xl md:text-3xl">Publics cibles</h2>
|
||||
<span class="circuit-pinline" aria-hidden="true"></span>
|
||||
<h2>Formats</h2>
|
||||
<div class="format-grid">
|
||||
<div class="format">
|
||||
<p class="format-dur">1 jour</p>
|
||||
<p class="format-name">Initiation</p>
|
||||
<p class="format-desc">Fondamentaux, démos live, TP sur matériel réel, support PDF</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 rounded-xl border border-[color:var(--line)] p-3">
|
||||
<ul class="m-0 grid grid-cols-1 gap-2 p-0">
|
||||
<li class="list-none studio-chip studio-chip--cyan">
|
||||
<strong>Entreprises industrielles</strong> — équipes R&D, bureaux d'études, techniciens de maintenance qui veulent concevoir, diagnostiquer ou fiabiliser des systèmes électroniques dans leurs produits ou process.
|
||||
</li>
|
||||
<li class="list-none studio-chip studio-chip--vio">
|
||||
<strong>Écoles et universités</strong> — BTS, IUT, grandes écoles : TP encadrés, projets tutorés, modules spécialisés en systèmes électroniques, PCB et prototypage.
|
||||
</li>
|
||||
<li class="list-none studio-chip studio-chip--pink">
|
||||
<strong>Centres de formation</strong> — organismes souhaitant intégrer des modules électronique, instrumentation, énergie, stockage, IoT ou automatisme à leur catalogue.
|
||||
</li>
|
||||
<li class="list-none studio-chip studio-chip--emerald">
|
||||
<strong>Studios créatifs et compagnies</strong> — équipes techniques du spectacle vivant, de l'événementiel ou des arts numériques qui veulent gagner en autonomie.
|
||||
</li>
|
||||
</ul>
|
||||
<div class="format">
|
||||
<p class="format-dur">3 jours</p>
|
||||
<p class="format-name">Approfondissement</p>
|
||||
<p class="format-desc">Projet fil rouge, prototype fonctionnel, documentation technique</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-5" data-reveal>
|
||||
<div class="graphic-sprints-panel graphic-sprints-panel--routing">
|
||||
<div class="graphic-sprints-head">
|
||||
<p class="graphic-sprints-kicker">Formats</p>
|
||||
<h2 class="graphic-sprints-title">Trois formats, un principe : le concret d'abord</h2>
|
||||
<p class="graphic-sprints-copy">
|
||||
Chaque format inclut un support de cours, des exercices pratiques sur matériel réel et un livrable que les participants repartent avec.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="graphic-sprints-grid">
|
||||
<article class="graphic-sprint-card" style="--sprint-progress-target: 33%">
|
||||
<header class="graphic-sprint-card-head">
|
||||
<p class="graphic-sprint-status graphic-sprint-status--trace">Initiation</p>
|
||||
<p class="graphic-sprint-window">1 jour (7h)</p>
|
||||
</header>
|
||||
<h3 class="graphic-sprint-title">Découverte</h3>
|
||||
<p class="graphic-sprint-objective">
|
||||
Comprendre les fondamentaux d'un domaine technique. Idéal pour une première prise en main ou une mise à niveau rapide.
|
||||
</p>
|
||||
<div class="graphic-sprint-progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="33">
|
||||
<span class="graphic-sprint-progress-fill"></span>
|
||||
</div>
|
||||
<ul class="graphic-sprint-deliverables">
|
||||
<li>Théorie appliquée + démos live</li>
|
||||
<li>TP sur matériel réel</li>
|
||||
<li>Support de cours PDF</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="graphic-sprint-card" style="--sprint-progress-target: 66%">
|
||||
<header class="graphic-sprint-card-head">
|
||||
<p class="graphic-sprint-status graphic-sprint-status--bench">Approfondissement</p>
|
||||
<p class="graphic-sprint-window">3 jours (21h)</p>
|
||||
</header>
|
||||
<h3 class="graphic-sprint-title">Pratique</h3>
|
||||
<p class="graphic-sprint-objective">
|
||||
Maîtriser un sujet de bout en bout avec un projet fil rouge. Les participants repartent avec un prototype fonctionnel.
|
||||
</p>
|
||||
<div class="graphic-sprint-progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="66">
|
||||
<span class="graphic-sprint-progress-fill"></span>
|
||||
</div>
|
||||
<ul class="graphic-sprint-deliverables">
|
||||
<li>Projet fil rouge réalisé</li>
|
||||
<li>Prototype fonctionnel</li>
|
||||
<li>Documentation technique complète</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="graphic-sprint-card" style="--sprint-progress-target: 100%">
|
||||
<header class="graphic-sprint-card-head">
|
||||
<p class="graphic-sprint-status graphic-sprint-status--run">Sur mesure</p>
|
||||
<p class="graphic-sprint-window">Durée adaptée</p>
|
||||
</header>
|
||||
<h3 class="graphic-sprint-title">Programme dédié</h3>
|
||||
<p class="graphic-sprint-objective">
|
||||
Formation construite intégralement autour de votre projet, vos équipements et vos objectifs. Accompagnement post-formation inclus.
|
||||
</p>
|
||||
<div class="graphic-sprint-progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100">
|
||||
<span class="graphic-sprint-progress-fill"></span>
|
||||
</div>
|
||||
<ul class="graphic-sprint-deliverables">
|
||||
<li>Programme sur mesure</li>
|
||||
<li>Matériel adapté à votre contexte</li>
|
||||
<li>Suivi post-formation</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
<div class="format">
|
||||
<p class="format-dur">Sur mesure</p>
|
||||
<p class="format-name">Programme dédié</p>
|
||||
<p class="format-desc">Autour de votre projet, vos équipements, suivi post-formation</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="circuit-board rounded-2xl p-5 md:p-6 mt-5" data-reveal>
|
||||
<div class="circuit-title-row">
|
||||
<span class="circuit-node" aria-hidden="true"></span>
|
||||
<h2 class="m-0 text-2xl md:text-3xl">Sujets de formation</h2>
|
||||
<span class="circuit-pinline" aria-hidden="true"></span>
|
||||
</div>
|
||||
<h2>Sujets</h2>
|
||||
<div class="subjects">
|
||||
<div class="subject"><p class="subject-t">Systèmes électroniques et embarqués</p><p class="subject-d">Architecture matérielle, µC (STM32, ESP32, Teensy), alimentation, debug bench, IPC-A-610, CEM.</p></div>
|
||||
<div class="subject"><p class="subject-t">Conception PCB</p><p class="subject-d">KiCad, routage, stackup, DRC/ERC, Gerber, BOM, pick & place.</p></div>
|
||||
<div class="subject"><p class="subject-t">IoT et protocoles</p><p class="subject-d">Capteurs, MQTT, WiFi, BLE, LoRa, TinyML, du capteur au dashboard.</p></div>
|
||||
<div class="subject"><p class="subject-t">Énergie et stockage</p><p class="subject-d">Batterie, BMS, télémétrie, supervision, optimisation d'exploitation.</p></div>
|
||||
<div class="subject"><p class="subject-t">Automatisme industriel</p><p class="subject-d">Siemens S7, Schneider, câblage armoires, variateurs, IEC 60204-1.</p></div>
|
||||
<div class="subject"><p class="subject-t">Contrôle LED et scénographie</p><p class="subject-d">DMX, Art-Net, DANTE, pilotage LED, intégration spectacle.</p></div>
|
||||
<div class="subject"><p class="subject-t">Firmware et programmation</p><p class="subject-d">C/C++ embarqué, Python, debug série, interruptions, RTOS.</p></div>
|
||||
<div class="subject"><p class="subject-t">Workflows agentiques et IA</p><p class="subject-d">Agents IA, génération de code, revue automatisée, documentation assistée.</p></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<article class="rounded-xl border border-[color:var(--line)] p-4">
|
||||
<h3 class="m-0 text-lg font-semibold">Systèmes électroniques et embarqués</h3>
|
||||
<p class="mt-2 mb-0 studio-muted">
|
||||
Architecture matérielle, microcontrôleurs (STM32, ESP32, Teensy), alimentation, lecture de datasheet, debug sur bench. Conformité IPC-A-610 et CEM.
|
||||
</p>
|
||||
</article>
|
||||
<h2>Catalogue</h2>
|
||||
|
||||
<article class="rounded-xl border border-[color:var(--line)] p-4">
|
||||
<h3 class="m-0 text-lg font-semibold">Conception PCB</h3>
|
||||
<p class="mt-2 mb-0 studio-muted">
|
||||
De la schématique au routage avec KiCad. Règles de design, stackup, vérification DRC/ERC, préparation des fichiers de fabrication (Gerber, BOM, pick & place).
|
||||
</p>
|
||||
</article>
|
||||
<h3>Initiations — Tous publics</h3>
|
||||
<div class="grid">
|
||||
<div class="card"><p class="card-t">Mes Premiers Circuits</p><p class="card-desc">Breadboard, soudure, badge lumineux. Zéro prérequis.</p><p class="card-meta">1 jour · 420€ · 6-8 pers.</p></div>
|
||||
<div class="card"><p class="card-t">Arduino Pour Tous</p><p class="card-desc">Capteurs, LED, moteurs. Kit Arduino à emporter.</p><p class="card-meta">1 jour · 450€ · 6-8 pers.</p></div>
|
||||
<div class="card"><p class="card-t">Lumière et Son</p><p class="card-desc">LED NeoPixel, DMX512, audio interactif. Kit inclus.</p><p class="card-meta">1 jour · 480€ · 6-8 pers.</p></div>
|
||||
<div class="card"><p class="card-t">KiCad Découverte</p><p class="card-desc">Du breadboard au PCB. Commandez votre carte chez JLCPCB.</p><p class="card-meta">1 jour · 420€ · 6-8 pers.</p></div>
|
||||
</div>
|
||||
|
||||
<article class="rounded-xl border border-[color:var(--line)] p-4">
|
||||
<h3 class="m-0 text-lg font-semibold">IoT et protocoles</h3>
|
||||
<p class="mt-2 mb-0 studio-muted">
|
||||
Capteurs, télémétrie, protocoles terrain (MQTT, HTTP, VE.Direct), connectivité (WiFi, BLE, LoRa), TinyML et IA embarquée. Du capteur au dashboard.
|
||||
</p>
|
||||
</article>
|
||||
<h3>Créatif & Artistes</h3>
|
||||
<div class="grid">
|
||||
<div class="card"><p class="card-t">Créer son Contrôleur MIDI</p><p class="card-desc">Teensy, potentiomètres, capteurs → instrument MIDI USB.</p><p class="card-meta">1 jour · 450€ · Variante 3h : 220€</p></div>
|
||||
<div class="card"><p class="card-t">Programmation Embarquée pour Artistes</p><p class="card-desc">ESP32, capteurs, DMX, Art-Net, OSC → installations interactives.</p><p class="card-meta">1 jour · 480€ · Variante 3h : 240€</p></div>
|
||||
<div class="card"><p class="card-t">IA pour la Création Artistique</p><p class="card-desc">Stable Diffusion, ComfyUI, Ollama. Image, son, vidéo, performance live.</p><p class="card-meta">1 jour · 500€ · Variante 3h : 250€</p></div>
|
||||
</div>
|
||||
|
||||
<article class="rounded-xl border border-[color:var(--line)] p-4">
|
||||
<h3 class="m-0 text-lg font-semibold">Énergie, stockage et optimisation</h3>
|
||||
<p class="mt-2 mb-0 studio-muted">
|
||||
Batterie, BMS, télémétrie, supervision, pilotage de charges, logique d'optimisation et amélioration d'exploitation. De la mesure au système mieux réglé.
|
||||
</p>
|
||||
</article>
|
||||
<h3>Pro — Avancé</h3>
|
||||
<div class="grid">
|
||||
<div class="card"><p class="card-t">KiCad — Conception PCB</p><p class="card-desc">KiCad 9/10, routage avancé, impédance, DFM, fabrication JLCPCB.</p><p class="card-meta">2 jours · 1 550€ · 6 pers. max</p></div>
|
||||
<div class="card"><p class="card-t">Dispositifs Culturels</p><p class="card-desc">LED, DMX512, Art-Net, autonomie batteries, intégration terrain spectacle.</p><p class="card-meta">1 jour · 775€ · Finançable Afdas/OPCO</p></div>
|
||||
</div>
|
||||
|
||||
<article class="rounded-xl border border-[color:var(--line)] p-4">
|
||||
<h3 class="m-0 text-lg font-semibold">Automatisme industriel</h3>
|
||||
<p class="mt-2 mb-0 studio-muted">
|
||||
Automates Siemens (S7) et Schneider, câblage d'armoires, variateurs, mise en service. Conformité IEC 60204-1, IEC 61439, NFC 13-200. Lecture de schémas électriques industriels.
|
||||
</p>
|
||||
</article>
|
||||
<h3>Enfants & Jeunes</h3>
|
||||
<div class="grid">
|
||||
<div class="card"><p class="card-t">Premiers Circuits (8-10 ans)</p><p class="card-desc">micro:bit, programmation par blocs, LED, capteurs.</p><p class="card-meta">3 demi-journées · 50€/demi-journée</p></div>
|
||||
<div class="card"><p class="card-t">Maker Junior (10-13 ans)</p><p class="card-desc">Arduino, première soudure, capteurs, projet personnel.</p><p class="card-meta">5 demi-journées · 50€/demi-journée</p></div>
|
||||
<div class="card"><p class="card-t">Electro Lab (13-16 ans)</p><p class="card-desc">ESP32, KiCad, conception et fabrication de vrai PCB.</p><p class="card-meta">8 sessions · 299€ hors frais de fabrication</p></div>
|
||||
<div class="card"><p class="card-t">Atelier Parent-Enfant</p><p class="card-desc">3h de découverte en duo. Montage, soudure, LED.</p><p class="card-meta">3h · 59€/binôme · À partir de 8 ans</p></div>
|
||||
</div>
|
||||
|
||||
<article class="rounded-xl border border-[color:var(--line)] p-4">
|
||||
<h3 class="m-0 text-lg font-semibold">Contrôle LED et scénographie</h3>
|
||||
<p class="mt-2 mb-0 studio-muted">
|
||||
Protocoles DMX, Art-Net, DANTE. Pilotage LED, topologies multi-cartes, intégration dans des dispositifs scéniques et événementiels.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="rounded-xl border border-[color:var(--line)] p-4">
|
||||
<h3 class="m-0 text-lg font-semibold">Firmware et programmation</h3>
|
||||
<p class="mt-2 mb-0 studio-muted">
|
||||
C/C++ embarqué, Python pour l'instrumentation, debug série, gestion mémoire, interruptions, RTOS. Du code qui tourne sur le terrain.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="rounded-xl border border-[color:var(--line)] p-4">
|
||||
<h3 class="m-0 text-lg font-semibold">Workflows agentiques et IA appliquée</h3>
|
||||
<p class="mt-2 mb-0 studio-muted">
|
||||
Intégrer l'IA et les agents autonomes dans vos flux de travail : génération de code embarqué, revue automatisée, documentation technique, analyse de datasheets et tests assistés par IA. Productivité et qualité augmentées.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="circuit-board rounded-2xl p-5 md:p-6 mt-5" data-reveal>
|
||||
<div class="circuit-title-row">
|
||||
<span class="circuit-node circuit-node--magenta" aria-hidden="true"></span>
|
||||
<h2 class="m-0 text-2xl md:text-3xl">Demander un programme</h2>
|
||||
<span class="circuit-pinline" aria-hidden="true"></span>
|
||||
</div>
|
||||
|
||||
<p class="section-lead mb-0 mt-3">
|
||||
Décrivez votre besoin en quelques lignes. Je vous propose un programme adapté sous 48h.
|
||||
</p>
|
||||
<p class="mb-0 mt-3 studio-muted">
|
||||
Précisez le public, le niveau, le sujet visé et le format souhaité. Même une idée vague suffit pour démarrer.
|
||||
</p>
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-3">
|
||||
<a
|
||||
href="mailto:contact@lelectronrare.fr?subject=Demande%20de%20formation&body=Bonjour%20Cl%C3%A9ment%2C%0A%0AJe%20suis%20int%C3%A9ress%C3%A9%20par%20une%20formation.%0A%0AContexte%20(entreprise%2C%20%C3%A9cole%2C%20centre%20de%20formation)%20%3A%0ASujet%20vis%C3%A9%20%3A%0APublic%20concern%C3%A9%20%3A%0AFormat%20souhait%C3%A9%20(dur%C3%A9e%2C%20pr%C3%A9sentiel%2Fdistanciel)%20%3A"
|
||||
class="figma-lab-pill figma-lab-pill--primary"
|
||||
{...trackAttrs(TRACK_EVENTS.outboundEmailContact, 'mailto:contact@lelectronrare.fr')}
|
||||
>
|
||||
Envoyer par mail
|
||||
</a>
|
||||
<a
|
||||
href="https://fr.linkedin.com/in/electron-rare"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="figma-lab-pill figma-lab-pill--secondary"
|
||||
{...trackAttrs(TRACK_EVENTS.outboundLinkedinContact, 'linkedin.com')}
|
||||
>
|
||||
Contacter sur LinkedIn
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ul aria-label="Repères" class="mb-0 mt-4 pl-5 studio-muted">
|
||||
<li>Intervention en présentiel (France entière) ou en distanciel</li>
|
||||
<li>Matériel de TP fourni ou adapté à votre parc existant</li>
|
||||
<li>Convention de formation et programme détaillé sur demande</li>
|
||||
<li>Intervention possible via un organisme certifié Qualiopi pour le financement OPCO</li>
|
||||
<li>Assuré RC Pro · NDA sur demande</li>
|
||||
</ul>
|
||||
</section>
|
||||
<div class="cta-box">
|
||||
<a href="/contact/?from=formation" class="cta-btn">Demander un programme</a>
|
||||
</div>
|
||||
|
||||
<p class="info">
|
||||
Intervention en présentiel (France entière) ou en distanciel · Matériel de TP fourni · Convention de formation sur demande · Financement OPCO possible via organisme Qualiopi · Assuré RC Pro · NDA sur demande
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer border-t border-[var(--line)] py-6 text-sm text-[var(--text-muted)]">
|
||||
<div class="site-shell footer-grid footer-grid--enhanced">
|
||||
<div class="footer-col footer-col--brand">
|
||||
<p class="m-0 footer-title">L'électron rare</p>
|
||||
<div class="footer-brand-meta">
|
||||
<p class="mb-0 footer-copy">
|
||||
© 2026 Clément Saillant - systèmes électroniques spécifiques, conseil, formation — industrie, culture et projets multi-techniques.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav aria-label="Ressources" class="footer-links footer-links--single-rail">
|
||||
<a href={homeHref} class="footer-link">Accueil</a>
|
||||
<a href={formationHref} class="footer-link">Formation</a>
|
||||
<a href="https://fr.linkedin.com/in/electron-rare" target="_blank" rel="noopener noreferrer me" class="footer-link">LinkedIn</a>
|
||||
<a href="mailto:contact@lelectronrare.fr" class="footer-link">contact@lelectronrare.fr</a>
|
||||
<a href="https://github.com/electron-rare/" target="_blank" rel="noopener noreferrer me" class="footer-link">GitHub</a>
|
||||
<a href={mentionsHref} class="footer-link">Mentions légales</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</BaseLayout>
|
||||
<div class="footer">© 2026 Clément Saillant — L'électron rare · <a href="https://blog.saillant.cc" target="_blank" rel="noopener" style="color:rgba(255,255,255,0.3);">Blog</a> · <a href="https://fr.linkedin.com/in/electron-rare" target="_blank" rel="noopener" style="color:rgba(255,255,255,0.3);">LinkedIn</a> · <a href="https://github.com/electron-rare/" target="_blank" rel="noopener" style="color:rgba(255,255,255,0.3);">GitHub</a></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,508 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import '@/styles/global.css';
|
||||
import '@/styles/home-workbench.css';
|
||||
import SiteHeader from '@/components/SiteHeader.astro';
|
||||
import { Hero } from '@/components/sections/Hero';
|
||||
import { About } from '@/components/sections/About';
|
||||
import { CaseStudies } from '@/components/sections/CaseStudies';
|
||||
import { GraphicSprints } from '@/components/sections/GraphicSprints';
|
||||
import Contact from '@/components/sections/Contact.astro';
|
||||
import Faq from '@/components/sections/Faq.astro';
|
||||
import { withSiteBase } from '@/lib/site';
|
||||
import { TRACK_EVENTS, trackAttrs } from '@/lib/tracking';
|
||||
|
||||
const LAUNCH_DATE = new Date('2026-05-01T00:00:00+02:00');
|
||||
const isLaunched = Date.now() >= LAUNCH_DATE.getTime();
|
||||
|
||||
const formationHref = withSiteBase('/formation/');
|
||||
const mentionsHref = withSiteBase('/mentions-legales/');
|
||||
const headerItems = [
|
||||
{ href: '#a-propos', label: 'Approche' },
|
||||
{ href: '#graphic-sprints-title', label: 'Missions' },
|
||||
{ href: formationHref, label: 'Formation' },
|
||||
{ href: '#faq', label: 'FAQ' },
|
||||
{ href: '#contact', label: 'Contact', desktopOnly: true }
|
||||
];
|
||||
|
||||
const title = isLaunched
|
||||
? "Systèmes électroniques spécifiques | L'électron rare"
|
||||
: "L'Electron Rare — Bientôt";
|
||||
const description = isLaunched
|
||||
? "Conception, mise au point et fiabilisation de systèmes électroniques spécifiques."
|
||||
: "L'Electron Rare arrive. Systèmes électroniques spécifiques, formations, prototypage.";
|
||||
---
|
||||
|
||||
<BaseLayout title={title} description={description}>
|
||||
{isLaunched ? (
|
||||
<Fragment>
|
||||
<SiteHeader brandHref="#top" items={headerItems} quickHref="#contact" />
|
||||
<main id="main-content" class="site-shell studio-structure pb-12 pt-3">
|
||||
<section class="structure-grid structure-grid--hero" data-reveal>
|
||||
<div class="structure-cell structure-cell--hero">
|
||||
<Hero />
|
||||
</div>
|
||||
</section>
|
||||
<section class="structure-grid structure-grid--systems" data-reveal>
|
||||
<div class="structure-cell structure-cell--about">
|
||||
<About />
|
||||
</div>
|
||||
</section>
|
||||
<section class="structure-grid structure-grid--cases" data-reveal>
|
||||
<div class="structure-cell structure-cell--cases">
|
||||
<CaseStudies />
|
||||
</div>
|
||||
</section>
|
||||
<section class="structure-grid structure-grid--sprints" data-reveal>
|
||||
<div class="structure-cell structure-cell--sprints">
|
||||
<GraphicSprints />
|
||||
</div>
|
||||
</section>
|
||||
<section class="structure-grid structure-grid--faq" data-reveal>
|
||||
<div class="structure-cell structure-cell--faq">
|
||||
<Faq />
|
||||
</div>
|
||||
</section>
|
||||
<section class="structure-grid structure-grid--conversion" data-reveal>
|
||||
<div class="structure-cell structure-cell--contact">
|
||||
<Contact />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer class="site-footer border-t border-[var(--line)] py-6 text-sm text-[var(--text-muted)]">
|
||||
<div class="site-shell footer-grid footer-grid--enhanced">
|
||||
<div class="footer-col footer-col--brand">
|
||||
<p class="m-0 footer-title">L'électron rare</p>
|
||||
<p class="mb-0 footer-copy">© 2026 Clément Saillant</p>
|
||||
</div>
|
||||
<nav aria-label="Ressources" class="footer-links footer-links--single-rail">
|
||||
<a href="#a-propos" class="footer-link">Approche</a>
|
||||
<a href="#graphic-sprints-title" class="footer-link">Missions</a>
|
||||
<a href={formationHref} class="footer-link">Formation</a>
|
||||
<a href="#faq" class="footer-link">FAQ</a>
|
||||
<a href="#contact" class="footer-link">Contact</a>
|
||||
<a href={mentionsHref} class="footer-link">Mentions légales</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</Fragment>
|
||||
) : (
|
||||
<!-- COUNTDOWN — random WebGL or CSS -->
|
||||
<main class="countdown-page">
|
||||
<!-- BG A: WebGL shader (PCB traces) -->
|
||||
<div class="countdown-bg" id="bg-webgl">
|
||||
<div class="webgl-bg" id="webgl-countdown"></div>
|
||||
</div>
|
||||
|
||||
<!-- BG B: Pure CSS circuit board -->
|
||||
<div class="countdown-bg" id="bg-css">
|
||||
<div class="css-circuit">
|
||||
<div class="circuit-grid">
|
||||
<!-- Horizontal traces -->
|
||||
<div class="trace trace-h trace-h1"><span class="pulse"></span></div>
|
||||
<div class="trace trace-h trace-h2"><span class="pulse"></span></div>
|
||||
<div class="trace trace-h trace-h3"><span class="pulse"></span></div>
|
||||
<div class="trace trace-h trace-h4"><span class="pulse"></span></div>
|
||||
<div class="trace trace-h trace-h5"><span class="pulse"></span></div>
|
||||
<div class="trace trace-h trace-h6"><span class="pulse"></span></div>
|
||||
<!-- Vertical traces -->
|
||||
<div class="trace trace-v trace-v1"><span class="pulse"></span></div>
|
||||
<div class="trace trace-v trace-v2"><span class="pulse"></span></div>
|
||||
<div class="trace trace-v trace-v3"><span class="pulse"></span></div>
|
||||
<div class="trace trace-v trace-v4"><span class="pulse"></span></div>
|
||||
<div class="trace trace-v trace-v5"><span class="pulse"></span></div>
|
||||
<div class="trace trace-v trace-v6"><span class="pulse"></span></div>
|
||||
<!-- Nodes (intersections) -->
|
||||
<div class="node node-1"></div>
|
||||
<div class="node node-2"></div>
|
||||
<div class="node node-3"></div>
|
||||
<div class="node node-4"></div>
|
||||
<div class="node node-5"></div>
|
||||
<div class="node node-6"></div>
|
||||
<div class="node node-7"></div>
|
||||
<div class="node node-8"></div>
|
||||
<div class="node node-9"></div>
|
||||
<div class="node node-10"></div>
|
||||
<div class="node node-11"></div>
|
||||
<div class="node node-12"></div>
|
||||
<!-- IC chips -->
|
||||
<div class="ic ic-1">
|
||||
<span class="ic-label">MCU</span>
|
||||
<div class="ic-pins ic-pins-l"></div>
|
||||
<div class="ic-pins ic-pins-r"></div>
|
||||
</div>
|
||||
<div class="ic ic-2">
|
||||
<span class="ic-label">FPGA</span>
|
||||
<div class="ic-pins ic-pins-l"></div>
|
||||
<div class="ic-pins ic-pins-r"></div>
|
||||
</div>
|
||||
<div class="ic ic-3">
|
||||
<span class="ic-label">PWR</span>
|
||||
<div class="ic-pins ic-pins-l"></div>
|
||||
<div class="ic-pins ic-pins-r"></div>
|
||||
</div>
|
||||
<!-- SMD components -->
|
||||
<div class="smd smd-1"></div>
|
||||
<div class="smd smd-2"></div>
|
||||
<div class="smd smd-3"></div>
|
||||
<div class="smd smd-4"></div>
|
||||
<div class="smd smd-5"></div>
|
||||
<div class="smd smd-6"></div>
|
||||
<div class="smd smd-7"></div>
|
||||
<div class="smd smd-8"></div>
|
||||
<!-- Vias -->
|
||||
<div class="via via-1"></div>
|
||||
<div class="via via-2"></div>
|
||||
<div class="via via-3"></div>
|
||||
<div class="via via-4"></div>
|
||||
<div class="via via-5"></div>
|
||||
<div class="via via-6"></div>
|
||||
</div>
|
||||
<!-- Scanline sweep -->
|
||||
<div class="scanline"></div>
|
||||
<!-- Vignette -->
|
||||
<div class="css-vignette"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay">
|
||||
<div class="top-bar">
|
||||
<div class="logo-sprite" id="logo-sprite" aria-label="L'Electron Rare"></div>
|
||||
<span class="brand-mark">L'ELECTRON RARE</span>
|
||||
</div>
|
||||
|
||||
<div class="center-spacer">
|
||||
<div class="countdown-hero-text" id="hero-text">
|
||||
<h1 class="hero-title">
|
||||
<span class="hero-line hero-line-1">Systèmes</span>
|
||||
<span class="hero-line hero-line-2">électroniques</span>
|
||||
<span class="hero-line hero-line-3">spécifiques</span>
|
||||
</h1>
|
||||
<div class="hero-divider"></div>
|
||||
<p class="hero-sub">Conception · Mise au point · Fiabilisation</p>
|
||||
</div>
|
||||
<div class="tagline-area" id="tagline-bubble" aria-live="polite">
|
||||
<span class="tagline-cursor">></span>
|
||||
<span class="tagline-text" id="tagline"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-bar">
|
||||
<span class="bottom-left">electronique · automatisme · energie</span>
|
||||
<span class="bottom-right">mai 2026</span>
|
||||
</div>
|
||||
|
||||
<div class="cartouche">
|
||||
<p class="cartouche-title">L'électron rare</p>
|
||||
<p class="cartouche-copy">© 2026 Clément Saillant — systèmes électroniques spécifiques, conseil, formation — industrie, culture et projets multi-techniques.</p>
|
||||
<p class="cartouche-contact">Contact : <a href="https://fr.linkedin.com/in/electron-rare" target="_blank" rel="noopener">LinkedIn</a> · <a href="mailto:contact@lelectronrare.fr">contact@lelectronrare.fr</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)}
|
||||
</BaseLayout>
|
||||
|
||||
{!isLaunched && (
|
||||
<script>
|
||||
// ── Random mode pick ──
|
||||
const useWebGL = Math.random() < 0.5;
|
||||
const bgWebGL = document.getElementById('bg-webgl');
|
||||
const bgCSS = document.getElementById('bg-css');
|
||||
if (useWebGL) { bgCSS?.remove(); }
|
||||
else { bgWebGL?.remove(); }
|
||||
document.body.dataset.mode = useWebGL ? 'webgl' : 'css';
|
||||
|
||||
// ── Hero text reveal ──
|
||||
const heroText = document.getElementById('hero-text');
|
||||
if (heroText) {
|
||||
setTimeout(() => heroText.classList.add('revealed'), 300);
|
||||
}
|
||||
|
||||
// ── Tagline rotation ──
|
||||
const messages = [
|
||||
"Charge des condensateurs en cours...",
|
||||
"Calibration de l'oscilloscope...",
|
||||
"Soudure des derniers composants...",
|
||||
"Vérification de la BOM...",
|
||||
"Flash du firmware en cours...",
|
||||
"Mesure de continuité... bip.",
|
||||
"Routage des dernières pistes...",
|
||||
"Test de la chaîne d'alimentation...",
|
||||
"Programmation du bootloader...",
|
||||
"Revue de schéma en cours...",
|
||||
"Placement des composants CMS...",
|
||||
"Passage au four de refusion...",
|
||||
"Debug JTAG en cours...",
|
||||
"Vérification du plan de masse...",
|
||||
"Analyse du spectre RF..."
|
||||
];
|
||||
let msgIndex = Math.floor(Math.random() * messages.length);
|
||||
|
||||
function rotateMessage() {
|
||||
const bubble = document.getElementById('tagline-bubble');
|
||||
const el = document.getElementById('tagline');
|
||||
if (!bubble || !el) return;
|
||||
bubble.style.opacity = '0';
|
||||
bubble.style.transform = 'translateY(4px)';
|
||||
setTimeout(() => {
|
||||
el.textContent = messages[msgIndex];
|
||||
bubble.style.opacity = '1';
|
||||
bubble.style.transform = 'translateY(0)';
|
||||
msgIndex = (msgIndex + 1) % messages.length;
|
||||
}, 400);
|
||||
}
|
||||
|
||||
const sprite = document.getElementById('logo-sprite');
|
||||
if (sprite) sprite.style.backgroundImage = 'url(/assets/brand/logo-mark.png)';
|
||||
|
||||
rotateMessage();
|
||||
setInterval(rotateMessage, 8000);
|
||||
|
||||
// ── WebGL shader (only if picked) ──
|
||||
if (useWebGL) {
|
||||
(function () {
|
||||
var c = document.getElementById('webgl-countdown');
|
||||
if (!c) return;
|
||||
var cvs = document.createElement('canvas');
|
||||
cvs.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block;';
|
||||
c.appendChild(cvs);
|
||||
var gl = cvs.getContext('webgl') || cvs.getContext('experimental-webgl');
|
||||
if (!gl) { cvs.remove(); return; }
|
||||
|
||||
function mk(type, src) {
|
||||
var s = gl.createShader(type);
|
||||
gl.shaderSource(s, src);
|
||||
gl.compileShader(s);
|
||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
|
||||
console.warn('WebGL shader error:', gl.getShaderInfoLog(s));
|
||||
return null;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
var vs = mk(gl.VERTEX_SHADER, 'attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}');
|
||||
var fs = mk(gl.FRAGMENT_SHADER, [
|
||||
'precision mediump float;',
|
||||
'uniform float T;uniform vec2 R;',
|
||||
'float h(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5);}',
|
||||
'float h1(float n){return fract(sin(n)*43758.5);}',
|
||||
'void main(){',
|
||||
' vec2 uv=gl_FragCoord.xy/R;',
|
||||
' float t=T*.22;',
|
||||
' vec3 col=vec3(0.),cy=vec3(.357,.82,.847),wh=vec3(.7,.97,1.);',
|
||||
' float S=18.;',
|
||||
' vec2 g=fract(uv*S),cell=floor(uv*S);',
|
||||
' float lw=.03;',
|
||||
' float hH=h(cell),hV=h(cell.yx+7.3);',
|
||||
' float tH=smoothstep(lw,0.,abs(g.y-.5))*step(.38,hH);',
|
||||
' float tV=smoothstep(lw,0.,abs(g.x-.5))*step(.38,hV);',
|
||||
' float tr=max(tH,tV);',
|
||||
' col+=cy*tr*.2;',
|
||||
' float nd=smoothstep(.11,.01,length(g-.5));',
|
||||
' col+=cy*nd*step(.38,hH)*step(.38,hV)*.8;',
|
||||
' for(int i=0;i<8;i++){',
|
||||
' float fi=float(i),sp=.3+h1(fi*7.31)*.5,ph=h1(fi*3.17+9.);',
|
||||
' float rw=floor(h1(fi*5.71+1.)*S);',
|
||||
' float rm=step(0.,.499-abs(uv.y*S-rw-.5));',
|
||||
' if(rm>.0){',
|
||||
' float px=fract(t*sp+ph);',
|
||||
' float pi=smoothstep(.06,.0,abs(uv.x-px));',
|
||||
' float pl=smoothstep(lw*.5,.0,abs(g.y-.5));',
|
||||
' col+=wh*pi*pl*step(.38,h(vec2(floor(uv.x*S),rw)))*rm*2.5;',
|
||||
' }',
|
||||
' float cl=floor(h1(fi*4.13+11.)*S);',
|
||||
' float cm=step(0.,.499-abs(uv.x*S-cl-.5));',
|
||||
' if(cm>.0){',
|
||||
' float py=fract(t*sp*.75+ph+.5);',
|
||||
' float pi2=smoothstep(.06,.0,abs(uv.y-py));',
|
||||
' float pl2=smoothstep(lw*.5,.0,abs(g.x-.5));',
|
||||
' col+=wh*pi2*pl2*step(.38,h(vec2(cl,floor(uv.y*S))+7.3))*cm*2.5;',
|
||||
' }',
|
||||
' }',
|
||||
' col+=cy*tr*(sin(t*6.28)*.5+.5)*.05;',
|
||||
' col=1.-exp(-col*1.8);',
|
||||
' float v=length((uv-.5)*vec2(1.4,1.));',
|
||||
' col*=1.-smoothstep(.35,1.1,v);',
|
||||
' col*=.94+.06*sin(gl_FragCoord.y*3.14159);',
|
||||
' gl_FragColor=vec4(col,1.);',
|
||||
'}'
|
||||
].join('\n'));
|
||||
|
||||
if (!vs || !fs) return;
|
||||
var prog = gl.createProgram();
|
||||
gl.attachShader(prog, vs);
|
||||
gl.attachShader(prog, fs);
|
||||
gl.linkProgram(prog);
|
||||
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
||||
console.warn('WebGL link error:', gl.getProgramInfoLog(prog));
|
||||
return;
|
||||
}
|
||||
gl.useProgram(prog);
|
||||
|
||||
var buf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,1,1]), gl.STATIC_DRAW);
|
||||
var ap = gl.getAttribLocation(prog, 'p');
|
||||
gl.enableVertexAttribArray(ap);
|
||||
gl.vertexAttribPointer(ap, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
var uT = gl.getUniformLocation(prog, 'T');
|
||||
var uR = gl.getUniformLocation(prog, 'R');
|
||||
|
||||
function resize() {
|
||||
cvs.width = Math.round(cvs.offsetWidth * (window.devicePixelRatio || 1));
|
||||
cvs.height = Math.round(cvs.offsetHeight * (window.devicePixelRatio || 1));
|
||||
gl.viewport(0, 0, cvs.width, cvs.height);
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize, { passive: true });
|
||||
|
||||
var t0 = performance.now();
|
||||
function frame() {
|
||||
gl.uniform1f(uT, (performance.now() - t0) * .001);
|
||||
gl.uniform2f(uR, cvs.width, cvs.height);
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
frame();
|
||||
})();
|
||||
}
|
||||
|
||||
// ── CSS mode: animate random node blinks ──
|
||||
if (!useWebGL) {
|
||||
const nodes = document.querySelectorAll('.node');
|
||||
function blinkNodes() {
|
||||
nodes.forEach(n => {
|
||||
if (Math.random() < 0.3) {
|
||||
n.classList.add('node-active');
|
||||
setTimeout(() => n.classList.remove('node-active'), 600 + Math.random() * 1200);
|
||||
}
|
||||
});
|
||||
}
|
||||
blinkNodes();
|
||||
setInterval(blinkNodes, 2000);
|
||||
}
|
||||
</script>
|
||||
)}
|
||||
|
||||
<style>
|
||||
/* ── Base countdown ── */
|
||||
.countdown-page { position:relative; min-height:100vh; overflow:hidden; background:#000; color:#fff; font-family:'Manrope',-apple-system,sans-serif; }
|
||||
.countdown-bg { position:fixed; inset:0; z-index:0; width:100vw; height:100vh; }
|
||||
.webgl-bg { position:absolute; inset:0; width:100%; height:100%; }
|
||||
.overlay { position:relative; z-index:10; min-height:100vh; display:flex; flex-direction:column; justify-content:space-between; padding:clamp(20px,3vw,40px) clamp(24px,5vw,60px); pointer-events:none; }
|
||||
.overlay > * { pointer-events:none; }
|
||||
.overlay a { pointer-events:auto; }
|
||||
|
||||
/* ── Top bar ── */
|
||||
.top-bar { display:flex; align-items:center; gap:16px; }
|
||||
.logo-sprite { width:48px; height:48px; background-size:contain; background-repeat:no-repeat; filter:brightness(1.2) drop-shadow(0 0 12px rgba(91,209,216,0.4)); }
|
||||
.brand-mark { font-size:11px; font-weight:600; letter-spacing:0.25em; color:rgba(255,255,255,0.5); text-transform:uppercase; }
|
||||
|
||||
/* ── Hero text ── */
|
||||
.center-spacer { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:24px; }
|
||||
.countdown-hero-text { text-align:center; opacity:0; transform:translateY(20px); transition:opacity 1.2s ease, transform 1.2s ease; }
|
||||
.countdown-hero-text.revealed { opacity:1; transform:translateY(0); }
|
||||
.hero-title { margin:0; line-height:1.1; }
|
||||
.hero-line { display:block; font-weight:800; letter-spacing:-0.03em; color:rgba(255,255,255,0.85); }
|
||||
.hero-line-1 { font-size:clamp(32px,5vw,64px); opacity:0; animation:fadeUp 0.8s ease 0.4s forwards; }
|
||||
.hero-line-2 { font-size:clamp(36px,6vw,72px); color:#5bd1d8; opacity:0; animation:fadeUp 0.8s ease 0.7s forwards; }
|
||||
.hero-line-3 { font-size:clamp(32px,5vw,64px); opacity:0; animation:fadeUp 0.8s ease 1.0s forwards; }
|
||||
.hero-divider { width:60px; height:2px; margin:16px auto; background:linear-gradient(90deg,transparent,#5bd1d8,transparent); opacity:0; animation:fadeUp 0.6s ease 1.3s forwards; }
|
||||
.hero-sub { font-size:clamp(12px,1.4vw,16px); color:rgba(255,255,255,0.35); letter-spacing:0.15em; text-transform:uppercase; margin:0; opacity:0; animation:fadeUp 0.6s ease 1.5s forwards; }
|
||||
@keyframes fadeUp { to { opacity:1; transform:translateY(0); } from { opacity:0; transform:translateY(12px); } }
|
||||
|
||||
/* ── Tagline ── */
|
||||
.tagline-area { display:flex; align-items:center; gap:8px; padding:10px 20px; background:rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.06); border-radius:8px; transition:opacity 0.4s ease, transform 0.4s ease; pointer-events:auto; }
|
||||
.tagline-cursor { font-family:monospace; font-size:14px; color:#5bd1d8; animation:blink 1s step-end infinite; }
|
||||
@keyframes blink { 50% { opacity:0; } }
|
||||
.tagline-text { font-family:monospace; font-size:clamp(11px,1.3vw,14px); color:rgba(255,255,255,0.4); }
|
||||
|
||||
/* ── Bottom bar ── */
|
||||
.bottom-bar { display:flex; justify-content:space-between; align-items:flex-end; }
|
||||
.bottom-left, .bottom-right { font-size:clamp(10px,1.1vw,12px); color:rgba(255,255,255,0.15); letter-spacing:0.08em; text-transform:uppercase; }
|
||||
|
||||
/* ── Cartouche ── */
|
||||
.cartouche { position:fixed; bottom:clamp(16px,2vw,24px); right:clamp(16px,2vw,24px); max-width:320px; padding:14px 18px; background:rgba(0,0,0,0.5); backdrop-filter:blur(12px); border:1px solid rgba(255,255,255,0.06); border-radius:12px; pointer-events:auto; }
|
||||
.cartouche-title { font-size:13px; font-weight:700; color:rgba(255,255,255,0.6); margin:0 0 6px; }
|
||||
.cartouche-copy { font-size:9px; color:rgba(255,255,255,0.25); margin:0 0 4px; line-height:1.4; }
|
||||
.cartouche-contact { font-size:9px; color:rgba(255,255,255,0.25); margin:0; }
|
||||
.cartouche-contact a { color:#5bd1d8; text-decoration:none; }
|
||||
|
||||
/* ── CSS Circuit Board Background ── */
|
||||
.css-circuit { position:absolute; inset:0; background:#020a0d; overflow:hidden; }
|
||||
.circuit-grid { position:absolute; inset:5%; }
|
||||
|
||||
/* Traces */
|
||||
.trace { position:absolute; background:rgba(91,209,216,0.08); }
|
||||
.trace-h { height:1px; left:0; right:0; }
|
||||
.trace-v { width:1px; top:0; bottom:0; }
|
||||
.trace-h1 { top:8%; } .trace-h2 { top:22%; } .trace-h3 { top:38%; }
|
||||
.trace-h4 { top:55%; } .trace-h5 { top:72%; } .trace-h6 { top:88%; }
|
||||
.trace-v1 { left:10%; } .trace-v2 { left:25%; } .trace-v3 { left:42%; }
|
||||
.trace-v4 { left:58%; } .trace-v5 { left:75%; } .trace-v6 { left:90%; }
|
||||
|
||||
/* Pulse on traces */
|
||||
.trace .pulse { position:absolute; border-radius:50%; }
|
||||
.trace-h .pulse { width:40px; height:3px; top:-1px; background:linear-gradient(90deg,transparent,#5bd1d8,transparent); animation:pulseH 4s linear infinite; }
|
||||
.trace-v .pulse { width:3px; height:40px; left:-1px; background:linear-gradient(180deg,transparent,#5bd1d8,transparent); animation:pulseV 5s linear infinite; }
|
||||
.trace-h1 .pulse { animation-delay:0s; } .trace-h2 .pulse { animation-delay:0.7s; } .trace-h3 .pulse { animation-delay:1.4s; }
|
||||
.trace-h4 .pulse { animation-delay:2.1s; } .trace-h5 .pulse { animation-delay:0.3s; } .trace-h6 .pulse { animation-delay:1.8s; }
|
||||
.trace-v1 .pulse { animation-delay:0.5s; } .trace-v2 .pulse { animation-delay:1.2s; } .trace-v3 .pulse { animation-delay:2.5s; }
|
||||
.trace-v4 .pulse { animation-delay:0.9s; } .trace-v5 .pulse { animation-delay:1.7s; } .trace-v6 .pulse { animation-delay:3.1s; }
|
||||
@keyframes pulseH { 0% { left:-40px; } 100% { left:100%; } }
|
||||
@keyframes pulseV { 0% { top:-40px; } 100% { top:100%; } }
|
||||
|
||||
/* Nodes */
|
||||
.node { position:absolute; width:6px; height:6px; border-radius:50%; background:rgba(91,209,216,0.15); border:1px solid rgba(91,209,216,0.2); transition:all 0.6s ease; }
|
||||
.node-active { background:#5bd1d8; box-shadow:0 0 12px rgba(91,209,216,0.6), 0 0 24px rgba(91,209,216,0.3); }
|
||||
.node-1 { top:8%; left:10%; } .node-2 { top:8%; left:42%; } .node-3 { top:8%; left:75%; }
|
||||
.node-4 { top:22%; left:25%; } .node-5 { top:38%; left:58%; } .node-6 { top:38%; left:90%; }
|
||||
.node-7 { top:55%; left:10%; } .node-8 { top:55%; left:42%; } .node-9 { top:72%; left:25%; }
|
||||
.node-10 { top:72%; left:75%; } .node-11 { top:88%; left:42%; } .node-12 { top:88%; left:58%; }
|
||||
|
||||
/* IC chips */
|
||||
.ic { position:absolute; background:rgba(91,209,216,0.04); border:1px solid rgba(91,209,216,0.12); border-radius:2px; display:flex; align-items:center; justify-content:center; }
|
||||
.ic-label { font-family:monospace; font-size:8px; color:rgba(91,209,216,0.4); letter-spacing:0.1em; }
|
||||
.ic-pins { position:absolute; }
|
||||
.ic-pins-l { left:-4px; top:20%; bottom:20%; width:4px; border-right:1px solid rgba(91,209,216,0.15); background:repeating-linear-gradient(180deg,transparent,transparent 3px,rgba(91,209,216,0.1) 3px,rgba(91,209,216,0.1) 5px); }
|
||||
.ic-pins-r { right:-4px; top:20%; bottom:20%; width:4px; border-left:1px solid rgba(91,209,216,0.15); background:repeating-linear-gradient(180deg,transparent,transparent 3px,rgba(91,209,216,0.1) 3px,rgba(91,209,216,0.1) 5px); }
|
||||
.ic-1 { width:60px; height:36px; top:18%; left:55%; }
|
||||
.ic-2 { width:50px; height:30px; top:50%; left:20%; }
|
||||
.ic-3 { width:44px; height:24px; top:78%; left:68%; }
|
||||
|
||||
/* SMD resistors/caps */
|
||||
.smd { position:absolute; width:12px; height:5px; background:rgba(241,194,122,0.08); border:1px solid rgba(241,194,122,0.15); border-radius:1px; }
|
||||
.smd-1 { top:12%; left:32%; } .smd-2 { top:15%; left:65%; transform:rotate(90deg); }
|
||||
.smd-3 { top:30%; left:15%; } .smd-4 { top:45%; left:80%; transform:rotate(90deg); }
|
||||
.smd-5 { top:60%; left:48%; } .smd-6 { top:65%; left:35%; transform:rotate(90deg); }
|
||||
.smd-7 { top:82%; left:52%; } .smd-8 { top:35%; left:72%; }
|
||||
|
||||
/* Vias */
|
||||
.via { position:absolute; width:8px; height:8px; border-radius:50%; border:1px solid rgba(91,209,216,0.2); background:radial-gradient(circle,rgba(91,209,216,0.15) 30%,transparent 70%); }
|
||||
.via::after { content:''; position:absolute; inset:2px; border-radius:50%; background:rgba(91,209,216,0.1); }
|
||||
.via-1 { top:14%; left:48%; } .via-2 { top:28%; left:82%; } .via-3 { top:42%; left:18%; }
|
||||
.via-4 { top:62%; left:62%; } .via-5 { top:76%; left:38%; } .via-6 { top:85%; left:85%; }
|
||||
|
||||
/* Scanline sweep */
|
||||
.scanline { position:absolute; inset:0; background:linear-gradient(180deg,transparent 0%,transparent 48%,rgba(91,209,216,0.03) 50%,transparent 52%,transparent 100%); background-size:100% 200%; animation:scanline 8s linear infinite; pointer-events:none; }
|
||||
@keyframes scanline { 0% { background-position:0 0; } 100% { background-position:0 100%; } }
|
||||
|
||||
/* CSS vignette */
|
||||
.css-vignette { position:absolute; inset:0; background:radial-gradient(ellipse at center,transparent 30%,rgba(0,0,0,0.7) 100%); pointer-events:none; }
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media (max-width:640px) {
|
||||
.bottom-bar { flex-direction:column; align-items:center; gap:4px; }
|
||||
.cartouche { position:relative; bottom:auto; right:auto; max-width:100%; margin-top:12px; text-align:center; }
|
||||
.hero-line-1, .hero-line-3 { font-size:28px; }
|
||||
.hero-line-2 { font-size:32px; }
|
||||
}
|
||||
|
||||
/* ── Reduced motion ── */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.trace .pulse, .scanline { animation:none !important; }
|
||||
.hero-line-1, .hero-line-2, .hero-line-3, .hero-divider, .hero-sub { animation:none !important; opacity:1 !important; }
|
||||
.countdown-hero-text { opacity:1 !important; transform:none !important; transition:none !important; }
|
||||
}
|
||||
</style>
|
||||
+357
-180
@@ -1,207 +1,384 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import '@/styles/global.css';
|
||||
import { BmuViewer } from '@/components/BmuViewer';
|
||||
---
|
||||
|
||||
<BaseLayout title="L'Electron Rare — Bientôt" description="L'Electron Rare arrive. Systèmes électroniques spécifiques, formations, prototypage.">
|
||||
<main class="countdown-page">
|
||||
<svg class="atom" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-cyan-soft, #5bd1d866)" stroke-width="0.8" transform="rotate(-30 150 150)"/>
|
||||
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-amber-soft, #f1c27a66)" stroke-width="0.8" transform="rotate(30 150 150)"/>
|
||||
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-green-soft, #b6d18f44)" stroke-width="0.8" transform="rotate(90 150 150)"/>
|
||||
<circle cx="150" cy="150" r="18" fill="url(#nucleusGlow)"/>
|
||||
<circle cx="150" cy="150" r="8" fill="var(--accent, #f1c27a)"/>
|
||||
<circle r="5" fill="var(--electric, #5bd1d8)">
|
||||
<animateMotion dur="3s" repeatCount="indefinite"><mpath href="#o1"/></animateMotion>
|
||||
</circle>
|
||||
<circle r="10" fill="var(--electric, #5bd1d8)" opacity="0.3">
|
||||
<animateMotion dur="3s" repeatCount="indefinite"><mpath href="#o1"/></animateMotion>
|
||||
</circle>
|
||||
<circle r="4" fill="var(--accent, #f1c27a)">
|
||||
<animateMotion dur="4.2s" repeatCount="indefinite"><mpath href="#o2"/></animateMotion>
|
||||
</circle>
|
||||
<circle r="9" fill="var(--accent, #f1c27a)" opacity="0.25">
|
||||
<animateMotion dur="4.2s" repeatCount="indefinite"><mpath href="#o2"/></animateMotion>
|
||||
</circle>
|
||||
<circle r="3" fill="var(--trace-green, #b6d18f)">
|
||||
<animateMotion dur="5.8s" repeatCount="indefinite"><mpath href="#o3"/></animateMotion>
|
||||
</circle>
|
||||
<circle r="8" fill="var(--trace-green, #b6d18f)" opacity="0.2">
|
||||
<animateMotion dur="5.8s" repeatCount="indefinite"><mpath href="#o3"/></animateMotion>
|
||||
</circle>
|
||||
<defs>
|
||||
<ellipse id="o1" cx="150" cy="150" rx="120" ry="45" transform="rotate(-30 150 150)" fill="none"/>
|
||||
<ellipse id="o2" cx="150" cy="150" rx="120" ry="45" transform="rotate(30 150 150)" fill="none"/>
|
||||
<ellipse id="o3" cx="150" cy="150" rx="120" ry="45" transform="rotate(90 150 150)" fill="none"/>
|
||||
<radialGradient id="nucleusGlow">
|
||||
<stop offset="0%" stop-color="var(--accent, #f1c27a)" stop-opacity="0.6"/>
|
||||
<stop offset="100%" stop-color="var(--accent, #f1c27a)" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>L'électron rare — Systèmes électroniques spécifiques</title>
|
||||
<meta name="description" content="Conception, mise au point et fiabilisation de systèmes électroniques spécifiques, automatismes et ensembles liés à l'énergie. Consulting et formation PCB, KiCad, µC." />
|
||||
<meta property="og:title" content="L'électron rare — Systèmes électroniques spécifiques" />
|
||||
<meta property="og:description" content="Conception, mise au point et fiabilisation de systèmes électroniques spécifiques. Consulting et formation." />
|
||||
<meta property="og:image" content="https://www.lelectronrare.fr/assets/og-cover.jpg" />
|
||||
<meta property="og:url" content="https://www.lelectronrare.fr/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="L'électron rare" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="L'électron rare — Systèmes électroniques spécifiques" />
|
||||
<meta name="twitter:description" content="Conception, mise au point et fiabilisation de systèmes électroniques spécifiques." />
|
||||
<meta name="twitter:image" content="https://www.lelectronrare.fr/assets/og-cover.jpg" />
|
||||
<link rel="me" href="https://mastodon.saillant.cc/@clement" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="preload" href="/assets/models3d/bmu_v2.glb" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/assets/models3d/i2c_repeater.glb" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/assets/models3d/mosfet_switch.glb" as="fetch" crossorigin />
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ProfessionalService",
|
||||
"name": "L'électron rare",
|
||||
"description": "Conception, mise au point et fiabilisation de systèmes électroniques spécifiques, automatismes et ensembles liés à l'énergie.",
|
||||
"url": "https://www.lelectronrare.fr",
|
||||
"email": "contact@lelectronrare.fr",
|
||||
"founder": { "@type": "Person", "name": "Clément Saillant" },
|
||||
"areaServed": "France",
|
||||
"serviceType": ["Conception électronique", "Formation PCB et embarqué", "Consulting technique", "Automatisme industriel"],
|
||||
"sameAs": ["https://fr.linkedin.com/in/electron-rare", "https://github.com/electron-rare/", "https://mastodon.saillant.cc/@clement"]
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { background: #06080b; color: #fff; overflow-x: hidden; }
|
||||
body { background: #06080b; min-height: 100vh; font-family: 'Courier New', monospace; }
|
||||
.scroll-track { height: 1000vh; position: relative; z-index: 1; }
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
|
||||
|
||||
<h1 class="title">L'<span class="electric">é</span>lectron rare</h1>
|
||||
/* Loader */
|
||||
.loader { position: fixed; inset: 0; z-index: 9999; background: #06080b; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; transition: opacity 0.8s ease, visibility 0.8s ease; }
|
||||
.loader.done { opacity: 0; visibility: hidden; pointer-events: none; }
|
||||
.loader-logo { width: clamp(60px, 8vw, 100px); height: auto; filter: drop-shadow(0 0 20px rgba(91,209,216,0.3)); }
|
||||
.loader-name { font-size: clamp(20px, 2.5vw, 32px); font-weight: 800; color: rgba(255,255,255,0.8); letter-spacing: 0.06em; }
|
||||
.loader-bar { width: clamp(180px, 30vw, 280px); height: 2px; background: rgba(255,255,255,0.06); border-radius: 1px; overflow: hidden; }
|
||||
.loader-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #5bd1d8, #a78bfa); transition: width 0.3s ease; }
|
||||
.loader-status { font-size: 10px; color: rgba(255,255,255,0.25); letter-spacing: 0.12em; text-transform: uppercase; }
|
||||
.loader-tagline { font-size: 12px; color: rgba(255,255,255,0.3); letter-spacing: 0.08em; margin-bottom: 20px; }
|
||||
.loader-actions { margin-top: 24px; }
|
||||
.loader-skip { display: inline-block; padding: 10px 24px; border: 1px solid rgba(91,209,216,0.3); border-radius: 8px; color: rgba(91,209,216,0.7); font-size: 12px; text-decoration: none; letter-spacing: 0.04em; transition: all 0.2s; }
|
||||
.loader-skip:hover { border-color: #5bd1d8; color: #5bd1d8; background: rgba(91,209,216,0.08); }
|
||||
.loader-a11y { margin-top: 20px; font-size: 10px; color: rgba(255,255,255,0.15); }
|
||||
.loader-a11y a { color: rgba(255,255,255,0.25); text-decoration: none; }
|
||||
.loader-a11y a:hover { color: #5bd1d8; }
|
||||
|
||||
<div class="timer" id="timer">
|
||||
<div class="unit"><span class="num" id="days">--</span><span class="label">jours</span></div>
|
||||
<div class="sep">:</div>
|
||||
<div class="unit"><span class="num" id="hours">--</span><span class="label">heures</span></div>
|
||||
<div class="sep">:</div>
|
||||
<div class="unit"><span class="num" id="minutes">--</span><span class="label">minutes</span></div>
|
||||
<div class="sep">:</div>
|
||||
<div class="unit"><span class="num" id="seconds">--</span><span class="label">secondes</span></div>
|
||||
.ov { position: fixed; z-index: 3; pointer-events: none; opacity: 0; transform: translateY(6px); transition: opacity 0.5s, transform 0.5s; }
|
||||
.ov.on { opacity: 1; transform: translateY(0); }
|
||||
|
||||
/* Hero */
|
||||
.ov-hero { inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; }
|
||||
.hero-bottom { display: flex; flex-direction: column; align-items: center; padding-bottom: clamp(60px, 10vh, 120px); }
|
||||
.hero-t { text-align: center; margin: 0; line-height: 1.1; }
|
||||
.hl1,.hl3 { display: block; font-size: clamp(28px, 4.5vw, 56px); font-weight: 800; color: rgba(255,255,255,0.85); letter-spacing: -0.02em; }
|
||||
.hl2 { display: block; font-size: clamp(32px, 5.5vw, 64px); font-weight: 800; color: #5bd1d8; letter-spacing: -0.02em; }
|
||||
.hero-s { font-size: clamp(11px, 1.2vw, 15px); color: rgba(255,255,255,0.3); letter-spacing: 0.15em; text-transform: uppercase; margin: 16px 0 0; }
|
||||
.hero-s2 { font-size: clamp(10px, 1vw, 13px); color: rgba(255,255,255,0.2); letter-spacing: 0.12em; text-transform: uppercase; margin: 8px 0 0; }
|
||||
|
||||
/* Header */
|
||||
.site-hdr { position: fixed; top: 0; left: 0; right: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; padding: 16px clamp(20px, 3vw, 40px); pointer-events: none; }
|
||||
.hdr-left { display: flex; align-items: center; gap: 10px; pointer-events: auto; }
|
||||
.hdr-logo { width: 36px; height: auto; filter: drop-shadow(0 0 12px rgba(91,209,216,0.3)); }
|
||||
.hdr-name { font-size: 16px; font-weight: 800; color: rgba(255,255,255,0.8); letter-spacing: 0.04em; }
|
||||
.hdr-nav { display: flex; align-items: center; gap: 8px; pointer-events: auto; }
|
||||
.hdr-link { padding: 8px 16px; color: rgba(255,255,255,0.6); font-size: 12px; font-weight: 600; text-decoration: none; letter-spacing: 0.04em; transition: color 0.2s; }
|
||||
.hdr-link:hover { color: #5bd1d8; }
|
||||
.hdr-social { font-weight: 800; font-size: 11px; text-transform: uppercase; padding: 8px 10px; opacity: 0.5; }
|
||||
.hdr-social:hover { opacity: 1; }
|
||||
.hdr-cta { padding: 8px 20px; border-radius: 6px; background: rgba(91,209,216,0.15); border: 1px solid rgba(91,209,216,0.3); color: #5bd1d8; font-size: 12px; font-weight: 700; text-decoration: none; letter-spacing: 0.05em; transition: background 0.2s; }
|
||||
.hdr-cta:hover { background: rgba(91,209,216,0.25); }
|
||||
.burger { display: none; background: none; border: none; color: #fff; font-size: 24px; cursor: pointer; padding: 8px; pointer-events: auto; }
|
||||
.mobile-menu { display: none; position: fixed; inset: 0; z-index: 100; background: rgba(6,8,11,0.95); backdrop-filter: blur(16px); flex-direction: column; align-items: center; justify-content: center; gap: 24px; }
|
||||
.mobile-menu.open { display: flex; }
|
||||
.mobile-menu a { font-size: 20px; font-weight: 700; color: rgba(255,255,255,0.8); text-decoration: none; letter-spacing: 0.05em; padding: 12px 24px; }
|
||||
.mobile-menu .close-btn { position: absolute; top: 20px; right: 20px; background: none; border: none; color: #fff; font-size: 28px; cursor: pointer; }
|
||||
@media (max-width: 768px) { .hdr-nav { display: none; } .burger { display: block; } }
|
||||
|
||||
/* Formats */
|
||||
.ov-formats { bottom: clamp(30px, 5vh, 60px); left: 50%; transform: translateX(-50%); display: flex; gap: 12px; max-width: 820px; width: 92vw; }
|
||||
.ov-formats.on { transform: translateX(-50%); }
|
||||
.fmt { flex: 1; padding: 14px 16px; border-radius: 8px; background: rgba(6,8,11,0.7); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.05); text-align: center; text-decoration: none; color: inherit; cursor: pointer; transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease; }
|
||||
.fmt:hover { background: rgba(6,8,11,0.85); border-color: rgba(255,255,255,0.18); transform: translateY(-2px); }
|
||||
.fmt-t { font-size: 13px; font-weight: 700; color: #fff; letter-spacing: 0.06em; text-transform: uppercase; margin-bottom: 4px; }
|
||||
.fmt-d { font-size: 10px; color: rgba(255,255,255,0.35); }
|
||||
|
||||
/* Progress */
|
||||
.prog { position: fixed; top: 0; left: 0; height: 2px; z-index: 5; background: linear-gradient(90deg, #5bd1d8, #a78bfa, #f59e0b); width: 0%; }
|
||||
.inf { position: fixed; bottom: 10px; right: 14px; z-index: 4; font-size: 8px; color: rgba(255,255,255,0.1); letter-spacing: 0.06em; }
|
||||
|
||||
/* Dots */
|
||||
.dots { position: fixed; right: 20px; top: 50%; transform: translateY(-50%); z-index: 4; display: flex; flex-direction: column; gap: 14px; }
|
||||
.dot { width: 12px; height: 12px; border-radius: 50%; background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.1); transition: background 0.3s, transform 0.3s, border-color 0.3s; cursor: pointer; pointer-events: auto; position: relative; }
|
||||
.dot.on { background: #5bd1d8; border-color: #5bd1d8; transform: scale(1.3); box-shadow: 0 0 10px rgba(91,209,216,0.4); }
|
||||
.dot:hover { background: rgba(91,209,216,0.5); border-color: rgba(91,209,216,0.5); }
|
||||
.dot[data-l]:not([data-l=""])::after { content: attr(data-l); position: absolute; right: 20px; top: 50%; transform: translateY(-50%); font-size: 9px; color: rgba(255,255,255,0.5); letter-spacing: 0.05em; white-space: nowrap; opacity: 0; transition: opacity 0.2s; pointer-events: none; }
|
||||
.dot[data-l]:not([data-l=""]):hover::after { opacity: 1; }
|
||||
|
||||
/* Trust */
|
||||
.trust { position: fixed; bottom: 12px; left: clamp(16px, 3vw, 40px); z-index: 3; pointer-events: none; }
|
||||
.trust-k { font-size: 9px; color: rgba(255,255,255,0.2); letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 4px; }
|
||||
.trust-names { font-size: 11px; color: rgba(255,255,255,0.3); letter-spacing: 0.04em; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ov-formats { flex-direction: column; gap: 6px; }
|
||||
.trust { display: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-da-variant="preview">
|
||||
<!-- Loader -->
|
||||
<div class="loader" id="loader">
|
||||
<img src="/assets/brand/logo-mark.svg" alt="L'électron rare" class="loader-logo" />
|
||||
<p class="loader-name">L'électron rare</p>
|
||||
<p class="loader-tagline">Systèmes électroniques spécifiques</p>
|
||||
<div class="loader-bar"><div class="loader-fill" id="loader-fill"></div></div>
|
||||
<p class="loader-status" id="loader-status">Chargement des modèles 3D...</p>
|
||||
<div class="loader-actions">
|
||||
<a href="/conception/" class="loader-skip">Accéder au site sans 3D →</a>
|
||||
</div>
|
||||
<p class="loader-a11y">Version accessible : <a href="/conception/">Conception</a> · <a href="/formation/">Formation</a> · <a href="/consulting/">Consulting</a> · <a href="/contact/">Contact</a></p>
|
||||
</div>
|
||||
|
||||
<p class="tagline" id="tagline"></p>
|
||||
<BmuViewer client:load />
|
||||
<div class="prog" id="prog"></div>
|
||||
|
||||
<p class="footnote">Systèmes électroniques spécifiques · Formations · Prototypage</p>
|
||||
</main>
|
||||
</BaseLayout>
|
||||
<div class="dots" id="dots">
|
||||
<div class="dot on" data-s="hero" data-p="0.03" data-l="Hero"></div>
|
||||
<div class="dot" data-s="hero-to-exp" data-p="0.09" data-l=""></div>
|
||||
<div class="dot" data-s="exp-elec" data-p="0.14" data-l="Élec"></div>
|
||||
<div class="dot" data-s="exp-auto" data-p="0.18" data-l="Auto"></div>
|
||||
<div class="dot" data-s="exp-energie" data-p="0.22" data-l="Énergie"></div>
|
||||
<div class="dot" data-s="exp-dispositifs" data-p="0.255" data-l="Dispositifs"></div>
|
||||
<div class="dot" data-s="exp-consulting" data-p="0.285" data-l="Consulting"></div>
|
||||
<div class="dot" data-s="eclate-recul" data-p="0.31" data-l=""></div>
|
||||
<div class="dot" data-s="eclate-sep" data-p="0.34" data-l=""></div>
|
||||
<div class="dot" data-s="eclate-plat" data-p="0.36" data-l=""></div>
|
||||
<div class="dot" data-s="cas" data-p="0.53" data-l="Cas concrets"></div>
|
||||
<div class="dot" data-s="media" data-p="0.70" data-l="Médias"></div>
|
||||
<div class="dot" data-s="formats" data-p="0.85" data-l="Services"></div>
|
||||
<div class="dot" data-s="contact" data-p="0.95" data-l="Contact"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const LAUNCH = new Date('2026-05-01T00:00:00+02:00').getTime();
|
||||
<!-- HEADER -->
|
||||
<header class="site-hdr">
|
||||
<div class="hdr-left">
|
||||
<img src="/assets/brand/logo-mark.svg" alt="" class="hdr-logo" />
|
||||
<span class="hdr-name">L'électron rare</span>
|
||||
</div>
|
||||
<nav class="hdr-nav">
|
||||
<a href="/conception/" class="hdr-link">Conception</a>
|
||||
<a href="/formation/" class="hdr-link">Formation</a>
|
||||
<a href="/consulting/" class="hdr-link">Consulting</a>
|
||||
<a href="https://fr.linkedin.com/in/electron-rare" target="_blank" rel="noopener" class="hdr-link hdr-social" title="LinkedIn">in</a>
|
||||
<a href="https://github.com/electron-rare/" target="_blank" rel="noopener" class="hdr-link hdr-social" title="GitHub">gh</a>
|
||||
<a href="https://mastodon.saillant.cc/@clement" target="_blank" rel="me noopener" class="hdr-link hdr-social" title="Mastodon">mt</a>
|
||||
<a href="/contact/" class="hdr-cta">Contact</a>
|
||||
</nav>
|
||||
<button class="burger" id="burger-btn" aria-label="Menu">☰</button>
|
||||
</header>
|
||||
<div class="mobile-menu" id="mobile-menu">
|
||||
<button class="close-btn" id="close-menu">✕</button>
|
||||
<a href="/">Accueil</a><a href="/conception/">Conception</a><a href="/formation/">Formation</a><a href="/consulting/">Consulting</a><a href="/contact/">Contact</a>
|
||||
</div>
|
||||
|
||||
const messages = [
|
||||
"Charge des condensateurs en cours...",
|
||||
"Calibration de l'oscilloscope...",
|
||||
"Soudure des derniers composants...",
|
||||
"Vérification de la BOM...",
|
||||
"Flash du firmware en cours...",
|
||||
"Mesure de continuité... bip.",
|
||||
"Routage des dernières pistes...",
|
||||
"Test de la chaîne d'alimentation...",
|
||||
"Programmation du bootloader...",
|
||||
"Revue de schéma en cours...",
|
||||
"Placement des composants CMS...",
|
||||
"Passage au four de refusion...",
|
||||
"Debug JTAG en cours...",
|
||||
"Vérification du plan de masse...",
|
||||
"Analyse du spectre RF..."
|
||||
];
|
||||
<!-- HERO -->
|
||||
<div class="ov ov-hero on" data-sec="hero">
|
||||
<div class="hero-bottom">
|
||||
<h1 class="hero-t">
|
||||
<span class="hl1">Systèmes</span>
|
||||
<span class="hl2">électroniques</span>
|
||||
<span class="hl3">spécifiques</span>
|
||||
</h1>
|
||||
<p class="hero-s">électronique · automatisme · énergie</p>
|
||||
<p class="hero-s2">Conception · Mise au point · Fiabilisation · Consulting · Formation</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
let msgIndex = Math.floor(Math.random() * messages.length);
|
||||
<!-- FORMATS -->
|
||||
<div class="ov ov-formats" data-sec="formats">
|
||||
<a class="fmt" href="/conception/">
|
||||
<p class="fmt-t" style="color:#5bd1d8">Conception</p>
|
||||
<p class="fmt-d">Schéma · PCB · Firmware · Validation</p>
|
||||
</a>
|
||||
<a class="fmt" href="/formation/">
|
||||
<p class="fmt-t" style="color:#a78bfa">Formation</p>
|
||||
<p class="fmt-d">PCB · KiCad · µC · Soudure</p>
|
||||
</a>
|
||||
<a class="fmt" href="/consulting/">
|
||||
<p class="fmt-t" style="color:#f59e0b">Consulting</p>
|
||||
<p class="fmt-d">Audit · Diagnostic · Transfert</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
function pad(n) { return String(n).padStart(2, '0'); }
|
||||
<!-- CONFIANCE -->
|
||||
<div class="trust" id="trust">
|
||||
<p class="trust-k">Ils m'ont fait confiance</p>
|
||||
<p class="trust-names">KXKM · Spectacle vivant · Secteur ferroviaire · Industrie automatisée · Écoles d'art numérique</p>
|
||||
</div>
|
||||
|
||||
function update() {
|
||||
const now = Date.now();
|
||||
const diff = Math.max(0, LAUNCH - now);
|
||||
const d = Math.floor(diff / 86400000);
|
||||
const h = Math.floor((diff % 86400000) / 3600000);
|
||||
const m = Math.floor((diff % 3600000) / 60000);
|
||||
const s = Math.floor((diff % 60000) / 1000);
|
||||
document.getElementById('days').textContent = pad(d);
|
||||
document.getElementById('hours').textContent = pad(h);
|
||||
document.getElementById('minutes').textContent = pad(m);
|
||||
document.getElementById('seconds').textContent = pad(s);
|
||||
}
|
||||
<div class="scroll-track"></div>
|
||||
|
||||
function rotateMessage() {
|
||||
const el = document.getElementById('tagline');
|
||||
el.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
el.textContent = messages[msgIndex];
|
||||
el.style.opacity = '1';
|
||||
msgIndex = (msgIndex + 1) % messages.length;
|
||||
}, 400);
|
||||
}
|
||||
<!-- SEO: texte indexable caché -->
|
||||
<div class="sr-only" aria-hidden="false">
|
||||
<h1>L'électron rare — Systèmes électroniques spécifiques</h1>
|
||||
<p>Conception, mise au point et fiabilisation de systèmes électroniques spécifiques, automatismes et ensembles liés à l'énergie, au stockage et à l'optimisation terrain.</p>
|
||||
<h2>Domaines d'expertise</h2>
|
||||
<ul>
|
||||
<li>Électronique spécifique : cartes, interfaces, capteurs, alimentation</li>
|
||||
<li>Instrumentation et automatisme : bancs, automates, variateurs, protocoles terrain</li>
|
||||
<li>Énergie et stockage : batterie, conversion, supervision, télémétrie</li>
|
||||
<li>Dispositifs pour le réel : audio, LED, scène, robustesse terrain</li>
|
||||
<li>Consulting et formation : audit, transfert, pédagogie, projet</li>
|
||||
</ul>
|
||||
<h2>Cas concrets</h2>
|
||||
<ul>
|
||||
<li>Industries créatives : système audio embarqué sur batterie pour le spectacle (KXKM)</li>
|
||||
<li>Industrie : diagnostic et remise en service d'une ligne automatisée</li>
|
||||
<li>Formation : formation PCB et embarqué pour une école d'art numérique</li>
|
||||
</ul>
|
||||
<h2>Services</h2>
|
||||
<ul>
|
||||
<li>Conception : schéma, PCB, firmware, validation</li>
|
||||
<li>Formation : PCB, KiCad, microcontrôleurs, soudure</li>
|
||||
<li>Consulting : audit, diagnostic, transfert</li>
|
||||
</ul>
|
||||
<h2>Ils m'ont fait confiance</h2>
|
||||
<p>KXKM (spectacle vivant), secteur ferroviaire, industrie automatisée, écoles d'art numérique.</p>
|
||||
<h2>À propos</h2>
|
||||
<p>Clément Saillant — 10 ans d'expérience en systèmes électroniques. Contributeur open-source KiCad et KXKM. Ambassadeur IA appliquée à l'électronique.</p>
|
||||
<p>Contact : <a href="/contact/">Parlons de votre projet</a> — contact@lelectronrare.fr · <a href="https://blog.saillant.cc">Blog technique</a></p>
|
||||
</div>
|
||||
|
||||
update();
|
||||
rotateMessage();
|
||||
setInterval(update, 1000);
|
||||
setInterval(rotateMessage, 8000);
|
||||
</script>
|
||||
<noscript>
|
||||
<div style="position:fixed;inset:0;z-index:9999;background:#06080b;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;font-family:monospace;text-align:center;padding:40px;">
|
||||
<img src="/assets/photos/bmu-assembly.jpg" alt="BMU v2 Assembly" style="width:clamp(200px,50vw,500px);border-radius:12px;margin-bottom:20px;" />
|
||||
<h1 style="font-size:24px;">L'électron rare</h1>
|
||||
<p style="color:rgba(255,255,255,0.5);margin:12px 0;">Systèmes électroniques spécifiques</p>
|
||||
<a href="https://www.lelectronrare.fr/" style="color:#5bd1d8;margin-top:16px;">Accéder au site →</a>
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<style>
|
||||
.countdown-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
background: var(--bg, #0b0a09);
|
||||
}
|
||||
<div class="inf">L'électron rare · Clément Saillant</div>
|
||||
|
||||
.atom {
|
||||
width: clamp(180px, 35vw, 260px);
|
||||
height: auto;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
<!-- GTM for A/B tracking -->
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!=='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-5SLM67QF');
|
||||
</script>
|
||||
|
||||
.title {
|
||||
font-family: 'Fraunces', serif;
|
||||
font-size: clamp(28px, 5vw, 48px);
|
||||
font-weight: 700;
|
||||
color: var(--text, #f4eee3);
|
||||
margin: 0 0 32px;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
<script>
|
||||
const prog = document.getElementById('prog');
|
||||
const dots = [...document.querySelectorAll('.dot')];
|
||||
const overlays = [...document.querySelectorAll('.ov')];
|
||||
const RANGES = [
|
||||
['hero',0,0.10],['hero-to-exp',0.10,0.16],
|
||||
['exp-elec',0.16,0.20],['exp-auto',0.20,0.24],['exp-energie',0.24,0.28],
|
||||
['exp-dispositifs',0.28,0.31],['exp-consulting',0.31,0.34],
|
||||
['eclate-recul',0.34,0.36],['eclate-sep',0.36,0.39],['eclate-plat',0.39,0.41],
|
||||
['cas',0.41,0.65],
|
||||
['media',0.65,0.81],['formats',0.81,0.91],['contact',0.91,1.0],
|
||||
];
|
||||
function update() {
|
||||
const max = document.documentElement.scrollHeight - window.innerHeight;
|
||||
const t = max > 0 ? window.scrollY / max : 0;
|
||||
if (prog) prog.style.width = (t*100)+'%';
|
||||
let active = 'hero';
|
||||
for (const [id,s,e] of RANGES) { if (t>=s&&t<=e){active=id;break;} }
|
||||
overlays.forEach(el => el.classList.toggle('on', el.getAttribute('data-sec')===active));
|
||||
dots.forEach(d => d.classList.toggle('on', d.getAttribute('data-s')===active));
|
||||
}
|
||||
window.addEventListener('scroll', update, {passive:true});
|
||||
update();
|
||||
|
||||
.electric {
|
||||
color: var(--electric, #5bd1d8);
|
||||
animation: pulse 3s ease-in-out infinite;
|
||||
}
|
||||
// Dots cliquables
|
||||
dots.forEach(d => {
|
||||
d.addEventListener('click', () => {
|
||||
const p = parseFloat(d.getAttribute('data-p') || '0');
|
||||
const max = document.documentElement.scrollHeight - window.innerHeight;
|
||||
window.scrollTo({ top: Math.round(max * p), behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; text-shadow: 0 0 12px var(--electric, #5bd1d8); }
|
||||
}
|
||||
// GA4 tracking
|
||||
document.querySelectorAll('a[href]').forEach(a => {
|
||||
a.addEventListener('click', () => {
|
||||
const label = a.textContent?.trim() || a.getAttribute('href') || '';
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.dataLayer.push({ event: 'click', event_category: 'navigation', event_label: label, da_variant: 'preview', page_path: '/preview/' });
|
||||
});
|
||||
});
|
||||
|
||||
.timer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
// Burger menu
|
||||
document.getElementById('burger-btn')?.addEventListener('click',()=>document.getElementById('mobile-menu')?.classList.add('open'));
|
||||
document.getElementById('close-menu')?.addEventListener('click',()=>document.getElementById('mobile-menu')?.classList.remove('open'));
|
||||
|
||||
.unit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
// Preloader — télécharge les assets en arrière-plan avec progression réelle
|
||||
const loader = document.getElementById('loader');
|
||||
const loaderFill = document.getElementById('loader-fill');
|
||||
const loaderStatus = document.getElementById('loader-status');
|
||||
|
||||
.num {
|
||||
font-family: 'IBM Plex Mono', 'Share Tech Mono', monospace;
|
||||
font-size: clamp(36px, 8vw, 72px);
|
||||
font-weight: 600;
|
||||
color: var(--text, #f4eee3);
|
||||
line-height: 1;
|
||||
min-width: 2ch;
|
||||
}
|
||||
const ASSETS = [
|
||||
{ url: '/assets/models3d/bmu_v2.glb', label: 'Carte BMU principale', size: 5500000 },
|
||||
{ url: '/assets/models3d/i2c_repeater.glb', label: 'Carte I2C Repeater', size: 1800000 },
|
||||
{ url: '/assets/models3d/mosfet_switch.glb', label: 'Cartes Mosfet Switch', size: 1500000 },
|
||||
];
|
||||
const TOTAL_SIZE = ASSETS.reduce((s, a) => s + a.size, 0);
|
||||
|
||||
.label {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim, #b6a894);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
let loadedBytes = 0;
|
||||
let allDone = false;
|
||||
|
||||
.sep {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: clamp(28px, 6vw, 56px);
|
||||
color: var(--accent, #f1c27a);
|
||||
opacity: 0.5;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
function updateProgress(bytes, label) {
|
||||
loadedBytes += bytes;
|
||||
const pct = Math.min(85, Math.round(loadedBytes / TOTAL_SIZE * 85));
|
||||
if (loaderFill) loaderFill.style.width = pct + '%';
|
||||
if (loaderStatus) loaderStatus.textContent = label;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 14px;
|
||||
color: var(--accent, #f1c27a);
|
||||
margin: 0 0 40px;
|
||||
min-height: 1.5em;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
async function preloadAsset(asset) {
|
||||
try {
|
||||
const res = await fetch(asset.url);
|
||||
const reader = res.body?.getReader();
|
||||
if (!reader) { updateProgress(asset.size, asset.label); return; }
|
||||
let received = 0;
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
received += value.length;
|
||||
updateProgress(value.length, asset.label);
|
||||
}
|
||||
} catch (e) {
|
||||
updateProgress(asset.size, asset.label + ' (cache)');
|
||||
}
|
||||
}
|
||||
|
||||
.footnote {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--text-dim, #b6a894);
|
||||
opacity: 0.4;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
</style>
|
||||
function hideLoader() {
|
||||
if (allDone) return;
|
||||
allDone = true;
|
||||
if (loaderFill) loaderFill.style.width = '100%';
|
||||
if (loaderStatus) loaderStatus.textContent = 'Prêt';
|
||||
setTimeout(() => { if (loader) loader.classList.add('done'); }, 400);
|
||||
}
|
||||
|
||||
// Phase 1 : preload les GLB en streaming
|
||||
(async () => {
|
||||
if (loaderStatus) loaderStatus.textContent = 'Téléchargement des modèles 3D...';
|
||||
for (const asset of ASSETS) {
|
||||
await preloadAsset(asset);
|
||||
}
|
||||
if (loaderStatus) loaderStatus.textContent = 'Initialisation WebGL...';
|
||||
if (loaderFill) loaderFill.style.width = '90%';
|
||||
|
||||
// Phase 2 : attendre le canvas Three.js
|
||||
const checkCanvas = setInterval(() => {
|
||||
const cvs = document.querySelector('canvas');
|
||||
if (cvs && cvs.width > 0) {
|
||||
clearInterval(checkCanvas);
|
||||
if (loaderFill) loaderFill.style.width = '95%';
|
||||
if (loaderStatus) loaderStatus.textContent = 'Compilation shaders...';
|
||||
setTimeout(hideLoader, 600);
|
||||
}
|
||||
}, 150);
|
||||
})();
|
||||
|
||||
// Fallback 15s max
|
||||
setTimeout(hideLoader, 15000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import '@/styles/global.css';
|
||||
---
|
||||
|
||||
<BaseLayout title="Intranet — L'Electron Rare" description="Dashboard interne des services.">
|
||||
<main class="intranet">
|
||||
<h1 class="intranet-title">Intranet <span class="electric">L'Electron Rare</span></h1>
|
||||
<p class="intranet-sub">Accès rapide · Services · Monitoring</p>
|
||||
|
||||
<div class="grid">
|
||||
<a href="https://tower.saillant.cc/app/crm" class="card" target="_blank" rel="noopener">
|
||||
<div class="card-icon">📊</div>
|
||||
<h2>Frappe CRM</h2>
|
||||
<p>Leads, deals, pipeline commercial, AI scoring</p>
|
||||
<span class="card-url">tower.saillant.cc/app/crm</span>
|
||||
</a>
|
||||
|
||||
<a href="https://tower.saillant.cc/lms/courses" class="card" target="_blank" rel="noopener">
|
||||
<div class="card-icon">🎓</div>
|
||||
<h2>Frappe LMS</h2>
|
||||
<p>3 formations : KiCad, Debug & Mesure, Dispositifs Culturels</p>
|
||||
<span class="card-url">tower.saillant.cc/lms/courses</span>
|
||||
</a>
|
||||
|
||||
<a href="https://mail.saillant.cc" class="card" target="_blank" rel="noopener">
|
||||
<div class="card-icon">📧</div>
|
||||
<h2>Listmonk</h2>
|
||||
<p>Newsletter, welcome sequence 3 emails, subscribers</p>
|
||||
<span class="card-url">mail.saillant.cc</span>
|
||||
</a>
|
||||
|
||||
<a href="https://n8n.saillant.cc" class="card" target="_blank" rel="noopener">
|
||||
<div class="card-icon">⚡</div>
|
||||
<h2>n8n</h2>
|
||||
<p>Workflows : lead scoring, newsletter sync, follow-up</p>
|
||||
<span class="card-url">n8n.saillant.cc</span>
|
||||
</a>
|
||||
|
||||
<a href="https://kpi.saillant.cc" class="card" target="_blank" rel="noopener">
|
||||
<div class="card-icon">📈</div>
|
||||
<h2>Metabase</h2>
|
||||
<p>Dashboard pipeline, leads par segment/source, score IA</p>
|
||||
<span class="card-url">kpi.saillant.cc</span>
|
||||
</a>
|
||||
|
||||
<a href="https://rdv.saillant.cc" class="card" target="_blank" rel="noopener">
|
||||
<div class="card-icon">📅</div>
|
||||
<h2>Cal.com</h2>
|
||||
<p>RDV : découverte 20min, projet 30min, formation, audit 1h</p>
|
||||
<span class="card-url">rdv.saillant.cc</span>
|
||||
</a>
|
||||
|
||||
<a href="https://mascarade.saillant.cc/health" class="card" target="_blank" rel="noopener">
|
||||
<div class="card-icon">🤖</div>
|
||||
<h2>mascarade IA</h2>
|
||||
<p>16 agents IA : writer, analyst, KiCad, SPICE, planner...</p>
|
||||
<span class="card-url">mascarade.saillant.cc</span>
|
||||
</a>
|
||||
|
||||
<a href="https://auth.saillant.cc" class="card" target="_blank" rel="noopener">
|
||||
<div class="card-icon">🔐</div>
|
||||
<h2>Authentik SSO</h2>
|
||||
<p>Utilisateurs, permissions, providers</p>
|
||||
<span class="card-url">auth.saillant.cc</span>
|
||||
</a>
|
||||
|
||||
<a href="https://www.lelectronrare.fr/ops" class="card card--highlight" target="_blank" rel="noopener">
|
||||
<div class="card-icon">🎯</div>
|
||||
<h2>Ops Center</h2>
|
||||
<p>Workflow, copilote IA, CRM live, monitoring</p>
|
||||
<span class="card-url">lelectronrare.fr/ops</span>
|
||||
</a>
|
||||
|
||||
<a href="https://api.lelectronrare.fr/health" class="card" target="_blank" rel="noopener">
|
||||
<div class="card-icon">🔌</div>
|
||||
<h2>API Proxy</h2>
|
||||
<p>Submit lead → Frappe + AI scoring Ollama</p>
|
||||
<span class="card-url">api.lelectronrare.fr</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">Status live</h2>
|
||||
<div id="status-grid" class="status-grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">Site public</h2>
|
||||
<div class="links">
|
||||
<a href="https://www.lelectronrare.fr/">Countdown</a>
|
||||
<a href="https://www.lelectronrare.fr/preview">Preview complet</a>
|
||||
<a href="https://www.lelectronrare.fr/formation">Formations</a>
|
||||
<a href="https://www.lelectronrare.fr/portfolio">Portfolio KiCanvas</a>
|
||||
<a href="https://www.lelectronrare.fr/mentions-legales">Mentions légales</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">RDV publics</h2>
|
||||
<div class="links">
|
||||
<a href="https://rdv.saillant.cc/clement/decouverte">Appel découverte 20min</a>
|
||||
<a href="https://rdv.saillant.cc/clement/point-projet">Point projet 30min</a>
|
||||
<a href="https://rdv.saillant.cc/clement/formation">Échange formation 30min</a>
|
||||
<a href="https://rdv.saillant.cc/clement/audit">Audit technique 1h</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">Ressources</h2>
|
||||
<div class="links">
|
||||
<a href="https://github.com/electron-rare">GitHub</a>
|
||||
<a href="https://github.com/KomplexKapharnaum">GitHub KXKM</a>
|
||||
<a href="https://fr.linkedin.com/in/electron-rare">LinkedIn</a>
|
||||
<a href="https://tower.saillant.cc/app/lms-course">Admin LMS</a>
|
||||
<a href="https://tower.saillant.cc/app/email-account">Email accounts</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">Credentials</h2>
|
||||
<div class="cred-grid">
|
||||
<div class="cred"><span class="cred-label">Frappe CRM</span><span class="cred-val">clement@lelectronrare.fr</span></div>
|
||||
<div class="cred"><span class="cred-label">Frappe Admin</span><span class="cred-val">Administrator</span></div>
|
||||
<div class="cred"><span class="cred-label">Authentik</span><span class="cred-val">clement@saillant.cc</span></div>
|
||||
<div class="cred"><span class="cred-label">Listmonk</span><span class="cred-val">admin</span></div>
|
||||
<div class="cred"><span class="cred-label">Cal.com</span><span class="cred-val">clement@lelectronrare.fr</span></div>
|
||||
<div class="cred"><span class="cred-label">Metabase</span><span class="cred-val">clement@lelectronrare.fr</span></div>
|
||||
<div class="cred"><span class="cred-label">n8n</span><span class="cred-val">clement@lelectronrare.fr</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
const services = [
|
||||
{ name: 'Site', url: 'https://www.lelectronrare.fr/' },
|
||||
{ name: 'API CRM', url: 'https://api.lelectronrare.fr/health' },
|
||||
{ name: 'Frappe CRM', url: 'https://tower.saillant.cc/' },
|
||||
{ name: 'Listmonk', url: 'https://mail.saillant.cc/' },
|
||||
{ name: 'n8n', url: 'https://n8n.saillant.cc/' },
|
||||
{ name: 'Cal.com', url: 'https://rdv.saillant.cc/' },
|
||||
{ name: 'Metabase', url: 'https://kpi.saillant.cc/' },
|
||||
{ name: 'mascarade', url: 'https://mascarade.saillant.cc/health' },
|
||||
{ name: 'Authentik', url: 'https://auth.saillant.cc/' },
|
||||
];
|
||||
|
||||
const grid = document.getElementById('status-grid');
|
||||
if (grid) {
|
||||
services.forEach(s => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'status-item';
|
||||
el.innerHTML = `<span class="status-dot status-checking"></span><span class="status-name">${s.name}</span><span class="status-ms"></span>`;
|
||||
grid.appendChild(el);
|
||||
|
||||
const t0 = Date.now();
|
||||
fetch(s.url, { mode: 'no-cors', cache: 'no-store', signal: AbortSignal.timeout(5000) })
|
||||
.then(() => {
|
||||
const ms = Date.now() - t0;
|
||||
el.querySelector('.status-dot')?.classList.replace('status-checking', 'status-up');
|
||||
const msEl = el.querySelector('.status-ms');
|
||||
if (msEl) msEl.textContent = ms + 'ms';
|
||||
})
|
||||
.catch(() => {
|
||||
el.querySelector('.status-dot')?.classList.replace('status-checking', 'status-down');
|
||||
const msEl = el.querySelector('.status-ms');
|
||||
if (msEl) msEl.textContent = 'timeout';
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.intranet { max-width:1000px; margin:0 auto; padding:48px 24px; min-height:100vh; }
|
||||
.intranet-title { font-family:'Fraunces',serif; font-size:clamp(24px,4vw,36px); color:var(--text,#f4eee3); margin:0 0 8px; }
|
||||
.electric { color:var(--electric,#5bd1d8); }
|
||||
.intranet-sub { font-family:'IBM Plex Mono',monospace; font-size:13px; color:var(--text-dim,#b6a894); margin:0 0 32px; }
|
||||
|
||||
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:14px; margin-bottom:40px; }
|
||||
.card { background:var(--surface,#1a1510); border:1px solid var(--line,#3a2f24); border-radius:8px; padding:18px; text-decoration:none; transition:all .2s; display:block; }
|
||||
.card:hover { border-color:var(--electric,#5bd1d8); box-shadow:0 0 16px rgba(91,209,216,.15); transform:translateY(-2px); }
|
||||
.card--highlight { border-color:var(--accent,#f1c27a); background:rgba(241,194,122,0.05); }
|
||||
.card-icon { font-size:24px; margin-bottom:8px; }
|
||||
.card h2 { font-family:'Fraunces',serif; font-size:15px; color:var(--text,#f4eee3); margin:0 0 4px; }
|
||||
.card p { font-size:12px; color:var(--text-muted,#d6cbb9); margin:0 0 8px; line-height:1.4; }
|
||||
.card-url { font-family:'IBM Plex Mono',monospace; font-size:10px; color:var(--text-dim,#b6a894); opacity:.5; }
|
||||
|
||||
.section { margin-bottom:28px; }
|
||||
.section-title { font-family:'Fraunces',serif; font-size:17px; color:var(--accent,#f1c27a); margin:0 0 14px; padding-bottom:6px; border-bottom:1px solid var(--line,#3a2f24); }
|
||||
|
||||
.status-grid { display:flex; flex-wrap:wrap; gap:10px; }
|
||||
.status-item { display:flex; align-items:center; gap:8px; font-family:'IBM Plex Mono',monospace; font-size:12px; color:var(--text-muted,#d6cbb9); background:var(--surface,#1a1510); padding:6px 12px; border-radius:6px; border:1px solid var(--line,#3a2f24); }
|
||||
.status-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
|
||||
.status-checking { background:#f1c27a; animation:blink 1s infinite; }
|
||||
.status-up { background:#4ade80; box-shadow:0 0 4px #4ade80; }
|
||||
.status-down { background:#ef4444; box-shadow:0 0 4px #ef4444; }
|
||||
.status-ms { font-size:10px; color:var(--text-dim,#b6a894); opacity:.6; }
|
||||
@keyframes blink { 50% { opacity:.3; } }
|
||||
|
||||
.links { display:flex; flex-wrap:wrap; gap:8px; }
|
||||
.links a { font-family:'IBM Plex Mono',monospace; font-size:12px; color:var(--electric,#5bd1d8); text-decoration:none; padding:6px 12px; border:1px solid var(--line,#3a2f24); border-radius:6px; transition:all .2s; }
|
||||
.links a:hover { border-color:var(--electric,#5bd1d8); background:rgba(91,209,216,.08); }
|
||||
|
||||
.cred-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); gap:8px; }
|
||||
.cred { display:flex; justify-content:space-between; align-items:center; font-family:'IBM Plex Mono',monospace; font-size:12px; padding:8px 12px; background:var(--surface,#1a1510); border:1px solid var(--line,#3a2f24); border-radius:6px; }
|
||||
.cred-label { color:var(--text-dim,#b6a894); }
|
||||
.cred-val { color:var(--electric,#5bd1d8); }
|
||||
</style>
|
||||
@@ -0,0 +1,884 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import '@/styles/global.css';
|
||||
---
|
||||
|
||||
<BaseLayout title="Ops — L'Electron Rare" description="Centre opérationnel IA-native.">
|
||||
<main class="ops" id="ops-main">
|
||||
|
||||
<div class="ops-header">
|
||||
<div class="ops-header-left">
|
||||
<img src="/assets/brand/logo-mark.jpg" alt="" width="32" height="32" class="ops-logo" />
|
||||
<div>
|
||||
<h1 class="ops-title">Ops</h1>
|
||||
<p class="ops-sub">L'Electron Rare · <span id="task-count">—</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ops-header-right">
|
||||
<div class="status-bar" id="status-bar">
|
||||
<span class="status-dot" id="status-dot"></span>
|
||||
<span class="status-text" id="status-text">...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="tabs" id="tabs">
|
||||
<button class="tab tab--active" data-tab="tasks">Tâches</button>
|
||||
<button class="tab" data-tab="dashboard">Dashboard</button>
|
||||
<button class="tab" data-tab="services">Services</button>
|
||||
</nav>
|
||||
|
||||
<!-- VUE 1: TÂCHES -->
|
||||
<section class="panel panel--active" id="panel-tasks">
|
||||
<div class="tasks-toolbar">
|
||||
<button class="btn btn--primary btn--sm" id="add-task-btn">+ Nouvelle tâche</button>
|
||||
<div class="tasks-filters">
|
||||
<select id="filter-cat" class="filter-select"><option value="">Toutes catégories</option></select>
|
||||
<select id="filter-status" class="filter-select">
|
||||
<option value="">Tous statuts</option>
|
||||
<option value="À faire" selected>À faire</option>
|
||||
<option value="En cours">En cours</option>
|
||||
<option value="Terminé">Terminé</option>
|
||||
</select>
|
||||
<input type="text" id="filter-search" class="filter-input" placeholder="Rechercher..." />
|
||||
</div>
|
||||
<button class="btn btn--outline btn--sm" id="refresh-tasks">↻</button>
|
||||
</div>
|
||||
|
||||
<div id="add-task-form" style="display:none" class="card-form">
|
||||
<input type="text" id="new-task-title" class="form-input" placeholder="Titre de la tâche..." />
|
||||
<div class="form-row">
|
||||
<select id="new-task-category" class="form-select">
|
||||
<option value="urgent">Urgent</option>
|
||||
<option value="prospection">Prospection</option>
|
||||
<option value="linkedin">LinkedIn</option>
|
||||
<option value="technique">Technique</option>
|
||||
<option value="lancement">Lancement</option>
|
||||
<option value="formation">Formation</option>
|
||||
<option value="gie">GIE</option>
|
||||
<option value="marketing">Marketing</option>
|
||||
<option value="business">Business</option>
|
||||
<option value="crm">CRM</option>
|
||||
</select>
|
||||
<select id="new-task-priority" class="form-select">
|
||||
<option value="P0">P0</option>
|
||||
<option value="P1">P1</option>
|
||||
<option value="P2" selected>P2</option>
|
||||
<option value="P3">P3</option>
|
||||
</select>
|
||||
<input type="date" id="new-task-deadline" class="form-input" style="width:auto" />
|
||||
<select id="new-task-agent" class="form-select">
|
||||
<option value="">Sans IA</option>
|
||||
<option value="writer">writer</option>
|
||||
<option value="analyst">analyst</option>
|
||||
<option value="brainstorm">brainstorm</option>
|
||||
<option value="planner">planner</option>
|
||||
<option value="prospection">prospection</option>
|
||||
<option value="formation-designer">formation-designer</option>
|
||||
</select>
|
||||
</div>
|
||||
<textarea id="new-task-desc" class="form-textarea" rows="2" placeholder="Description (optionnel)..."></textarea>
|
||||
<div class="form-row">
|
||||
<button class="btn btn--primary" id="save-task-btn">Enregistrer</button>
|
||||
<button class="btn btn--outline" id="cancel-task-btn">Annuler</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tasks-container"><div class="loading">Chargement...</div></div>
|
||||
</section>
|
||||
|
||||
<!-- VUE 2: DASHBOARD -->
|
||||
<section class="panel" id="panel-dashboard">
|
||||
<div class="kpi-grid" id="kpi-grid">
|
||||
<div class="kpi-card"><div class="kpi-num" id="kpi-leads">—</div><div class="kpi-label">Leads</div></div>
|
||||
<div class="kpi-card"><div class="kpi-num" id="kpi-hot">—</div><div class="kpi-label">Chauds</div></div>
|
||||
<div class="kpi-card"><div class="kpi-num" id="kpi-score">—</div><div class="kpi-label">Score IA</div></div>
|
||||
<div class="kpi-card"><div class="kpi-num" id="kpi-tasks">—</div><div class="kpi-label">Tâches</div></div>
|
||||
<div class="kpi-card"><div class="kpi-num" id="kpi-done">—</div><div class="kpi-label">Terminées</div></div>
|
||||
<div class="kpi-card"><div class="kpi-num" id="kpi-agents">—</div><div class="kpi-label">Agents IA</div></div>
|
||||
</div>
|
||||
|
||||
<h3 class="sub-title">Agents mascarade <button class="btn btn--outline btn--sm" id="add-agent-btn" style="margin-left:8px">+ Créer</button></h3>
|
||||
<div id="add-agent-form" style="display:none" class="card-form">
|
||||
<input type="text" id="new-agent-name" class="form-input" placeholder="Nom (ex: email-drafter)" />
|
||||
<input type="text" id="new-agent-desc" class="form-input" placeholder="Description" />
|
||||
<textarea id="new-agent-prompt" class="form-textarea" rows="3" placeholder="System prompt..."></textarea>
|
||||
<div class="form-row">
|
||||
<select id="new-agent-strategy" class="form-select"><option value="best">best</option><option value="cheapest">cheapest</option><option value="fastest">fastest</option></select>
|
||||
<select id="new-agent-provider" class="form-select"><option value="">Auto</option><option value="ollama">ollama</option><option value="claude">claude</option><option value="mistral">mistral</option></select>
|
||||
<button class="btn btn--primary" id="save-agent-btn">Créer</button>
|
||||
<button class="btn btn--outline" id="cancel-agent-btn">Annuler</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agents-grid" id="agents-grid"></div>
|
||||
|
||||
<h3 class="sub-title">Copilote rapide</h3>
|
||||
<div class="copilot-bar">
|
||||
<select id="chat-agent" class="form-select"></select>
|
||||
<input type="text" id="chat-input" class="form-input" placeholder="Demande à l'IA..." style="flex:1" />
|
||||
<button class="btn btn--primary" id="chat-send">→</button>
|
||||
</div>
|
||||
<div id="chat-output" class="chat-output"></div>
|
||||
|
||||
<div class="quick-links">
|
||||
<a href="https://tower.saillant.cc/app/crm" target="_blank" class="btn btn--outline btn--sm">CRM</a>
|
||||
<a href="https://kpi.saillant.cc/dashboard/2" target="_blank" class="btn btn--outline btn--sm">Metabase</a>
|
||||
<a href="https://mail.saillant.cc" target="_blank" class="btn btn--outline btn--sm">Listmonk</a>
|
||||
<a href="https://rdv.saillant.cc" target="_blank" class="btn btn--outline btn--sm">Cal.com</a>
|
||||
<a href="https://n8n.saillant.cc" target="_blank" class="btn btn--outline btn--sm">n8n</a>
|
||||
<a href="https://tower.saillant.cc/lms/courses" target="_blank" class="btn btn--outline btn--sm">LMS</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- VUE 3: SERVICES -->
|
||||
<section class="panel" id="panel-services">
|
||||
<h3 class="sub-title">Status <span class="status-refresh" id="refresh-services">↻</span></h3>
|
||||
<div class="monitor-grid" id="monitor-grid"></div>
|
||||
|
||||
<h3 class="sub-title">Business</h3>
|
||||
<div class="services-grid">
|
||||
<a href="https://tower.saillant.cc/app/crm" class="svc-card" target="_blank"><img src="/assets/icons/crm.png" width="36" height="36" /><span>CRM</span></a>
|
||||
<a href="https://tower.saillant.cc/lms/courses" class="svc-card" target="_blank"><img src="/assets/icons/lms.png" width="36" height="36" /><span>LMS</span></a>
|
||||
<a href="https://mail.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/newsletter.png" width="36" height="36" /><span>Listmonk</span></a>
|
||||
<a href="https://n8n.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/automation.png" width="36" height="36" /><span>n8n</span></a>
|
||||
<a href="https://kpi.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/analytics.png" width="36" height="36" /><span>Metabase</span></a>
|
||||
<a href="https://rdv.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/calendar.png" width="36" height="36" /><span>Cal.com</span></a>
|
||||
</div>
|
||||
|
||||
<h3 class="sub-title">IA & Collaboratif</h3>
|
||||
<div class="services-grid">
|
||||
<a href="https://mascarade.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/ai-brain.png" width="36" height="36" /><span>mascarade</span></a>
|
||||
<a href="https://dify.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/workflow-ai.png" width="36" height="36" /><span>Dify</span></a>
|
||||
<a href="https://langfuse.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/analytics.png" width="36" height="36" /><span>Langfuse</span></a>
|
||||
<a href="https://memos.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/notes.png" width="36" height="36" /><span>Memos</span></a>
|
||||
<a href="https://docs.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/documents.png" width="36" height="36" /><span>Paperless</span></a>
|
||||
<a href="https://karakeep.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/bookmarks.png" width="36" height="36" /><span>Karakeep</span></a>
|
||||
<a href="https://draw.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/eye.png" width="36" height="36" /><span>Excalidraw</span></a>
|
||||
<a href="https://paste.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/logs.png" width="36" height="36" /><span>PrivateBin</span></a>
|
||||
</div>
|
||||
|
||||
<h3 class="sub-title">Monitoring</h3>
|
||||
<div class="services-grid">
|
||||
<a href="https://grafana.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/chart.png" width="36" height="36" /><span>Grafana</span></a>
|
||||
<a href="https://logs.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/logs.png" width="36" height="36" /><span>Dozzle</span></a>
|
||||
<a href="https://ntfy.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/bell.png" width="36" height="36" /><span>ntfy</span></a>
|
||||
<a href="https://checks.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/health.png" width="36" height="36" /><span>Healthchecks</span></a>
|
||||
<a href="https://watch.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/eye.png" width="36" height="36" /><span>Changedetection</span></a>
|
||||
<a href="https://feeds.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/rss.png" width="36" height="36" /><span>Miniflux</span></a>
|
||||
<a href="https://auth.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/auth.png" width="36" height="36" /><span>Authentik</span></a>
|
||||
<a href="https://browser.saillant.cc" class="svc-card" target="_blank"><img src="/assets/icons/eye.png" width="36" height="36" /><span>browser-use</span></a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
// API Contracts:
|
||||
// GET /health → { ok, agents, providers }
|
||||
// GET /api/ai → { agents: [...] }
|
||||
// POST /api/ai → create agent
|
||||
// POST /api/ai/:name/run → { messages: [{role, content}] }
|
||||
// GET /api/ops/tasks → { data: [...] }
|
||||
// POST /api/ops/tasks → create task
|
||||
// PUT /api/ops/tasks/:id → update task
|
||||
// POST /api/ops/execute/:id → AI execute
|
||||
|
||||
const API = 'https://api.lelectronrare.fr';
|
||||
const OPS = API + '/api/ops';
|
||||
const AI = API + '/api/ai';
|
||||
|
||||
// Auth token for API calls — populate via data-token attribute on #ops-main
|
||||
// or a <meta name="api-token"> tag. Empty string keeps unauthenticated flow working.
|
||||
const AUTH_TOKEN = document.getElementById('ops-main')?.dataset?.token
|
||||
|| document.querySelector('meta[name="api-token"]')?.content
|
||||
|| '';
|
||||
|
||||
const catLabels = {urgent:'Urgent',prospection:'Prospection',linkedin:'LinkedIn',technique:'Technique',lancement:'Lancement',formation:'Formation',gie:'GIE',marketing:'Marketing',business:'Business',crm:'CRM'};
|
||||
const catColors = {urgent:'red',prospection:'amber',linkedin:'blue',technique:'cyan',lancement:'green',formation:'amber',gie:'amber',crm:'cyan',marketing:'blue',business:'green'};
|
||||
const prioColors = {P0:'red',P1:'amber',P2:'cyan',P3:'green'};
|
||||
|
||||
// --- Tabs ---
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('tab--active'));
|
||||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('panel--active'));
|
||||
tab.classList.add('tab--active');
|
||||
document.getElementById('panel-' + tab.dataset.tab)?.classList.add('panel--active');
|
||||
});
|
||||
});
|
||||
|
||||
// --- TASKS ---
|
||||
let allTasks = [];
|
||||
|
||||
async function loadTasks() {
|
||||
try {
|
||||
const res = await fetch(OPS + '/tasks', {headers:{'Content-Type':'application/json'}});
|
||||
allTasks = (await res.json()).data || [];
|
||||
renderTasks();
|
||||
} catch(e) { document.getElementById('tasks-container').innerHTML = '<p class="err">Erreur: '+e.message+'</p>'; }
|
||||
}
|
||||
|
||||
function renderTasks() {
|
||||
const cat = document.getElementById('filter-cat')?.value || '';
|
||||
const status = document.getElementById('filter-status')?.value || '';
|
||||
const search = (document.getElementById('filter-search')?.value || '').toLowerCase();
|
||||
|
||||
let filtered = allTasks;
|
||||
if (cat) filtered = filtered.filter(t => t.category === cat);
|
||||
if (status) filtered = filtered.filter(t => t.status === status);
|
||||
if (search) filtered = filtered.filter(t => (t.task_title||'').toLowerCase().includes(search) || (t.description||'').toLowerCase().includes(search));
|
||||
|
||||
const groups = {};
|
||||
const done = [];
|
||||
filtered.forEach(t => {
|
||||
if (t.status === 'Terminé') { done.push(t); return; }
|
||||
const c = t.category || 'technique';
|
||||
if (!groups[c]) groups[c] = [];
|
||||
groups[c].push(t);
|
||||
});
|
||||
|
||||
let html = '';
|
||||
for (const [c, tasks] of Object.entries(groups)) {
|
||||
html += `<div class="task-group"><div class="group-label group-label--${catColors[c]||'cyan'}">${catLabels[c]||c} (${tasks.length})</div><div class="task-list">${tasks.map(t => taskCard(t)).join('')}</div></div>`;
|
||||
}
|
||||
if (done.length) {
|
||||
html += `<details class="task-group"><summary class="group-label group-done">Terminé (${done.length})</summary><div class="task-list">${done.map(t => taskCard(t,true)).join('')}</div></details>`;
|
||||
}
|
||||
|
||||
const total = allTasks.length;
|
||||
const doneCount = allTasks.filter(t => t.status === 'Terminé').length;
|
||||
document.getElementById('task-count').textContent = `${doneCount}/${total} tâches`;
|
||||
document.getElementById('tasks-container').innerHTML = html || '<p class="hint">Aucune tâche</p>';
|
||||
bindTasks();
|
||||
|
||||
// Populate category filter
|
||||
const catSelect = document.getElementById('filter-cat');
|
||||
if (catSelect && catSelect.options.length <= 1) {
|
||||
const cats = [...new Set(allTasks.map(t => t.category).filter(Boolean))].sort();
|
||||
cats.forEach(c => { const o = document.createElement('option'); o.value = c; o.textContent = catLabels[c]||c; catSelect.appendChild(o); });
|
||||
}
|
||||
}
|
||||
|
||||
function taskCard(t, isDone) {
|
||||
const overdue = t.deadline && new Date(t.deadline) < new Date() && !isDone;
|
||||
const isLinkedin = t.category === 'linkedin';
|
||||
const lines = isLinkedin && t.description ? t.description.split('\\n').map(l => l ? '<p class="li-line">'+l+'</p>' : '<div class="li-spacer"></div>').join('') : '';
|
||||
|
||||
return `<div class="task ${overdue?'task--overdue':''} ${isDone?'task--done':''}" data-id="${t.name}">
|
||||
<div class="task-header">
|
||||
<span class="task-status">${t.status==='Terminé'?'●':t.status==='En cours'?'◐':'○'}</span>
|
||||
<span class="task-title">${t.task_title||''}</span>
|
||||
${t.priority?`<span class="badge badge--${prioColors[t.priority]||'cyan'}">${t.priority}</span>`:''}
|
||||
${t.deadline?`<span class="badge badge--${overdue?'red':'blue'}">${t.deadline}</span>`:''}
|
||||
${t.ai_agent?`<span class="badge badge--cyan">IA</span>`:''}
|
||||
</div>
|
||||
<div class="task-body">
|
||||
${t.description?`<div class="task-desc">${isLinkedin?'<div class="li-preview"><div class="li-header"><div class="li-avatar">CS</div><div><b>Clément Saillant</b><br><small>Ingénieur électronique · L\'Electron Rare</small></div></div>'+lines+'</div>':'<p>'+t.description+'</p>'}</div>`:''}
|
||||
${t.ai_result?`<details class="ai-saved"><summary>Résultat IA</summary><pre>${(t.ai_result||'').substring(0,500)}</pre></details>`:''}
|
||||
<div class="task-actions">
|
||||
<select class="action-status" data-id="${t.name}">${['À faire','En cours','Terminé','Reporté'].map(s=>`<option${s===t.status?' selected':''}>${s}</option>`).join('')}</select>
|
||||
<input type="text" class="action-note" data-id="${t.name}" placeholder="Note..." value="${(t.notes||'').replace(/"/g,'"')}" />
|
||||
<button class="btn btn--primary btn--sm save-btn" data-id="${t.name}">Save</button>
|
||||
${t.ai_agent?`<button class="btn btn--outline btn--sm exec-btn" data-id="${t.name}">IA: ${t.ai_agent}</button>`:''}
|
||||
${isLinkedin?`<button class="btn btn--outline btn--sm copy-btn" data-text="${encodeURIComponent(t.description||'')}">Copier</button><a href="https://linkedin.com/feed/" target="_blank" class="btn btn--outline btn--sm">LinkedIn</a>`:''}
|
||||
</div>
|
||||
<div class="ai-out" id="ai-${t.name}"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function bindTasks() {
|
||||
document.querySelectorAll('#tasks-container .task-header').forEach(h => h.addEventListener('click', () => h.closest('.task')?.classList.toggle('task--open')));
|
||||
document.querySelectorAll('.save-btn').forEach(b => b.addEventListener('click', async e => {
|
||||
e.stopPropagation(); const id=b.dataset.id, task=b.closest('.task');
|
||||
b.textContent='...';
|
||||
await fetch(OPS+'/tasks/'+id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({status:task.querySelector('.action-status')?.value,notes:task.querySelector('.action-note')?.value})});
|
||||
b.textContent='✓'; setTimeout(loadTasks,500);
|
||||
}));
|
||||
document.querySelectorAll('.exec-btn').forEach(b => b.addEventListener('click', async e => {
|
||||
e.stopPropagation(); const id=b.dataset.id, out=document.getElementById('ai-'+id);
|
||||
if(out){out.innerHTML='<div class="loading">IA...</div>';out.style.display='block';}
|
||||
b.textContent='...';
|
||||
try{const r=await fetch(OPS+'/execute/'+id,{method:'POST',headers:{'Content-Type':'application/json'}});const d=await r.json();
|
||||
if(out)out.innerHTML='<pre>'+(d.result||d.error||'').substring(0,500).replace(/</g,'<')+'</pre>';
|
||||
}catch(err){if(out)out.innerHTML='<p class="err">'+err.message+'</p>';}
|
||||
b.textContent='IA'; loadTasks();
|
||||
}));
|
||||
document.querySelectorAll('.copy-btn').forEach(b => b.addEventListener('click', e => {
|
||||
e.stopPropagation(); navigator.clipboard.writeText(decodeURIComponent(b.dataset.text)); b.textContent='✓'; setTimeout(()=>b.textContent='Copier',2000);
|
||||
}));
|
||||
}
|
||||
|
||||
// Filters
|
||||
['filter-cat','filter-status'].forEach(id => document.getElementById(id)?.addEventListener('change', renderTasks));
|
||||
document.getElementById('filter-search')?.addEventListener('input', renderTasks);
|
||||
document.getElementById('refresh-tasks')?.addEventListener('click', loadTasks);
|
||||
|
||||
// Add task
|
||||
document.getElementById('add-task-btn')?.addEventListener('click', () => { const f=document.getElementById('add-task-form'); f.style.display=f.style.display==='none'?'block':'none'; });
|
||||
document.getElementById('cancel-task-btn')?.addEventListener('click', () => document.getElementById('add-task-form').style.display='none');
|
||||
document.getElementById('save-task-btn')?.addEventListener('click', async () => {
|
||||
const title=document.getElementById('new-task-title')?.value?.trim(); if(!title)return;
|
||||
await fetch(OPS+'/tasks',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
|
||||
task_title:title, category:document.getElementById('new-task-category')?.value||'technique',
|
||||
priority:document.getElementById('new-task-priority')?.value||'P2',
|
||||
deadline:document.getElementById('new-task-deadline')?.value||null,
|
||||
ai_agent:document.getElementById('new-task-agent')?.value||null,
|
||||
description:document.getElementById('new-task-desc')?.value||'',
|
||||
status:'À faire', assigned_to:'clement@lelectronrare.fr'
|
||||
})});
|
||||
document.getElementById('new-task-title').value=''; document.getElementById('add-task-form').style.display='none'; loadTasks();
|
||||
});
|
||||
|
||||
loadTasks();
|
||||
|
||||
// --- DASHBOARD ---
|
||||
let allAgents = [];
|
||||
|
||||
async function loadDashboard() {
|
||||
// KPIs
|
||||
try {
|
||||
document.getElementById('kpi-tasks').textContent = allTasks.length;
|
||||
document.getElementById('kpi-done').textContent = allTasks.filter(t=>t.status==='Terminé').length;
|
||||
} catch {}
|
||||
// Agents
|
||||
try {
|
||||
const agentHeaders = {'Content-Type':'application/json'};
|
||||
if (AUTH_TOKEN) agentHeaders['Authorization'] = 'Bearer ' + AUTH_TOKEN;
|
||||
const r = await fetch(AI, {headers: agentHeaders});
|
||||
const d = await r.json();
|
||||
allAgents = d.agents || (Array.isArray(d) ? d : []);
|
||||
// kpi-agents is set from /health below; fallback to allAgents.length
|
||||
if (!document.getElementById('kpi-agents').dataset.fromHealth) {
|
||||
document.getElementById('kpi-agents').textContent = allAgents.length;
|
||||
}
|
||||
const grid = document.getElementById('agents-grid');
|
||||
grid.innerHTML = allAgents.map(a => `<div class="agent-card" data-name="${a.name}"><b>${a.name}</b><span class="badge badge--${a.strategy==='best'?'cyan':a.strategy==='cheapest'?'green':'blue'}">${a.strategy}</span><p>${(a.description||'').substring(0,60)}</p></div>`).join('');
|
||||
grid.querySelectorAll('.agent-card').forEach(c => c.addEventListener('click', () => {
|
||||
const sel = document.getElementById('chat-agent'); if(sel) sel.value = c.dataset.name;
|
||||
document.getElementById('chat-input')?.focus();
|
||||
}));
|
||||
// Populate chat selector
|
||||
const sel = document.getElementById('chat-agent');
|
||||
if (sel) sel.innerHTML = allAgents.map(a=>`<option value="${a.name}">${a.name}</option>`).join('');
|
||||
} catch {}
|
||||
// API health — parse { ok, agents, providers }
|
||||
try {
|
||||
const h = await fetch(API+'/health'); const hd = await h.json();
|
||||
document.getElementById('status-dot').className = 'status-dot status-dot--ok';
|
||||
const parts = ['API OK'];
|
||||
if (typeof hd.agents === 'number') parts.push(hd.agents + ' agents');
|
||||
if (Array.isArray(hd.providers) && hd.providers.length) parts.push(hd.providers.join(', '));
|
||||
document.getElementById('status-text').textContent = parts.join(' \u00b7 ');
|
||||
// Update kpi-agents from health (authoritative count)
|
||||
if (typeof hd.agents === 'number') {
|
||||
document.getElementById('kpi-agents').textContent = hd.agents;
|
||||
document.getElementById('kpi-agents').dataset.fromHealth = '1';
|
||||
}
|
||||
} catch {
|
||||
document.getElementById('status-dot').className = 'status-dot status-dot--err';
|
||||
document.getElementById('status-text').textContent = 'API down';
|
||||
}
|
||||
}
|
||||
|
||||
// Agent CRUD
|
||||
document.getElementById('add-agent-btn')?.addEventListener('click', () => { const f=document.getElementById('add-agent-form'); f.style.display=f.style.display==='none'?'block':'none'; });
|
||||
document.getElementById('cancel-agent-btn')?.addEventListener('click', () => document.getElementById('add-agent-form').style.display='none');
|
||||
document.getElementById('save-agent-btn')?.addEventListener('click', async () => {
|
||||
const name=document.getElementById('new-agent-name')?.value?.trim(); if(!name)return;
|
||||
await fetch(AI,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
|
||||
name, description:document.getElementById('new-agent-desc')?.value||'',
|
||||
system_prompt:document.getElementById('new-agent-prompt')?.value||'',
|
||||
strategy:document.getElementById('new-agent-strategy')?.value||'best',
|
||||
preferred_provider:document.getElementById('new-agent-provider')?.value||null,
|
||||
temperature:0.5, max_tokens:4096
|
||||
})});
|
||||
document.getElementById('add-agent-form').style.display='none'; loadDashboard();
|
||||
});
|
||||
|
||||
// Copilot quick
|
||||
document.getElementById('chat-send')?.addEventListener('click', sendChat);
|
||||
document.getElementById('chat-input')?.addEventListener('keydown', e => { if(e.key==='Enter') sendChat(); });
|
||||
async function sendChat() {
|
||||
const input = document.getElementById('chat-input');
|
||||
const msg = input?.value?.trim(); if(!msg) return; input.value = '';
|
||||
const agent = document.getElementById('chat-agent')?.value || 'agent-zero';
|
||||
const out = document.getElementById('chat-output');
|
||||
out.innerHTML = '<div class="loading">'+agent+' réfléchit...</div>';
|
||||
try {
|
||||
const r = await fetch(AI+'/'+agent+'/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({messages:[{role:'user',content:msg}]})});
|
||||
const d = await r.json();
|
||||
out.innerHTML = '<pre>'+(d.content||d.response||JSON.stringify(d)).replace(/</g,'<')+'</pre><button class="btn btn--outline btn--sm copy-chat">Copier</button>';
|
||||
out.querySelector('.copy-chat')?.addEventListener('click', function(){navigator.clipboard.writeText(d.content||d.response||'');this.textContent='✓';});
|
||||
} catch(e) { out.innerHTML = '<p class="err">Erreur: '+e.message+'</p>'; }
|
||||
}
|
||||
|
||||
loadDashboard();
|
||||
|
||||
// --- SERVICES MONITORING ---
|
||||
async function loadServices() {
|
||||
const services = [
|
||||
{name:'Site',url:'https://www.lelectronrare.fr/'},{name:'API',url:API+'/health'},
|
||||
{name:'CRM',url:'https://tower.saillant.cc/'},{name:'Listmonk',url:'https://mail.saillant.cc/'},
|
||||
{name:'n8n',url:'https://n8n.saillant.cc/'},{name:'Cal.com',url:'https://rdv.saillant.cc/'},
|
||||
{name:'Metabase',url:'https://kpi.saillant.cc/'},{name:'Authentik',url:'https://auth.saillant.cc/'},
|
||||
{name:'Memos',url:'https://memos.saillant.cc/'},{name:'Excalidraw',url:'https://draw.saillant.cc/'},
|
||||
{name:'Paperless',url:'https://docs.saillant.cc/'},{name:'ntfy',url:'https://ntfy.saillant.cc/'},
|
||||
];
|
||||
const grid = document.getElementById('monitor-grid');
|
||||
grid.innerHTML = services.map(s => `<div class="mon-card" id="mon-${s.name}"><span class="mon-dot"></span><span class="mon-name">${s.name}</span><span class="mon-ms"></span></div>`).join('');
|
||||
for (const s of services) {
|
||||
const card = document.getElementById('mon-'+s.name); if(!card) continue;
|
||||
const t0 = Date.now();
|
||||
try { await fetch(s.url,{mode:'no-cors',signal:AbortSignal.timeout(5000)}); card.querySelector('.mon-dot').classList.add('mon-ok'); card.querySelector('.mon-ms').textContent=(Date.now()-t0)+'ms'; }
|
||||
catch { card.querySelector('.mon-dot').classList.add('mon-err'); card.querySelector('.mon-ms').textContent='timeout'; }
|
||||
}
|
||||
}
|
||||
document.getElementById('refresh-services')?.addEventListener('click', loadServices);
|
||||
loadServices();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* ═══ Apple SOTA 2026 — Ops Dashboard ═══ */
|
||||
.ops {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem clamp(1rem, 4vw, 2rem);
|
||||
min-height: 100vh;
|
||||
background: #ffffff;
|
||||
color: #1d1d1f;
|
||||
font-family: Manrope, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Helvetica Neue, sans-serif;
|
||||
font-size: 0.875rem;
|
||||
letter-spacing: -0.01em;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.ops-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 0.5px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.ops-header-left { display: flex; align-items: center; gap: 0.75rem; }
|
||||
.ops-header-right { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.ops-logo { border-radius: 0.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
|
||||
.ops-title {
|
||||
font-family: Manrope, -apple-system, "SF Pro Display", sans-serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
.ops-sub { font-size: 0.75rem; color: #86868b; margin: 0.15rem 0 0; font-weight: 500; }
|
||||
|
||||
/* Status bar */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.4rem 0.75rem;
|
||||
background: #f5f5f7;
|
||||
border-radius: 999px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: #d2d2d7; transition: background 0.3s; }
|
||||
.status-dot--ok { background: #30d158; }
|
||||
.status-dot--err { background: #ff3b30; }
|
||||
.status-text { font-size: 0.7rem; color: #86868b; font-weight: 500; }
|
||||
|
||||
/* Tabs — segmented control Apple-style */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
background: #f5f5f7;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
.tab {
|
||||
flex: 1;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.55rem 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #86868b;
|
||||
cursor: pointer;
|
||||
border-radius: 0.6rem;
|
||||
transition: all 0.2s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
.tab:hover { color: #1d1d1f; }
|
||||
.tab--active {
|
||||
color: #1d1d1f;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08), 0 0.5px 1px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* Panels */
|
||||
.panel { display: none; animation: fadeIn 0.25s cubic-bezier(0.25, 0.1, 0.25, 1); }
|
||||
.panel--active { display: block; }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
||||
|
||||
/* Tasks toolbar */
|
||||
.tasks-toolbar { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
||||
.tasks-filters { display: flex; gap: 0.35rem; flex: 1; flex-wrap: wrap; }
|
||||
|
||||
/* Form controls */
|
||||
.filter-select, .form-select {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.4rem 0.65rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
background: #ffffff;
|
||||
color: #1d1d1f;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.filter-input, .form-input {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.625rem;
|
||||
background: #ffffff;
|
||||
color: #1d1d1f;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.filter-input:focus, .form-input:focus, .form-textarea:focus, .filter-select:focus, .form-select:focus {
|
||||
outline: none;
|
||||
border-color: #0071e3;
|
||||
box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.12);
|
||||
}
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.6rem 0.75rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.625rem;
|
||||
background: #ffffff;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.form-row { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
|
||||
.card-form {
|
||||
background: #ffffff;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* Task groups */
|
||||
.task-group { margin-bottom: 1.25rem; }
|
||||
.group-label {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 0.3rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
display: inline-block;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.group-label--red { color: #ff3b30; background: rgba(255, 59, 48, 0.08); }
|
||||
.group-label--amber { color: #ff9f0a; background: rgba(255, 159, 10, 0.08); }
|
||||
.group-label--blue { color: #0071e3; background: rgba(0, 113, 227, 0.06); }
|
||||
.group-label--green { color: #30d158; background: rgba(48, 209, 88, 0.08); }
|
||||
.group-label--cyan { color: #0071e3; background: rgba(0, 113, 227, 0.06); }
|
||||
.group-done { color: #86868b; background: #f5f5f7; cursor: pointer; list-style: none; border-radius: 999px; }
|
||||
.group-done::-webkit-details-marker { display: none; }
|
||||
.group-done::before { content: '+ '; font-weight: 700; }
|
||||
details[open] > .group-done::before { content: '- '; }
|
||||
|
||||
/* Task cards */
|
||||
.task-list { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.task {
|
||||
background: #ffffff;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 0.875rem;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
.task:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); transform: translateY(-1px); }
|
||||
.task--overdue { border-left: 3px solid #ff3b30; }
|
||||
.task--done { opacity: 0.35; }
|
||||
.task--done .task-title { text-decoration: line-through; }
|
||||
.task--open .task-body { display: block; }
|
||||
|
||||
.task-header { display: flex; align-items: center; gap: 0.5rem; padding: 0.75rem 1rem; cursor: pointer; }
|
||||
.task-status { font-size: 0.9rem; color: #86868b; width: 1.25rem; text-align: center; }
|
||||
.task-title { flex: 1; font-size: 0.875rem; color: #1d1d1f; font-weight: 500; }
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
font-size: 0.65rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
}
|
||||
.badge--red { background: rgba(255, 59, 48, 0.08); color: #ff3b30; }
|
||||
.badge--amber { background: rgba(255, 159, 10, 0.08); color: #c77d00; }
|
||||
.badge--cyan { background: rgba(0, 113, 227, 0.06); color: #0071e3; }
|
||||
.badge--blue { background: rgba(0, 113, 227, 0.06); color: #0071e3; }
|
||||
.badge--green { background: rgba(48, 209, 88, 0.08); color: #1a9d4a; }
|
||||
|
||||
/* Task body */
|
||||
.task-body { display: none; padding: 0 1rem 1rem; font-size: 0.85rem; color: #6e6e73; line-height: 1.55; }
|
||||
.task-desc p { margin: 0.25rem 0; }
|
||||
.task-actions { display: flex; gap: 0.35rem; margin-top: 0.5rem; flex-wrap: wrap; align-items: center; }
|
||||
.action-status {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
font-family: inherit;
|
||||
background: #ffffff;
|
||||
}
|
||||
.action-note {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
font-size: 0.8rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
.ai-out { margin-top: 0.5rem; }
|
||||
.ai-out pre {
|
||||
font-size: 0.8rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background: #f5f5f7;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.75rem;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.ai-saved summary { font-size: 0.75rem; color: #0071e3; cursor: pointer; font-weight: 600; }
|
||||
.ai-saved pre {
|
||||
font-size: 0.75rem;
|
||||
white-space: pre-wrap;
|
||||
background: rgba(0, 113, 227, 0.04);
|
||||
padding: 0.6rem;
|
||||
border-radius: 0.625rem;
|
||||
margin-top: 0.25rem;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* LinkedIn preview */
|
||||
.li-preview { background: #f5f5f7; border: 0.5px solid rgba(0, 0, 0, 0.06); border-radius: 0.75rem; padding: 0.85rem; margin-bottom: 0.5rem; }
|
||||
.li-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.6rem; }
|
||||
.li-avatar {
|
||||
width: 2rem; height: 2rem; border-radius: 50%;
|
||||
background: #0071e3; color: #fff;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.75rem; font-weight: 700;
|
||||
}
|
||||
.li-line { margin: 0 0 0.15rem; font-size: 0.8rem; color: #1d1d1f; }
|
||||
.li-spacer { height: 0.5rem; }
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.45rem 0.85rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.2s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
font-family: inherit;
|
||||
}
|
||||
.btn--primary {
|
||||
background: #0071e3;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(0, 113, 227, 0.2);
|
||||
}
|
||||
.btn--primary:hover {
|
||||
filter: brightness(1.08);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 113, 227, 0.25);
|
||||
}
|
||||
.btn--outline {
|
||||
background: #ffffff;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.1);
|
||||
color: #6e6e73;
|
||||
}
|
||||
.btn--outline:hover { color: #0071e3; border-color: rgba(0, 113, 227, 0.2); background: rgba(0, 113, 227, 0.04); }
|
||||
.btn--sm { padding: 0.3rem 0.6rem; font-size: 0.7rem; }
|
||||
|
||||
/* Dashboard KPIs */
|
||||
.kpi-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.kpi-card {
|
||||
background: #ffffff;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 1rem;
|
||||
padding: 1.25rem 1rem;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.kpi-card:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06); }
|
||||
.kpi-num {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #1d1d1f;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.kpi-label {
|
||||
font-size: 0.7rem;
|
||||
color: #86868b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-top: 0.15rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-family: Manrope, -apple-system, "SF Pro Display", sans-serif;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: #1d1d1f;
|
||||
margin: 1.5rem 0 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
/* Agents grid */
|
||||
.agents-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 0.65rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.agent-card {
|
||||
background: #ffffff;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 0.875rem;
|
||||
padding: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
.agent-card:hover {
|
||||
border-color: rgba(0, 113, 227, 0.15);
|
||||
box-shadow: 0 4px 16px rgba(0, 113, 227, 0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.agent-card b { font-size: 0.85rem; color: #0071e3; display: block; margin-bottom: 0.25rem; }
|
||||
.agent-card p { font-size: 0.75rem; color: #86868b; margin: 0.25rem 0 0; line-height: 1.4; }
|
||||
|
||||
/* Copilot */
|
||||
.copilot-bar { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; }
|
||||
.chat-output { min-height: 1.25rem; }
|
||||
.chat-output pre {
|
||||
font-size: 0.85rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background: #f5f5f7;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.04);
|
||||
padding: 1rem;
|
||||
border-radius: 0.875rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.quick-links { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 1.25rem; }
|
||||
|
||||
/* Services monitoring */
|
||||
.monitor-grid { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1.25rem; }
|
||||
.mon-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: #ffffff;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 999px;
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
.mon-card:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); }
|
||||
.mon-dot { width: 8px; height: 8px; border-radius: 50%; background: #d2d2d7; transition: background 0.3s; }
|
||||
.mon-ok { background: #30d158; }
|
||||
.mon-err { background: #ff3b30; }
|
||||
.mon-name { color: #1d1d1f; font-weight: 500; }
|
||||
.mon-ms { color: #86868b; font-size: 0.65rem; }
|
||||
|
||||
/* Services cards */
|
||||
.services-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 0.65rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.svc-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 0.6rem;
|
||||
background: #ffffff;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 1rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
.svc-card:hover {
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
.svc-card img { border-radius: 0.625rem; }
|
||||
.svc-card span { font-size: 0.75rem; color: #6e6e73; text-align: center; font-weight: 500; }
|
||||
.status-refresh { font-size: 1rem; cursor: pointer; color: #0071e3; font-weight: 500; }
|
||||
.status-refresh:hover { opacity: 0.7; }
|
||||
|
||||
/* Utilities */
|
||||
.loading { text-align: center; padding: 2rem; color: #0071e3; animation: pulse 1.5s infinite; font-weight: 500; }
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||
.err { color: #ff3b30; font-size: 0.85rem; }
|
||||
.hint { color: #86868b; font-size: 0.85rem; }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 640px) {
|
||||
.ops { padding: 1rem 0.75rem; }
|
||||
.ops-title { font-size: 1.25rem; }
|
||||
.tasks-toolbar { flex-direction: column; align-items: stretch; }
|
||||
.tasks-filters { flex-direction: column; }
|
||||
.task-actions { flex-direction: column; }
|
||||
.btn { width: 100%; justify-content: center; }
|
||||
.kpi-grid { grid-template-columns: repeat(3, 1fr); gap: 0.5rem; }
|
||||
.kpi-card { padding: 0.85rem 0.5rem; }
|
||||
.kpi-num { font-size: 1.5rem; }
|
||||
.services-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
.agents-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.copilot-bar { flex-direction: column; }
|
||||
.tab { padding: 0.45rem 0.6rem; font-size: 0.8rem; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,196 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import '@/styles/global.css';
|
||||
---
|
||||
|
||||
<BaseLayout title="Portfolio — L'Electron Rare" description="Projets électronique embarquée, prototypage et systèmes spécifiques.">
|
||||
<main class="portfolio-page">
|
||||
<h1 class="portfolio-title">Portfolio</h1>
|
||||
<p class="portfolio-intro">
|
||||
Explorez les schémas et PCB de projets réalisés. Les viewers interactifs
|
||||
permettent de zoomer, naviguer et inspecter chaque composant.
|
||||
</p>
|
||||
|
||||
<!-- KXKM ESP32 Base Board -->
|
||||
<section class="portfolio-section">
|
||||
<h2 class="portfolio-h2">KXKM — ESP32 Battery Management Board</h2>
|
||||
<p class="portfolio-desc">
|
||||
Carte de gestion batterie et ESP32 pour systèmes audio autonomes.
|
||||
Monitoring batterie, alimentation régulée, connectique terrain.
|
||||
<span class="portfolio-tag">ESP32</span>
|
||||
<span class="portfolio-tag">BMS</span>
|
||||
<span class="portfolio-tag">Audio</span>
|
||||
<span class="portfolio-tag">KXKM</span>
|
||||
</p>
|
||||
<div class="viewer-tabs">
|
||||
<button class="viewer-tab viewer-tab--active" data-target="kxkm-esp32-sch">Schéma</button>
|
||||
<button class="viewer-tab" data-target="kxkm-esp32-pcb">PCB</button>
|
||||
<button class="viewer-tab" data-target="kxkm-batt-sch">Monitoring batterie</button>
|
||||
<button class="viewer-tab" data-target="kxkm-power-sch">Alimentation</button>
|
||||
</div>
|
||||
<div class="viewer-panels">
|
||||
<div class="viewer-panel viewer-panel--active" id="kxkm-esp32-sch">
|
||||
<kicanvas-embed src="/assets/kicad/kxkm-esp32-base.kicad_sch" controls="full" theme="kicad" style="width:100%;height:600px;"></kicanvas-embed>
|
||||
</div>
|
||||
<div class="viewer-panel" id="kxkm-esp32-pcb">
|
||||
<kicanvas-embed src="/assets/kicad/kxkm-esp32-base.kicad_pcb" controls="full" theme="kicad" style="width:100%;height:600px;"></kicanvas-embed>
|
||||
</div>
|
||||
<div class="viewer-panel" id="kxkm-batt-sch">
|
||||
<kicanvas-embed src="/assets/kicad/kxkm-battery-monitoring.kicad_sch" controls="full" theme="kicad" style="width:100%;height:600px;"></kicanvas-embed>
|
||||
</div>
|
||||
<div class="viewer-panel" id="kxkm-power-sch">
|
||||
<kicanvas-embed src="/assets/kicad/kxkm-power.kicad_sch" controls="full" theme="kicad" style="width:100%;height:600px;"></kicanvas-embed>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- KXKM Batterie Parallelator -->
|
||||
<section class="portfolio-section">
|
||||
<h2 class="portfolio-h2">KXKM — Batterie Parallelator (BMU v2)</h2>
|
||||
<p class="portfolio-desc">
|
||||
Système de parallélisation de batteries lithium avec monitoring individuel,
|
||||
commutation MOSFET et bus I2C. Gestion intelligente multi-pack pour autonomie terrain.
|
||||
<span class="portfolio-tag">BMS</span>
|
||||
<span class="portfolio-tag">MOSFET</span>
|
||||
<span class="portfolio-tag">I2C</span>
|
||||
<span class="portfolio-tag">Lithium</span>
|
||||
</p>
|
||||
<div class="viewer-tabs">
|
||||
<button class="viewer-tab viewer-tab--active" data-target="para-bmu-sch">Schéma BMU</button>
|
||||
<button class="viewer-tab" data-target="para-bmu-pcb">PCB BMU</button>
|
||||
<button class="viewer-tab" data-target="para-mosfet-sch">Switch MOSFET</button>
|
||||
<button class="viewer-tab" data-target="para-mosfet-pcb">PCB MOSFET</button>
|
||||
<button class="viewer-tab" data-target="para-batt-sch">Batteries</button>
|
||||
<button class="viewer-tab" data-target="para-power-sch">Alimentation</button>
|
||||
</div>
|
||||
<div class="viewer-panels">
|
||||
<div class="viewer-panel viewer-panel--active" id="para-bmu-sch">
|
||||
<kicanvas-embed src="/assets/kicad/kxkm-parallelator-bmu-v2.kicad_sch" controls="full" theme="kicad" style="width:100%;height:600px;"></kicanvas-embed>
|
||||
</div>
|
||||
<div class="viewer-panel" id="para-bmu-pcb">
|
||||
<kicanvas-embed src="/assets/kicad/kxkm-parallelator-bmu-v2.kicad_pcb" controls="full" theme="kicad" style="width:100%;height:600px;"></kicanvas-embed>
|
||||
</div>
|
||||
<div class="viewer-panel" id="para-mosfet-sch">
|
||||
<kicanvas-embed src="/assets/kicad/kxkm-parallelator-mosfet.kicad_sch" controls="full" theme="kicad" style="width:100%;height:600px;"></kicanvas-embed>
|
||||
</div>
|
||||
<div class="viewer-panel" id="para-mosfet-pcb">
|
||||
<kicanvas-embed src="/assets/kicad/kxkm-parallelator-mosfet.kicad_pcb" controls="full" theme="kicad" style="width:100%;height:600px;"></kicanvas-embed>
|
||||
</div>
|
||||
<div class="viewer-panel" id="para-batt-sch">
|
||||
<kicanvas-embed src="/assets/kicad/kxkm-parallelator-batteries.kicad_sch" controls="full" theme="kicad" style="width:100%;height:600px;"></kicanvas-embed>
|
||||
</div>
|
||||
<div class="viewer-panel" id="para-power-sch">
|
||||
<kicanvas-embed src="/assets/kicad/kxkm-parallelator-power.kicad_sch" controls="full" theme="kicad" style="width:100%;height:600px;"></kicanvas-embed>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Upload custom -->
|
||||
<section class="portfolio-section">
|
||||
<h2 class="portfolio-h2">Viewer libre</h2>
|
||||
<p class="portfolio-desc">
|
||||
Chargez votre propre fichier <code>.kicad_sch</code> ou <code>.kicad_pcb</code> pour l'explorer.
|
||||
</p>
|
||||
<div class="portfolio-upload">
|
||||
<label for="kicad-file" class="portfolio-upload-label">
|
||||
Glissez un fichier KiCad ou cliquez pour ouvrir
|
||||
</label>
|
||||
<input type="file" id="kicad-file" accept=".kicad_sch,.kicad_pcb,.kicad_prj" class="portfolio-upload-input" />
|
||||
</div>
|
||||
<div id="viewer-container" class="viewer-wrap" style="display:none;">
|
||||
<kicanvas-embed id="kicad-viewer" controls="full" theme="kicad" style="width:100%;height:600px;"></kicanvas-embed>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Projets -->
|
||||
<section class="portfolio-section">
|
||||
<h2 class="portfolio-h2">Autres projets</h2>
|
||||
<div class="portfolio-grid">
|
||||
<div class="portfolio-card">
|
||||
<h3>Contrôleur LED DMX</h3>
|
||||
<p>Carte custom ESP32 pour pilotage LED haute puissance via DMX512. Spectacle vivant, KXKM.</p>
|
||||
<span class="portfolio-tag">ESP32</span><span class="portfolio-tag">DMX</span><span class="portfolio-tag">LED</span>
|
||||
</div>
|
||||
<div class="portfolio-card">
|
||||
<h3>Banc de test BMS</h3>
|
||||
<p>Système de test automatisé pour batteries lithium. Mesure, charge/décharge, logging.</p>
|
||||
<span class="portfolio-tag">BMS</span><span class="portfolio-tag">Mesure</span><span class="portfolio-tag">XT60</span>
|
||||
</div>
|
||||
<div class="portfolio-card">
|
||||
<h3>Automate Siemens S7</h3>
|
||||
<p>Diagnostic et reprogrammation d'automate industriel. Migration firmware, câblage armoire.</p>
|
||||
<span class="portfolio-tag">Siemens</span><span class="portfolio-tag">PLC</span><span class="portfolio-tag">Industrie</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p class="portfolio-note">
|
||||
Viewers propulsés par <a href="https://kicanvas.org" target="_blank" rel="noopener" class="portfolio-link">KiCanvas</a> — open source.
|
||||
Fichiers source : <a href="https://github.com/KomplexKapharnaum/KXKM_ESP32_Audio_Battery_hardware" target="_blank" rel="noopener" class="portfolio-link">GitHub KXKM</a>.
|
||||
</p>
|
||||
|
||||
<div class="portfolio-cta">
|
||||
<a href="/preview#contact" class="portfolio-btn">Discuter de votre projet →</a>
|
||||
</div>
|
||||
</main>
|
||||
</BaseLayout>
|
||||
|
||||
<script type="module" src="/assets/js/kicanvas.js"></script>
|
||||
<script>
|
||||
// Tab switching for viewer panels
|
||||
document.querySelectorAll('.viewer-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const section = tab.closest('.portfolio-section');
|
||||
section.querySelectorAll('.viewer-tab').forEach(t => t.classList.remove('viewer-tab--active'));
|
||||
section.querySelectorAll('.viewer-panel').forEach(p => p.classList.remove('viewer-panel--active'));
|
||||
tab.classList.add('viewer-tab--active');
|
||||
document.getElementById(tab.dataset.target)?.classList.add('viewer-panel--active');
|
||||
});
|
||||
});
|
||||
|
||||
// File upload
|
||||
const fileInput = document.getElementById('kicad-file');
|
||||
const viewerContainer = document.getElementById('viewer-container');
|
||||
const viewer = document.getElementById('kicad-viewer');
|
||||
fileInput?.addEventListener('change', (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
viewer?.setAttribute('src', URL.createObjectURL(file));
|
||||
if (viewerContainer) viewerContainer.style.display = 'block';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.portfolio-page { max-width:960px; margin:0 auto; padding:48px 24px; min-height:100vh; }
|
||||
.portfolio-title { font-family:'Fraunces',serif; font-size:clamp(28px,5vw,42px); color:var(--text,#f4eee3); margin:0 0 16px; }
|
||||
.portfolio-intro { font-family:'IBM Plex Mono',monospace; font-size:14px; color:var(--text-muted,#d6cbb9); line-height:1.7; margin:0 0 40px; }
|
||||
.portfolio-section { margin-bottom:48px; }
|
||||
.portfolio-h2 { font-family:'Fraunces',serif; font-size:22px; color:var(--accent,#f1c27a); margin:0 0 12px; }
|
||||
.portfolio-desc { font-size:14px; color:var(--text-dim,#b6a894); margin:0 0 16px; line-height:1.6; }
|
||||
.portfolio-desc code { color:var(--electric,#5bd1d8); background:var(--surface,#1a1510); padding:2px 6px; border-radius:3px; font-size:13px; }
|
||||
|
||||
.viewer-tabs { display:flex; gap:4px; margin-bottom:12px; border-bottom:1px solid var(--line,#3a2f24); }
|
||||
.viewer-tab { font-family:'IBM Plex Mono',monospace; font-size:12px; padding:8px 14px; background:none; border:none; color:var(--text-dim,#b6a894); cursor:pointer; border-bottom:2px solid transparent; transition:all .2s; }
|
||||
.viewer-tab:hover { color:var(--text,#f4eee3); }
|
||||
.viewer-tab--active { color:var(--electric,#5bd1d8); border-bottom-color:var(--electric,#5bd1d8); }
|
||||
.viewer-panel { display:none; }
|
||||
.viewer-panel--active { display:block; }
|
||||
.viewer-wrap { border-radius:8px; overflow:hidden; border:1px solid var(--line,#3a2f24); }
|
||||
|
||||
.portfolio-upload { border:2px dashed var(--line,#3a2f24); border-radius:8px; padding:32px; text-align:center; margin-bottom:20px; transition:border-color .2s; }
|
||||
.portfolio-upload:hover { border-color:var(--electric,#5bd1d8); }
|
||||
.portfolio-upload-label { font-family:'IBM Plex Mono',monospace; font-size:14px; color:var(--text-muted,#d6cbb9); cursor:pointer; display:block; }
|
||||
.portfolio-upload-input { position:absolute; opacity:0; width:0; height:0; }
|
||||
|
||||
.portfolio-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:16px; }
|
||||
.portfolio-card { background:var(--surface,#1a1510); border:1px solid var(--line,#3a2f24); border-radius:8px; padding:20px; }
|
||||
.portfolio-card h3 { font-family:'Fraunces',serif; font-size:18px; color:var(--text,#f4eee3); margin:0 0 8px; }
|
||||
.portfolio-card p { font-size:13px; color:var(--text-muted,#d6cbb9); line-height:1.6; margin:0 0 12px; }
|
||||
.portfolio-tag { display:inline-block; font-family:'IBM Plex Mono',monospace; font-size:11px; color:var(--electric,#5bd1d8); background:rgba(91,209,216,.1); padding:2px 8px; border-radius:3px; margin-right:4px; margin-bottom:4px; }
|
||||
|
||||
.portfolio-note { font-size:12px; color:var(--text-dim,#b6a894); opacity:.6; margin:0 0 24px; }
|
||||
.portfolio-link { color:var(--electric,#5bd1d8); }
|
||||
.portfolio-cta { text-align:center; margin-top:32px; }
|
||||
.portfolio-btn { display:inline-block; padding:12px 28px; font-family:'IBM Plex Mono',monospace; font-size:14px; color:var(--bg,#0b0a09); background:var(--accent,#f1c27a); text-decoration:none; border-radius:4px; transition:all .2s; }
|
||||
.portfolio-btn:hover { background:var(--electric,#5bd1d8); }
|
||||
</style>
|
||||
@@ -0,0 +1,307 @@
|
||||
---
|
||||
import { BmuViewer } from '@/components/BmuViewer';
|
||||
---
|
||||
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>L'électron rare — Systèmes électroniques spécifiques</title>
|
||||
<meta name="description" content="Conception, mise au point et fiabilisation de systèmes électroniques spécifiques, automatismes et ensembles liés à l'énergie. Consulting et formation PCB, KiCad, µC." />
|
||||
<meta property="og:title" content="L'électron rare — Systèmes électroniques spécifiques" />
|
||||
<meta property="og:description" content="Conception, mise au point et fiabilisation de systèmes électroniques spécifiques. Consulting et formation." />
|
||||
<meta property="og:image" content="https://www.lelectronrare.fr/assets/og-cover.jpg" />
|
||||
<meta property="og:url" content="https://www.lelectronrare.fr/preview/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="L'électron rare" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="L'électron rare — Systèmes électroniques spécifiques" />
|
||||
<meta name="twitter:description" content="Conception, mise au point et fiabilisation de systèmes électroniques spécifiques." />
|
||||
<meta name="twitter:image" content="https://www.lelectronrare.fr/assets/og-cover.jpg" />
|
||||
<link rel="me" href="https://mastodon.saillant.cc/@clement" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ProfessionalService",
|
||||
"name": "L'électron rare",
|
||||
"description": "Conception, mise au point et fiabilisation de systèmes électroniques spécifiques, automatismes et ensembles liés à l'énergie.",
|
||||
"url": "https://www.lelectronrare.fr",
|
||||
"email": "contact@lelectronrare.fr",
|
||||
"founder": { "@type": "Person", "name": "Clément Saillant" },
|
||||
"areaServed": "France",
|
||||
"serviceType": ["Conception électronique", "Formation PCB et embarqué", "Consulting technique", "Automatisme industriel"],
|
||||
"sameAs": ["https://fr.linkedin.com/in/electron-rare", "https://github.com/electron-rare/", "https://mastodon.saillant.cc/@clement"]
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { background: #06080b; color: #fff; overflow-x: hidden; }
|
||||
body { background: #06080b; min-height: 100vh; font-family: 'Courier New', monospace; }
|
||||
.scroll-track { height: 1000vh; position: relative; z-index: 1; }
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
|
||||
|
||||
/* Loader */
|
||||
.loader { position: fixed; inset: 0; z-index: 9999; background: #06080b; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; transition: opacity 0.8s ease, visibility 0.8s ease; }
|
||||
.loader.done { opacity: 0; visibility: hidden; pointer-events: none; }
|
||||
.loader-logo { width: clamp(60px, 8vw, 100px); height: auto; filter: drop-shadow(0 0 20px rgba(91,209,216,0.3)); }
|
||||
.loader-name { font-size: clamp(20px, 2.5vw, 32px); font-weight: 800; color: rgba(255,255,255,0.8); letter-spacing: 0.06em; }
|
||||
.loader-bar { width: clamp(180px, 30vw, 280px); height: 2px; background: rgba(255,255,255,0.06); border-radius: 1px; overflow: hidden; }
|
||||
.loader-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #5bd1d8, #a78bfa); transition: width 0.3s ease; }
|
||||
.loader-status { font-size: 10px; color: rgba(255,255,255,0.25); letter-spacing: 0.12em; text-transform: uppercase; }
|
||||
.loader-tagline { font-size: 12px; color: rgba(255,255,255,0.3); letter-spacing: 0.08em; margin-bottom: 20px; }
|
||||
.loader-actions { margin-top: 24px; }
|
||||
.loader-skip { display: inline-block; padding: 10px 24px; border: 1px solid rgba(91,209,216,0.3); border-radius: 8px; color: rgba(91,209,216,0.7); font-size: 12px; text-decoration: none; letter-spacing: 0.04em; transition: all 0.2s; }
|
||||
.loader-skip:hover { border-color: #5bd1d8; color: #5bd1d8; background: rgba(91,209,216,0.08); }
|
||||
.loader-a11y { margin-top: 20px; font-size: 10px; color: rgba(255,255,255,0.15); }
|
||||
.loader-a11y a { color: rgba(255,255,255,0.25); text-decoration: none; }
|
||||
.loader-a11y a:hover { color: #5bd1d8; }
|
||||
|
||||
.ov { position: fixed; z-index: 3; pointer-events: none; opacity: 0; transform: translateY(6px); transition: opacity 0.5s, transform 0.5s; }
|
||||
.ov.on { opacity: 1; transform: translateY(0); }
|
||||
|
||||
/* Hero */
|
||||
.ov-hero { inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; }
|
||||
.hero-bottom { display: flex; flex-direction: column; align-items: center; padding-bottom: clamp(60px, 10vh, 120px); }
|
||||
.hero-t { text-align: center; margin: 0; line-height: 1.1; }
|
||||
.hl1,.hl3 { display: block; font-size: clamp(28px, 4.5vw, 56px); font-weight: 800; color: rgba(255,255,255,0.85); letter-spacing: -0.02em; }
|
||||
.hl2 { display: block; font-size: clamp(32px, 5.5vw, 64px); font-weight: 800; color: #5bd1d8; letter-spacing: -0.02em; }
|
||||
.hero-s { font-size: clamp(11px, 1.2vw, 15px); color: rgba(255,255,255,0.3); letter-spacing: 0.15em; text-transform: uppercase; margin: 16px 0 0; }
|
||||
.hero-s2 { font-size: clamp(10px, 1vw, 13px); color: rgba(255,255,255,0.2); letter-spacing: 0.12em; text-transform: uppercase; margin: 8px 0 0; }
|
||||
|
||||
/* Header */
|
||||
.site-hdr { position: fixed; top: 0; left: 0; right: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; padding: 16px clamp(20px, 3vw, 40px); pointer-events: none; }
|
||||
.hdr-left { display: flex; align-items: center; gap: 10px; pointer-events: auto; }
|
||||
.hdr-logo { width: 36px; height: auto; filter: drop-shadow(0 0 12px rgba(91,209,216,0.3)); }
|
||||
.hdr-name { font-size: 16px; font-weight: 800; color: rgba(255,255,255,0.8); letter-spacing: 0.04em; }
|
||||
.hdr-nav { display: flex; align-items: center; gap: 8px; pointer-events: auto; }
|
||||
.hdr-link { padding: 8px 16px; color: rgba(255,255,255,0.6); font-size: 12px; font-weight: 600; text-decoration: none; letter-spacing: 0.04em; transition: color 0.2s; }
|
||||
.hdr-link:hover { color: #5bd1d8; }
|
||||
.hdr-social { font-weight: 800; font-size: 11px; text-transform: uppercase; padding: 8px 10px; opacity: 0.5; }
|
||||
.hdr-social:hover { opacity: 1; }
|
||||
.hdr-cta { padding: 8px 20px; border-radius: 6px; background: rgba(91,209,216,0.15); border: 1px solid rgba(91,209,216,0.3); color: #5bd1d8; font-size: 12px; font-weight: 700; text-decoration: none; letter-spacing: 0.05em; transition: background 0.2s; }
|
||||
.hdr-cta:hover { background: rgba(91,209,216,0.25); }
|
||||
|
||||
/* Formats */
|
||||
.ov-formats { bottom: clamp(30px, 5vh, 60px); left: 50%; transform: translateX(-50%); display: flex; gap: 12px; max-width: 820px; width: 92vw; }
|
||||
.ov-formats.on { transform: translateX(-50%); }
|
||||
.fmt { flex: 1; padding: 14px 16px; border-radius: 8px; background: rgba(6,8,11,0.7); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.05); text-align: center; text-decoration: none; color: inherit; cursor: pointer; transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease; }
|
||||
.fmt:hover { background: rgba(6,8,11,0.85); border-color: rgba(255,255,255,0.18); transform: translateY(-2px); }
|
||||
.fmt-t { font-size: 13px; font-weight: 700; color: #fff; letter-spacing: 0.06em; text-transform: uppercase; margin-bottom: 4px; }
|
||||
.fmt-d { font-size: 10px; color: rgba(255,255,255,0.35); }
|
||||
|
||||
/* Progress */
|
||||
.prog { position: fixed; top: 0; left: 0; height: 2px; z-index: 5; background: linear-gradient(90deg, #5bd1d8, #a78bfa, #f59e0b); width: 0%; }
|
||||
.inf { position: fixed; bottom: 10px; right: 14px; z-index: 4; font-size: 8px; color: rgba(255,255,255,0.1); letter-spacing: 0.06em; }
|
||||
|
||||
/* Dots */
|
||||
.dots { position: fixed; right: 20px; top: 50%; transform: translateY(-50%); z-index: 4; display: flex; flex-direction: column; gap: 14px; }
|
||||
.dot { width: 12px; height: 12px; border-radius: 50%; background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.1); transition: background 0.3s, transform 0.3s, border-color 0.3s; cursor: pointer; pointer-events: auto; position: relative; }
|
||||
.dot.on { background: #5bd1d8; border-color: #5bd1d8; transform: scale(1.3); box-shadow: 0 0 10px rgba(91,209,216,0.4); }
|
||||
.dot:hover { background: rgba(91,209,216,0.5); border-color: rgba(91,209,216,0.5); }
|
||||
.dot[data-l]:not([data-l=""])::after { content: attr(data-l); position: absolute; right: 20px; top: 50%; transform: translateY(-50%); font-size: 9px; color: rgba(255,255,255,0.5); letter-spacing: 0.05em; white-space: nowrap; opacity: 0; transition: opacity 0.2s; pointer-events: none; }
|
||||
.dot[data-l]:not([data-l=""]):hover::after { opacity: 1; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ov-formats { flex-direction: column; gap: 6px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-da-variant="preview">
|
||||
<!-- Loader -->
|
||||
<div class="loader" id="loader">
|
||||
<img src="/assets/brand/logo-mark.svg" alt="L'électron rare" class="loader-logo" />
|
||||
<p class="loader-name">L'électron rare</p>
|
||||
<p class="loader-tagline">Systèmes électroniques spécifiques</p>
|
||||
<div class="loader-bar"><div class="loader-fill" id="loader-fill"></div></div>
|
||||
<p class="loader-status" id="loader-status">Chargement des modèles 3D...</p>
|
||||
<div class="loader-actions">
|
||||
<a href="/conception/" class="loader-skip">Accéder au site sans 3D →</a>
|
||||
</div>
|
||||
<p class="loader-a11y">Version accessible : <a href="/conception/">Conception</a> · <a href="/formation/">Formation</a> · <a href="/consulting/">Consulting</a> · <a href="/contact/">Contact</a></p>
|
||||
</div>
|
||||
|
||||
<BmuViewer client:load />
|
||||
<div class="prog" id="prog"></div>
|
||||
|
||||
<div class="dots" id="dots">
|
||||
<div class="dot on" data-s="hero" data-p="0.03" data-l="Hero"></div>
|
||||
<div class="dot" data-s="hero-to-exp" data-p="0.09" data-l=""></div>
|
||||
<div class="dot" data-s="exp-elec" data-p="0.14" data-l="Élec"></div>
|
||||
<div class="dot" data-s="exp-auto" data-p="0.18" data-l="Auto"></div>
|
||||
<div class="dot" data-s="exp-energie" data-p="0.22" data-l="Énergie"></div>
|
||||
<div class="dot" data-s="exp-dispositifs" data-p="0.255" data-l="Dispositifs"></div>
|
||||
<div class="dot" data-s="exp-consulting" data-p="0.285" data-l="Consulting"></div>
|
||||
<div class="dot" data-s="eclate-recul" data-p="0.31" data-l=""></div>
|
||||
<div class="dot" data-s="eclate-sep" data-p="0.34" data-l=""></div>
|
||||
<div class="dot" data-s="eclate-plat" data-p="0.36" data-l=""></div>
|
||||
<div class="dot" data-s="cas-kxkm" data-p="0.42" data-l="KXKM"></div>
|
||||
<div class="dot" data-s="cas-industrie" data-p="0.52" data-l="Industrie"></div>
|
||||
<div class="dot" data-s="cas-formation" data-p="0.60" data-l="Formation"></div>
|
||||
<div class="dot" data-s="media" data-p="0.70" data-l="Médias"></div>
|
||||
<div class="dot" data-s="formats" data-p="0.85" data-l="Services"></div>
|
||||
<div class="dot" data-s="contact" data-p="0.95" data-l="Contact"></div>
|
||||
</div>
|
||||
|
||||
<!-- HEADER -->
|
||||
<header class="site-hdr">
|
||||
<div class="hdr-left">
|
||||
<img src="/assets/brand/logo-mark.svg" alt="" class="hdr-logo" />
|
||||
<span class="hdr-name">L'électron rare</span>
|
||||
</div>
|
||||
<nav class="hdr-nav">
|
||||
<a href="https://www.lelectronrare.fr/#graphic-sprints-title" class="hdr-link">Conception</a>
|
||||
<a href="https://www.lelectronrare.fr/formation/" class="hdr-link">Formation</a>
|
||||
<a href="https://www.lelectronrare.fr/#a-propos" class="hdr-link">Consulting</a>
|
||||
<a href="https://fr.linkedin.com/in/electron-rare" target="_blank" rel="noopener" class="hdr-link hdr-social" title="LinkedIn">in</a>
|
||||
<a href="https://github.com/electron-rare/" target="_blank" rel="noopener" class="hdr-link hdr-social" title="GitHub">gh</a>
|
||||
<a href="https://mastodon.saillant.cc/@clement" target="_blank" rel="me noopener" class="hdr-link hdr-social" title="Mastodon">mt</a>
|
||||
<a href="https://www.lelectronrare.fr/" class="hdr-link" style="opacity:0.4">Site →</a>
|
||||
<a href="https://www.lelectronrare.fr/#contact" class="hdr-cta">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- HERO -->
|
||||
<div class="ov ov-hero on" data-sec="hero">
|
||||
<div class="hero-bottom">
|
||||
<h1 class="hero-t">
|
||||
<span class="hl1">Systèmes</span>
|
||||
<span class="hl2">électroniques</span>
|
||||
<span class="hl3">spécifiques</span>
|
||||
</h1>
|
||||
<p class="hero-s">électronique · automatisme · énergie</p>
|
||||
<p class="hero-s2">Conception · Mise au point · Fiabilisation · Consulting · Formation</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FORMATS -->
|
||||
<div class="ov ov-formats" data-sec="formats">
|
||||
<a class="fmt" href="/conception/">
|
||||
<p class="fmt-t" style="color:#5bd1d8">Conception</p>
|
||||
<p class="fmt-d">Schéma · PCB · Firmware · Validation</p>
|
||||
</a>
|
||||
<a class="fmt" href="/formation/">
|
||||
<p class="fmt-t" style="color:#a78bfa">Formation</p>
|
||||
<p class="fmt-d">PCB · KiCad · µC · Soudure</p>
|
||||
</a>
|
||||
<a class="fmt" href="/consulting/">
|
||||
<p class="fmt-t" style="color:#f59e0b">Consulting</p>
|
||||
<p class="fmt-d">Audit · Diagnostic · Transfert</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="scroll-track"></div>
|
||||
|
||||
<!-- SEO: texte indexable caché -->
|
||||
<div class="sr-only" aria-hidden="false">
|
||||
<h1>L'électron rare — Systèmes électroniques spécifiques</h1>
|
||||
<p>Conception, mise au point et fiabilisation de systèmes électroniques spécifiques, automatismes et ensembles liés à l'énergie, au stockage et à l'optimisation terrain.</p>
|
||||
<h2>Domaines d'expertise</h2>
|
||||
<ul>
|
||||
<li>Électronique spécifique : cartes, interfaces, capteurs, alimentation</li>
|
||||
<li>Instrumentation et automatisme : bancs, automates, variateurs, protocoles terrain</li>
|
||||
<li>Énergie et stockage : batterie, conversion, supervision, télémétrie</li>
|
||||
<li>Dispositifs pour le réel : audio, LED, scène, robustesse terrain</li>
|
||||
<li>Consulting et formation : audit, transfert, pédagogie, projet</li>
|
||||
</ul>
|
||||
<h2>Cas concrets</h2>
|
||||
<ul>
|
||||
<li>Industries créatives : système audio embarqué sur batterie pour le spectacle (KXKM)</li>
|
||||
<li>Industrie : diagnostic et remise en service d'une ligne automatisée</li>
|
||||
<li>Formation : formation PCB et embarqué pour une école d'art numérique</li>
|
||||
</ul>
|
||||
<h2>Services</h2>
|
||||
<ul>
|
||||
<li>Conception : schéma, PCB, firmware, validation</li>
|
||||
<li>Formation : PCB, KiCad, microcontrôleurs, soudure</li>
|
||||
<li>Consulting : audit, diagnostic, transfert</li>
|
||||
</ul>
|
||||
<p>Contact : <a href="https://www.lelectronrare.fr/#contact">Parlons de votre projet</a> — contact@lelectronrare.fr</p>
|
||||
</div>
|
||||
|
||||
<noscript>
|
||||
<div style="position:fixed;inset:0;z-index:9999;background:#06080b;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;font-family:monospace;text-align:center;padding:40px;">
|
||||
<img src="/assets/photos/bmu-assembly.jpg" alt="BMU v2 Assembly" style="width:clamp(200px,50vw,500px);border-radius:12px;margin-bottom:20px;" />
|
||||
<h1 style="font-size:24px;">L'électron rare</h1>
|
||||
<p style="color:rgba(255,255,255,0.5);margin:12px 0;">Systèmes électroniques spécifiques</p>
|
||||
<a href="https://www.lelectronrare.fr/" style="color:#5bd1d8;margin-top:16px;">Accéder au site →</a>
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<div class="inf">L'électron rare · Clément Saillant</div>
|
||||
|
||||
<!-- GTM for A/B tracking -->
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!=='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-5SLM67QF');
|
||||
</script>
|
||||
|
||||
<script>
|
||||
const prog = document.getElementById('prog');
|
||||
const dots = [...document.querySelectorAll('.dot')];
|
||||
const overlays = [...document.querySelectorAll('.ov')];
|
||||
const RANGES = [
|
||||
['hero',0,0.10],['hero-to-exp',0.10,0.16],
|
||||
['exp-elec',0.16,0.20],['exp-auto',0.20,0.24],['exp-energie',0.24,0.28],
|
||||
['exp-dispositifs',0.28,0.31],['exp-consulting',0.31,0.34],
|
||||
['eclate-recul',0.34,0.36],['eclate-sep',0.36,0.39],['eclate-plat',0.39,0.41],
|
||||
['cas',0.41,0.65],
|
||||
['media',0.65,0.81],['formats',0.81,0.91],['contact',0.91,1.0],
|
||||
];
|
||||
function update() {
|
||||
const max = document.documentElement.scrollHeight - window.innerHeight;
|
||||
const t = max > 0 ? window.scrollY / max : 0;
|
||||
if (prog) prog.style.width = (t*100)+'%';
|
||||
let active = 'hero';
|
||||
for (const [id,s,e] of RANGES) { if (t>=s&&t<=e){active=id;break;} }
|
||||
overlays.forEach(el => el.classList.toggle('on', el.getAttribute('data-sec')===active));
|
||||
dots.forEach(d => d.classList.toggle('on', d.getAttribute('data-s')===active));
|
||||
}
|
||||
window.addEventListener('scroll', update, {passive:true});
|
||||
update();
|
||||
|
||||
// Dots cliquables
|
||||
dots.forEach(d => {
|
||||
d.addEventListener('click', () => {
|
||||
const p = parseFloat(d.getAttribute('data-p') || '0');
|
||||
const max = document.documentElement.scrollHeight - window.innerHeight;
|
||||
window.scrollTo({ top: Math.round(max * p), behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
|
||||
// GA4 tracking
|
||||
document.querySelectorAll('a[href]').forEach(a => {
|
||||
a.addEventListener('click', () => {
|
||||
const label = a.textContent?.trim() || a.getAttribute('href') || '';
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.dataLayer.push({ event: 'click', event_category: 'navigation', event_label: label, da_variant: 'preview', page_path: '/preview/' });
|
||||
});
|
||||
});
|
||||
|
||||
// Loader
|
||||
const loader = document.getElementById('loader');
|
||||
const loaderFill = document.getElementById('loader-fill');
|
||||
const loaderStatus = document.getElementById('loader-status');
|
||||
const msgs = ['Chargement des modèles 3D...', 'Compilation shaders...', 'Préparation du rendu...'];
|
||||
let msgIdx = 0;
|
||||
const msgInterval = setInterval(() => {
|
||||
msgIdx = (msgIdx + 1) % msgs.length;
|
||||
if (loaderStatus) loaderStatus.textContent = msgs[msgIdx];
|
||||
if (loaderFill) loaderFill.style.width = Math.min(90, parseFloat(loaderFill.style.width || '0') + 20) + '%';
|
||||
}, 1200);
|
||||
|
||||
function hideLoader() {
|
||||
if (loaderFill) loaderFill.style.width = '100%';
|
||||
if (loaderStatus) loaderStatus.textContent = 'Prêt';
|
||||
clearInterval(msgInterval);
|
||||
setTimeout(() => { if (loader) loader.classList.add('done'); }, 400);
|
||||
}
|
||||
|
||||
const checkReady = setInterval(() => {
|
||||
const cvs = document.querySelector('canvas');
|
||||
if (cvs && cvs.width > 0) { clearInterval(checkReady); setTimeout(hideLoader, 800); }
|
||||
}, 200);
|
||||
setTimeout(hideLoader, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,6 +8,11 @@ Disallow: /
|
||||
`
|
||||
: `User-agent: *
|
||||
Allow: /
|
||||
Disallow: /preview
|
||||
Disallow: /ops
|
||||
Disallow: /intranet
|
||||
Disallow: /portfolio
|
||||
Disallow: /countdown
|
||||
|
||||
Sitemap: ${CANONICAL_URL}sitemap.xml
|
||||
`;
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { CANONICAL_URL, PUBLIC_SITE_ROOT_URL, SITE_IS_SUBPATH_BUILD } from '../lib/site';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const GET: APIRoute = () => {
|
||||
const sitemapRootUrl = SITE_IS_SUBPATH_BUILD ? PUBLIC_SITE_ROOT_URL : CANONICAL_URL;
|
||||
const sitemapRootUrl = process.env.PUBLIC_SITE_URL || CANONICAL_URL;
|
||||
const pages = [
|
||||
{ path: '', priority: '1.0', changefreq: 'weekly' },
|
||||
{ path: 'formation/', priority: '0.9', changefreq: 'monthly' },
|
||||
{ path: 'preview/', priority: '0.8', changefreq: 'weekly' },
|
||||
{ path: 'mentions-legales/', priority: '0.3', changefreq: 'yearly' }
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import '@/styles/global.css';
|
||||
import { About } from '@/components/sections/About';
|
||||
import { CaseStudies } from '@/components/sections/CaseStudies';
|
||||
import { GraphicSprints } from '@/components/sections/GraphicSprints';
|
||||
import Contact from '@/components/sections/Contact.astro';
|
||||
import Faq from '@/components/sections/Faq.astro';
|
||||
import { withSiteBase } from '@/lib/site';
|
||||
import { TRACK_EVENTS, trackAttrs } from '@/lib/tracking';
|
||||
|
||||
const formationHref = withSiteBase('/formation/');
|
||||
const mentionsHref = withSiteBase('/mentions-legales/');
|
||||
---
|
||||
|
||||
<BaseLayout title="L'électron rare — Version accessible" description="Systèmes électroniques spécifiques. Version sans animations 3D, optimisée pour l'accessibilité.">
|
||||
<a class="skip-link" href="#main-content">Aller au contenu principal</a>
|
||||
|
||||
<header class="static-header" role="banner">
|
||||
<nav aria-label="Navigation principale" class="static-nav">
|
||||
<a href="#top" class="static-brand">L'électron rare</a>
|
||||
<ul class="static-nav-list" role="list">
|
||||
<li><a href="#a-propos">Approche</a></li>
|
||||
<li><a href="#cas-concrets">Cas concrets</a></li>
|
||||
<li><a href="#missions">Missions</a></li>
|
||||
<li><a href={formationHref}>Formation</a></li>
|
||||
<li><a href="#faq">FAQ</a></li>
|
||||
<li><a href="#contact" class="static-cta">Contact</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main id="main-content">
|
||||
<!-- HERO -->
|
||||
<section class="static-hero" id="top" aria-labelledby="hero-title">
|
||||
<h1 id="hero-title" class="static-hero-title">
|
||||
Systèmes électroniques spécifiques, du besoin au livrable fiable
|
||||
</h1>
|
||||
<p class="static-hero-sub">
|
||||
L'Électron Rare conçoit, met au point et fiabilise des systèmes électroniques spécifiques
|
||||
en électronique, automatisme, énergie et stockage, avec les partenaires adaptés
|
||||
quand le projet devient multi-technique.
|
||||
</p>
|
||||
<div class="static-hero-actions">
|
||||
<a href="#contact" class="static-btn static-btn--primary">Discuter de votre projet</a>
|
||||
<a href="/preview" class="static-btn static-btn--secondary">Version 3D</a>
|
||||
</div>
|
||||
<p class="static-hero-kicker" aria-hidden="true">électronique · automatisme · énergie · stockage</p>
|
||||
</section>
|
||||
|
||||
<!-- ABOUT -->
|
||||
<section id="a-propos" class="static-section" aria-labelledby="about-title">
|
||||
<About />
|
||||
</section>
|
||||
|
||||
<!-- CASE STUDIES -->
|
||||
<section id="cas-concrets" class="static-section" aria-labelledby="cases-title">
|
||||
<CaseStudies />
|
||||
</section>
|
||||
|
||||
<!-- PHOTOS -->
|
||||
<section class="static-section static-photos" aria-label="Photos de terrain">
|
||||
<h2 class="static-section-title">Terrain</h2>
|
||||
<div class="static-photo-grid">
|
||||
<img src={withSiteBase('/assets/photos/pcb-teensy-led-kxkm.webp')} alt="PCB custom Teensy LED — KXKM" loading="lazy" width="280" height="190" />
|
||||
<img src={withSiteBase('/assets/photos/automate-siemens-s7.webp')} alt="Automate Siemens S7" loading="lazy" width="280" height="190" />
|
||||
<img src={withSiteBase('/assets/photos/bench-bms-batteries-xt60.webp')} alt="Banc de test BMS batteries" loading="lazy" width="280" height="190" />
|
||||
<img src={withSiteBase('/assets/photos/soudure-pcb-composants.webp')} alt="Soudure PCB composants" loading="lazy" width="280" height="190" />
|
||||
<img src={withSiteBase('/assets/photos/oscilloscope-philips-vintage.webp')} alt="Oscilloscope — instrumentation" loading="lazy" width="280" height="190" />
|
||||
<img src={withSiteBase('/assets/photos/armoire-automate-schneider.webp')} alt="Armoire Schneider — automatisme" loading="lazy" width="280" height="190" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SPRINTS -->
|
||||
<section id="missions" class="static-section" aria-labelledby="sprints-title">
|
||||
<GraphicSprints />
|
||||
</section>
|
||||
|
||||
<!-- FAQ -->
|
||||
<section id="faq" class="static-section" aria-labelledby="faq-title">
|
||||
<Faq />
|
||||
</section>
|
||||
|
||||
<!-- CONTACT -->
|
||||
<section id="contact" class="static-section" aria-labelledby="contact-title">
|
||||
<Contact />
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="static-footer" role="contentinfo">
|
||||
<div class="static-footer-inner">
|
||||
<p class="static-footer-brand">L'électron rare</p>
|
||||
<p class="static-footer-copy">© 2026 Clément Saillant — systèmes électroniques spécifiques, conseil, formation — industrie, culture et projets multi-techniques.</p>
|
||||
<p class="static-footer-contact">Contact : <a href="https://fr.linkedin.com/in/electron-rare">LinkedIn</a> · <a href="mailto:contact@lelectronrare.fr">contact@lelectronrare.fr</a></p>
|
||||
<nav aria-label="Liens de pied de page" class="static-footer-links">
|
||||
<a href="#a-propos">Approche</a>
|
||||
<a href="#missions">Missions</a>
|
||||
<a href={formationHref}>Formation</a>
|
||||
<a href="#faq">FAQ</a>
|
||||
<a href="https://github.com/electron-rare/">GitHub</a>
|
||||
<a href="https://blog.saillant.cc">Blog</a>
|
||||
<a href={mentionsHref}>Mentions légales</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
/* ============ ACCESSIBLE STATIC VERSION ============ */
|
||||
/* High contrast, no animations, semantic structure */
|
||||
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #0071e3;
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border-radius: 0 0 8px 8px;
|
||||
z-index: 1000;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.static-header {
|
||||
background: #fff;
|
||||
border-bottom: 2px solid #1d1d1f;
|
||||
padding: 12px clamp(16px, 4vw, 48px);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
.static-nav {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.static-brand {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: #1d1d1f;
|
||||
text-decoration: none;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.static-nav-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
.static-nav-list a {
|
||||
display: block;
|
||||
padding: 6px 12px;
|
||||
color: #1d1d1f;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.static-nav-list a:hover,
|
||||
.static-nav-list a:focus {
|
||||
background: #f0f0f0;
|
||||
outline: 2px solid #0071e3;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.static-cta {
|
||||
background: #0071e3 !important;
|
||||
color: #fff !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* Hero */
|
||||
.static-hero {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: clamp(40px, 8vw, 80px) clamp(16px, 4vw, 48px);
|
||||
text-align: center;
|
||||
}
|
||||
.static-hero-title {
|
||||
font-size: clamp(28px, 5vw, 48px);
|
||||
font-weight: 800;
|
||||
color: #1d1d1f;
|
||||
line-height: 1.15;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
.static-hero-sub {
|
||||
font-size: clamp(16px, 2vw, 20px);
|
||||
color: #6e6e73;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
.static-hero-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.static-btn {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: none;
|
||||
}
|
||||
.static-btn--primary {
|
||||
background: #0071e3;
|
||||
color: #fff;
|
||||
}
|
||||
.static-btn--primary:hover,
|
||||
.static-btn--primary:focus {
|
||||
background: #005bb5;
|
||||
outline: 3px solid #0071e3;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.static-btn--secondary {
|
||||
background: #f5f5f7;
|
||||
color: #1d1d1f;
|
||||
border: 1px solid #d2d2d7;
|
||||
}
|
||||
.static-btn--secondary:hover,
|
||||
.static-btn--secondary:focus {
|
||||
background: #e8e8e8;
|
||||
outline: 3px solid #0071e3;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.static-hero-kicker {
|
||||
font-size: 12px;
|
||||
color: #aeaeb2;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
.static-section {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: clamp(32px, 6vw, 64px) clamp(16px, 4vw, 48px);
|
||||
border-top: 1px solid #e5e5e5;
|
||||
}
|
||||
.static-section-title {
|
||||
font-size: 12px;
|
||||
color: #86868b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
margin: 0 0 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Photo grid */
|
||||
.static-photo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.static-photo-grid img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.static-footer {
|
||||
background: #f5f5f7;
|
||||
border-top: 2px solid #1d1d1f;
|
||||
padding: 24px clamp(16px, 4vw, 48px);
|
||||
}
|
||||
.static-footer-inner {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.static-footer-brand {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1d1d1f;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
.static-footer-copy {
|
||||
font-size: 12px;
|
||||
color: #86868b;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
.static-footer-contact {
|
||||
font-size: 13px;
|
||||
color: #6e6e73;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.static-footer-contact a {
|
||||
color: #0071e3;
|
||||
}
|
||||
.static-footer-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.static-footer-links a {
|
||||
font-size: 13px;
|
||||
color: #6e6e73;
|
||||
text-decoration: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.static-footer-links a:hover,
|
||||
.static-footer-links a:focus {
|
||||
background: #e5e5e5;
|
||||
outline: 2px solid #0071e3;
|
||||
}
|
||||
|
||||
/* Focus styles — WCAG 2.2 compliant */
|
||||
:focus-visible {
|
||||
outline: 3px solid #0071e3;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* High contrast mode support */
|
||||
@media (forced-colors: active) {
|
||||
.static-btn--primary {
|
||||
border: 2px solid ButtonText;
|
||||
}
|
||||
.static-header {
|
||||
border-bottom-color: ButtonText;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion — no animations at all */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* { animation: none !important; transition: none !important; }
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.static-header, .static-footer, .static-hero-actions { display: none; }
|
||||
.static-section { border: none; page-break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
+2141
-5106
File diff suppressed because it is too large
Load Diff
+2
-1882
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user