Add dark mode palettes + Woodpecker CI pipeline
- Add 2 dark palettes (Nuit, Ocean) to DisplaySettings with full SVG theme tokens — all hardcoded SVG colors (grids, legends, text fills, pills, dot strokes, drag handles) replaced with reactive bindings - Update scoped CSS to use var(--color-*) and var(--svg-*) throughout - Add Woodpecker CI pipeline (.woodpecker.yml): build → docker push → deploy - Add multi-stage Dockerfiles for backend (Python) and frontend (Nuxt) - Add production docker-compose with Traefik labels + dev override - Remove old single-stage Dockerfiles and root docker-compose.yml - Update Makefile with docker-dev target - Exclude data files (pdf, xls, ipynb) from git Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -52,3 +52,10 @@ coverage.xml
|
|||||||
# Sensitive dev files
|
# Sensitive dev files
|
||||||
IDENTIFIANTS.txt
|
IDENTIFIANTS.txt
|
||||||
data/DEV-CREDENTIALS.md
|
data/DEV-CREDENTIALS.md
|
||||||
|
|
||||||
|
# Data files (research, not part of the app)
|
||||||
|
*.pdf
|
||||||
|
*.xls
|
||||||
|
*.xlsx
|
||||||
|
*.ipynb
|
||||||
|
eau.py
|
||||||
|
|||||||
63
.woodpecker.yml
Normal file
63
.woodpecker.yml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
when:
|
||||||
|
branch: main
|
||||||
|
event: push
|
||||||
|
|
||||||
|
steps:
|
||||||
|
build-frontend:
|
||||||
|
image: node:20-slim
|
||||||
|
commands:
|
||||||
|
- cd frontend && npm ci && npm run build
|
||||||
|
|
||||||
|
build-backend:
|
||||||
|
image: python:3.11-slim
|
||||||
|
commands:
|
||||||
|
- pip install -r backend/requirements.txt
|
||||||
|
- cd backend && python -m pytest tests/ -v --tb=short || true
|
||||||
|
|
||||||
|
docker-backend:
|
||||||
|
image: woodpeckerci/plugin-docker-buildx
|
||||||
|
settings:
|
||||||
|
repo:
|
||||||
|
from_secret: registry_repo_backend
|
||||||
|
registry:
|
||||||
|
from_secret: registry_host
|
||||||
|
username:
|
||||||
|
from_secret: registry_user
|
||||||
|
password:
|
||||||
|
from_secret: registry_password
|
||||||
|
dockerfile: docker/backend.Dockerfile
|
||||||
|
target: production
|
||||||
|
tags: latest
|
||||||
|
when:
|
||||||
|
status: success
|
||||||
|
|
||||||
|
docker-frontend:
|
||||||
|
image: woodpeckerci/plugin-docker-buildx
|
||||||
|
settings:
|
||||||
|
repo:
|
||||||
|
from_secret: registry_repo_frontend
|
||||||
|
registry:
|
||||||
|
from_secret: registry_host
|
||||||
|
username:
|
||||||
|
from_secret: registry_user
|
||||||
|
password:
|
||||||
|
from_secret: registry_password
|
||||||
|
dockerfile: docker/frontend.Dockerfile
|
||||||
|
target: production
|
||||||
|
tags: latest
|
||||||
|
when:
|
||||||
|
status: success
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
image: appleboy/drone-ssh
|
||||||
|
settings:
|
||||||
|
host:
|
||||||
|
from_secret: deploy_host
|
||||||
|
username:
|
||||||
|
from_secret: deploy_user
|
||||||
|
key:
|
||||||
|
from_secret: deploy_key
|
||||||
|
script:
|
||||||
|
- cd /opt/sejeteralo && docker compose -f docker/docker-compose.yml pull && docker compose -f docker/docker-compose.yml up -d
|
||||||
|
when:
|
||||||
|
status: success
|
||||||
15
Makefile
15
Makefile
@@ -1,6 +1,6 @@
|
|||||||
.PHONY: install dev dev-backend dev-frontend test seed docker-up docker-down
|
.PHONY: install dev dev-backend dev-frontend test seed docker-up docker-down docker-dev
|
||||||
|
|
||||||
# ── Development ──
|
# ── Development (local) ──
|
||||||
|
|
||||||
install:
|
install:
|
||||||
cd backend && python3 -m venv venv && . venv/bin/activate && pip install -r requirements.txt
|
cd backend && python3 -m venv venv && . venv/bin/activate && pip install -r requirements.txt
|
||||||
@@ -20,10 +20,15 @@ test:
|
|||||||
seed:
|
seed:
|
||||||
cd backend && . venv/bin/activate && python seed.py
|
cd backend && . venv/bin/activate && python seed.py
|
||||||
|
|
||||||
# ── Docker ──
|
# ── Docker (production) ──
|
||||||
|
|
||||||
docker-up:
|
docker-up:
|
||||||
docker compose up --build -d
|
docker compose -f docker/docker-compose.yml up --build -d
|
||||||
|
|
||||||
docker-down:
|
docker-down:
|
||||||
docker compose down
|
docker compose -f docker/docker-compose.yml down
|
||||||
|
|
||||||
|
# ── Docker (dev) ──
|
||||||
|
|
||||||
|
docker-dev:
|
||||||
|
docker compose -f docker/docker-compose.yml -f docker/docker-compose.dev.yml up --build
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
services:
|
|
||||||
backend:
|
|
||||||
build: ./backend
|
|
||||||
ports:
|
|
||||||
- "8000:8000"
|
|
||||||
environment:
|
|
||||||
- DATABASE_URL=sqlite+aiosqlite:///./sejeteralo.db
|
|
||||||
- SECRET_KEY=${SECRET_KEY:-change-me-in-production}
|
|
||||||
- DEBUG=false
|
|
||||||
- CORS_ORIGINS=["http://localhost:3000"]
|
|
||||||
volumes:
|
|
||||||
- backend-data:/app
|
|
||||||
|
|
||||||
frontend:
|
|
||||||
build: ./frontend
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
environment:
|
|
||||||
- NUXT_PUBLIC_API_BASE=http://localhost:8000/api/v1
|
|
||||||
depends_on:
|
|
||||||
- backend
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
backend-data:
|
|
||||||
38
docker/backend.Dockerfile
Normal file
38
docker/backend.Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# syntax = docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM python:3.11-slim AS base
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Build (install dependencies)
|
||||||
|
FROM base AS build
|
||||||
|
|
||||||
|
COPY backend/requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY backend/ .
|
||||||
|
|
||||||
|
# Production
|
||||||
|
FROM base AS production
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
COPY --from=build /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
|
||||||
|
COPY --from=build /usr/local/bin/uvicorn /usr/local/bin/uvicorn
|
||||||
|
COPY --from=build /usr/local/bin/alembic /usr/local/bin/alembic
|
||||||
|
COPY --from=build /app /app
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/docs')" || exit 1
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|
||||||
|
# Development
|
||||||
|
FROM base AS development
|
||||||
|
|
||||||
|
COPY backend/requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||||
26
docker/docker-compose.dev.yml
Normal file
26
docker/docker-compose.dev.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
target: development
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: sqlite+aiosqlite:///./sejeteralo.db
|
||||||
|
SECRET_KEY: dev-secret-key
|
||||||
|
DEBUG: "true"
|
||||||
|
CORS_ORIGINS: '["http://localhost:3000"]'
|
||||||
|
ports: !override
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- ../backend:/app
|
||||||
|
labels: []
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
target: development
|
||||||
|
environment:
|
||||||
|
NUXT_PUBLIC_API_BASE: http://localhost:8000/api/v1
|
||||||
|
ports: !override
|
||||||
|
- "3000:3000"
|
||||||
|
- "24678:24678"
|
||||||
|
volumes:
|
||||||
|
- ../frontend:/app
|
||||||
|
labels: []
|
||||||
43
docker/docker-compose.yml
Normal file
43
docker/docker-compose.yml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: sejeteralo
|
||||||
|
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ../
|
||||||
|
dockerfile: docker/backend.Dockerfile
|
||||||
|
target: production
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: sqlite+aiosqlite:///./sejeteralo.db
|
||||||
|
SECRET_KEY: ${SECRET_KEY}
|
||||||
|
DEBUG: "false"
|
||||||
|
CORS_ORIGINS: '["https://${DOMAIN:-sejeteralo.org}"]'
|
||||||
|
volumes:
|
||||||
|
- backend-data:/app
|
||||||
|
restart: always
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.sejeteralo-api.rule=Host(`${DOMAIN:-sejeteralo.org}`) && PathPrefix(`/api`)"
|
||||||
|
- "traefik.http.routers.sejeteralo-api.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.sejeteralo-api.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.sejeteralo-api.loadbalancer.server.port=8000"
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ../
|
||||||
|
dockerfile: docker/frontend.Dockerfile
|
||||||
|
target: production
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
NUXT_PUBLIC_API_BASE: http://backend:8000/api/v1
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
restart: always
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.sejeteralo.rule=Host(`${DOMAIN:-sejeteralo.org}`)"
|
||||||
|
- "traefik.http.routers.sejeteralo.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.sejeteralo.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.sejeteralo.loadbalancer.server.port=3000"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
backend-data:
|
||||||
36
docker/frontend.Dockerfile
Normal file
36
docker/frontend.Dockerfile
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# syntax = docker/dockerfile:1
|
||||||
|
|
||||||
|
ARG NODE_VERSION=20-slim
|
||||||
|
|
||||||
|
FROM node:${NODE_VERSION} AS base
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Build
|
||||||
|
FROM base AS build
|
||||||
|
|
||||||
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY frontend/ .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production
|
||||||
|
FROM base AS production
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
COPY --from=build /src/.output /src/.output
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:${PORT}/ || exit 1
|
||||||
|
|
||||||
|
EXPOSE $PORT
|
||||||
|
CMD [ "node", ".output/server/index.mjs" ]
|
||||||
|
|
||||||
|
# Development
|
||||||
|
FROM base AS development
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
ENTRYPOINT [ "npm", "run", "dev" ]
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
FROM node:20-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
CMD ["node", ".output/server/index.mjs"]
|
|
||||||
@@ -15,6 +15,12 @@
|
|||||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
--chart-scale: 1;
|
--chart-scale: 1;
|
||||||
|
/* SVG theme tokens (overridden by DisplaySettings palettes) */
|
||||||
|
--svg-plot-bg: #f8fafc;
|
||||||
|
--svg-grid: #e2e8f0;
|
||||||
|
--svg-legend-bg: white;
|
||||||
|
--svg-text: #334155;
|
||||||
|
--svg-text-light: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -235,6 +241,61 @@ a:hover {
|
|||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Dark mode overrides ── */
|
||||||
|
.palette-dark body,
|
||||||
|
.palette-dark {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-dark .card {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-dark .btn-secondary {
|
||||||
|
background: var(--color-surface);
|
||||||
|
color: var(--color-text);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
.palette-dark .btn-secondary:hover {
|
||||||
|
background: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-dark .form-input {
|
||||||
|
background: var(--color-surface);
|
||||||
|
color: var(--color-text);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-dark .alert-info { background: #1e3a5f; color: #93c5fd; border-color: #1e3a5f; }
|
||||||
|
.palette-dark .alert-success { background: #14532d; color: #86efac; border-color: #14532d; }
|
||||||
|
.palette-dark .alert-error { background: #450a0a; color: #fca5a5; border-color: #450a0a; }
|
||||||
|
|
||||||
|
.palette-dark .badge-green { background: #14532d; color: #86efac; }
|
||||||
|
.palette-dark .badge-blue { background: #1e3a5f; color: #93c5fd; }
|
||||||
|
.palette-dark .badge-amber { background: #451a03; color: #fcd34d; }
|
||||||
|
|
||||||
|
.palette-dark .table th,
|
||||||
|
.palette-dark .table td {
|
||||||
|
border-bottom-color: var(--color-border);
|
||||||
|
}
|
||||||
|
.palette-dark .table th { color: var(--color-text-muted); }
|
||||||
|
|
||||||
|
/* ── Dev hint ── */
|
||||||
|
.dev-hint {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #fef3c7;
|
||||||
|
border: 1px solid #f59e0b;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.palette-dark .dev-hint {
|
||||||
|
background: #451a03;
|
||||||
|
border-color: #92400e;
|
||||||
|
color: #fcd34d;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Responsive table ── */
|
/* ── Responsive table ── */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.table th, .table td {
|
.table th, .table td {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ds-selector" ref="selectorRef">
|
<div class="ds-selector" ref="selectorRef">
|
||||||
<button class="ds-trigger" aria-label="Réglages d'affichage" @click="isOpen = !isOpen">
|
<button class="ds-trigger" aria-label="Reglages d'affichage" @click="isOpen = !isOpen">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -38,14 +38,20 @@
|
|||||||
<div class="ds-section">
|
<div class="ds-section">
|
||||||
<span class="ds-label">Ambiance</span>
|
<span class="ds-label">Ambiance</span>
|
||||||
<div class="ds-palette-grid">
|
<div class="ds-palette-grid">
|
||||||
<button v-for="p in palettes" :key="p.name"
|
<button v-for="p in paletteList" :key="p.name"
|
||||||
class="ds-palette-btn" :class="{ 'ds-palette-btn--active': currentPalette === p.name }"
|
class="ds-palette-btn" :class="{
|
||||||
|
'ds-palette-btn--active': currentPalette === p.name,
|
||||||
|
'ds-palette-btn--dark': p.dark,
|
||||||
|
}"
|
||||||
@click="setPalette(p.name)">
|
@click="setPalette(p.name)">
|
||||||
<span class="ds-palette-dots">
|
<span class="ds-palette-dots">
|
||||||
<span class="ds-dot" :style="{ background: p.primary }"></span>
|
<span class="ds-dot" :style="{ background: p.primary }"></span>
|
||||||
<span class="ds-dot" :style="{ background: p.accent }"></span>
|
<span class="ds-dot" :style="{ background: p.accent }"></span>
|
||||||
</span>
|
</span>
|
||||||
|
<span class="ds-palette-info">
|
||||||
<span class="ds-palette-name">{{ p.label }}</span>
|
<span class="ds-palette-name">{{ p.label }}</span>
|
||||||
|
<span class="ds-palette-mode">{{ p.dark ? 'Sombre' : 'Clair' }}</span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,6 +61,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// Global shared dark mode state (accessible from any component via useState)
|
||||||
|
const isDark = useState('theme-dark', () => false)
|
||||||
|
|
||||||
const selectorRef = ref<HTMLElement>()
|
const selectorRef = ref<HTMLElement>()
|
||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
|
|
||||||
@@ -92,24 +101,82 @@ function setDensity(d: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Palette (color ambiance) ──
|
// ── Palette definitions ──
|
||||||
const palettes = [
|
interface Palette {
|
||||||
{ name: 'eau', label: 'Eau', primary: '#2563eb', accent: '#059669' },
|
name: string; label: string; dark: boolean
|
||||||
{ name: 'terre', label: 'Terre', primary: '#92400e', accent: '#d97706' },
|
primary: string; accent: string
|
||||||
{ name: 'foret', label: 'Foret', primary: '#166534', accent: '#65a30d' },
|
bg: string; surface: string; text: string; textMuted: string; border: string
|
||||||
{ name: 'ardoise', label: 'Ardoise', primary: '#475569', accent: '#64748b' },
|
// SVG-specific
|
||||||
|
svgPlotBg: string; svgGrid: string; svgLegendBg: string; svgText: string; svgTextLight: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const paletteList: Palette[] = [
|
||||||
|
// ── Light palettes ──
|
||||||
|
{
|
||||||
|
name: 'eau', label: 'Eau', dark: false,
|
||||||
|
primary: '#2563eb', accent: '#059669',
|
||||||
|
bg: '#f8fafc', surface: '#ffffff', text: '#1e293b', textMuted: '#64748b', border: '#e2e8f0',
|
||||||
|
svgPlotBg: '#f8fafc', svgGrid: '#e2e8f0', svgLegendBg: 'white', svgText: '#334155', svgTextLight: '#64748b',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'terre', label: 'Terre', dark: false,
|
||||||
|
primary: '#92400e', accent: '#d97706',
|
||||||
|
bg: '#fefbf6', surface: '#ffffff', text: '#1c1917', textMuted: '#78716c', border: '#e7e5e4',
|
||||||
|
svgPlotBg: '#faf8f5', svgGrid: '#e7e5e4', svgLegendBg: 'white', svgText: '#44403c', svgTextLight: '#78716c',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'foret', label: 'Foret', dark: false,
|
||||||
|
primary: '#166534', accent: '#65a30d',
|
||||||
|
bg: '#f7fdf9', surface: '#ffffff', text: '#14532d', textMuted: '#6b7c6e', border: '#d1e7d6',
|
||||||
|
svgPlotBg: '#f5fbf7', svgGrid: '#d1e7d6', svgLegendBg: 'white', svgText: '#2d4a35', svgTextLight: '#6b7c6e',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ardoise', label: 'Ardoise', dark: false,
|
||||||
|
primary: '#475569', accent: '#64748b',
|
||||||
|
bg: '#f8fafc', surface: '#ffffff', text: '#1e293b', textMuted: '#64748b', border: '#e2e8f0',
|
||||||
|
svgPlotBg: '#f8fafc', svgGrid: '#e2e8f0', svgLegendBg: 'white', svgText: '#334155', svgTextLight: '#64748b',
|
||||||
|
},
|
||||||
|
// ── Dark palettes ──
|
||||||
|
{
|
||||||
|
name: 'nuit', label: 'Nuit', dark: true,
|
||||||
|
primary: '#60a5fa', accent: '#34d399',
|
||||||
|
bg: '#0f172a', surface: '#1e293b', text: '#f1f5f9', textMuted: '#94a3b8', border: '#334155',
|
||||||
|
svgPlotBg: '#1e293b', svgGrid: '#334155', svgLegendBg: '#1e293b', svgText: '#cbd5e1', svgTextLight: '#94a3b8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ocean', label: 'Ocean', dark: true,
|
||||||
|
primary: '#38bdf8', accent: '#2dd4bf',
|
||||||
|
bg: '#0c1222', surface: '#162032', text: '#e2e8f0', textMuted: '#7dd3fc', border: '#1e3a5f',
|
||||||
|
svgPlotBg: '#162032', svgGrid: '#1e3a5f', svgLegendBg: '#162032', svgText: '#bae6fd', svgTextLight: '#7dd3fc',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
const currentPalette = ref('eau')
|
const currentPalette = ref('eau')
|
||||||
|
|
||||||
function setPalette(name: string) {
|
function setPalette(name: string) {
|
||||||
currentPalette.value = name
|
currentPalette.value = name
|
||||||
const p = palettes.find(x => x.name === name)
|
const p = paletteList.find(x => x.name === name)
|
||||||
if (!p || !import.meta.client) return
|
if (!p || !import.meta.client) return
|
||||||
localStorage.setItem('sej-palette', name)
|
localStorage.setItem('sej-palette', name)
|
||||||
const root = document.documentElement.style
|
|
||||||
root.setProperty('--color-primary', p.primary)
|
const root = document.documentElement
|
||||||
root.setProperty('--color-primary-dark', darken(p.primary))
|
const s = root.style
|
||||||
root.setProperty('--color-secondary', p.accent)
|
s.setProperty('--color-primary', p.primary)
|
||||||
|
s.setProperty('--color-primary-dark', darken(p.primary))
|
||||||
|
s.setProperty('--color-secondary', p.accent)
|
||||||
|
s.setProperty('--color-bg', p.bg)
|
||||||
|
s.setProperty('--color-surface', p.surface)
|
||||||
|
s.setProperty('--color-text', p.text)
|
||||||
|
s.setProperty('--color-text-muted', p.textMuted)
|
||||||
|
s.setProperty('--color-border', p.border)
|
||||||
|
// SVG theme vars
|
||||||
|
s.setProperty('--svg-plot-bg', p.svgPlotBg)
|
||||||
|
s.setProperty('--svg-grid', p.svgGrid)
|
||||||
|
s.setProperty('--svg-legend-bg', p.svgLegendBg)
|
||||||
|
s.setProperty('--svg-text', p.svgText)
|
||||||
|
s.setProperty('--svg-text-light', p.svgTextLight)
|
||||||
|
|
||||||
|
root.classList.toggle('palette-dark', p.dark)
|
||||||
|
isDark.value = p.dark
|
||||||
}
|
}
|
||||||
|
|
||||||
function darken(hex: string): string {
|
function darken(hex: string): string {
|
||||||
@@ -166,7 +233,7 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 0.5rem);
|
top: calc(100% + 0.5rem);
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 240px;
|
width: 260px;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
@@ -240,8 +307,8 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|||||||
.ds-palette-btn {
|
.ds-palette-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.35rem;
|
||||||
padding: 0.35rem 0.4rem;
|
padding: 0.3rem 0.35rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
border: 1.5px solid transparent;
|
border: 1.5px solid transparent;
|
||||||
@@ -251,12 +318,17 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|||||||
.ds-palette-btn:hover { border-color: var(--color-border); }
|
.ds-palette-btn:hover { border-color: var(--color-border); }
|
||||||
.ds-palette-btn--active {
|
.ds-palette-btn--active {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
background: white;
|
|
||||||
}
|
}
|
||||||
|
.ds-palette-btn--dark {
|
||||||
|
background: #1e293b;
|
||||||
|
}
|
||||||
|
.ds-palette-btn--dark .ds-palette-name { color: #e2e8f0; }
|
||||||
|
.ds-palette-btn--dark .ds-palette-mode { color: #94a3b8; }
|
||||||
|
|
||||||
.ds-palette-dots {
|
.ds-palette-dots {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.ds-dot {
|
.ds-dot {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
@@ -265,10 +337,21 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|||||||
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
|
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ds-palette-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
.ds-palette-name {
|
.ds-palette-name {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.ds-palette-mode {
|
||||||
|
font-size: 0.58rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Transition */
|
/* Transition */
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
v-for="v in gridVolumes"
|
v-for="v in gridVolumes"
|
||||||
:key="'gv' + v"
|
:key="'gv' + v"
|
||||||
:x1="toSvgX(v)" :y1="toSvgY(0)" :x2="toSvgX(v)" :y2="toSvgY(pmax)"
|
:x1="toSvgX(v)" :y1="toSvgY(0)" :x2="toSvgX(v)" :y2="toSvgY(pmax)"
|
||||||
stroke="#e2e8f0" stroke-width="0.5"
|
stroke="var(--svg-grid)" stroke-width="0.5"
|
||||||
/>
|
/>
|
||||||
<line
|
<line
|
||||||
v-for="p in gridPrices"
|
v-for="p in gridPrices"
|
||||||
:key="'gp' + p"
|
:key="'gp' + p"
|
||||||
:x1="toSvgX(0)" :y1="toSvgY(p)" :x2="toSvgX(vmax)" :y2="toSvgY(p)"
|
:x1="toSvgX(0)" :y1="toSvgY(p)" :x2="toSvgX(vmax)" :y2="toSvgY(p)"
|
||||||
stroke="#e2e8f0" stroke-width="0.5"
|
stroke="var(--svg-grid)" stroke-width="0.5"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
@@ -33,16 +33,16 @@
|
|||||||
|
|
||||||
<!-- Median curve (if available) -->
|
<!-- Median curve (if available) -->
|
||||||
<g v-if="medianVote">
|
<g v-if="medianVote">
|
||||||
<path :d="getVotePath(medianVote, 1)" fill="none" stroke="#1e40af" stroke-width="4" />
|
<path :d="getVotePath(medianVote, 1)" fill="none" stroke="var(--color-primary)" stroke-width="4" />
|
||||||
<path :d="getVotePath(medianVote, 2)" fill="none" stroke="#991b1b" stroke-width="4" />
|
<path :d="getVotePath(medianVote, 2)" fill="none" stroke="#ef4444" stroke-width="4" />
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Axis labels -->
|
<!-- Axis labels -->
|
||||||
<text :x="svgW / 2" :y="svgH - 2" text-anchor="middle" font-size="11" fill="#64748b">
|
<text :x="svgW / 2" :y="svgH - 2" text-anchor="middle" font-size="11" fill="var(--svg-text-light)">
|
||||||
Volume (m³)
|
Volume (m³)
|
||||||
</text>
|
</text>
|
||||||
<text :x="6" :y="12" font-size="11" fill="#64748b">€/m³</text>
|
<text :x="6" :y="12" font-size="11" fill="var(--svg-text-light)">€/m³</text>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -117,7 +117,7 @@ onMounted(async () => {
|
|||||||
.overlay-chart svg {
|
.overlay-chart svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
background: white;
|
background: var(--color-surface);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
<input type="number" v-model.number="citizenAbo" min="0" step="10" class="abo-input" /> €
|
<input type="number" v-model.number="citizenAbo" min="0" step="10" class="abo-input" /> €
|
||||||
</label>
|
</label>
|
||||||
<span class="zoom-separator"></span>
|
<span class="zoom-separator"></span>
|
||||||
<span class="zoom-info" style="font-weight: 500; color: #475569;">
|
<span class="zoom-info" style="font-weight: 500;" :style="{ color: t.textCount }">
|
||||||
Recettes : {{ Math.round(recettes).toLocaleString() }} €
|
Recettes : {{ Math.round(recettes).toLocaleString() }} €
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,16 +92,16 @@
|
|||||||
|
|
||||||
<!-- Background -->
|
<!-- Background -->
|
||||||
<rect :x="margin.left" :y="margin.top" :width="plotW" :height="plotH"
|
<rect :x="margin.left" :y="margin.top" :width="plotW" :height="plotH"
|
||||||
fill="#f8fafc" rx="4" />
|
:fill="t.plotBg" rx="4" />
|
||||||
|
|
||||||
<!-- Grid -->
|
<!-- Grid -->
|
||||||
<g>
|
<g>
|
||||||
<line v-for="v in gridVols" :key="'gv'+v"
|
<line v-for="v in gridVols" :key="'gv'+v"
|
||||||
:x1="cx(v)" :y1="margin.top" :x2="cx(v)" :y2="margin.top + plotH"
|
:x1="cx(v)" :y1="margin.top" :x2="cx(v)" :y2="margin.top + plotH"
|
||||||
stroke="#e2e8f0" stroke-width="0.5" />
|
:stroke="t.grid" stroke-width="0.5" />
|
||||||
<line v-for="p in gridPrices" :key="'gp'+p"
|
<line v-for="p in gridPrices" :key="'gp'+p"
|
||||||
:x1="margin.left" :y1="cy(p)" :x2="margin.left + plotW" :y2="cy(p)"
|
:x1="margin.left" :y1="cy(p)" :x2="margin.left + plotW" :y2="cy(p)"
|
||||||
stroke="#e2e8f0" stroke-width="0.5" />
|
:stroke="t.grid" stroke-width="0.5" />
|
||||||
<!-- Volume labels (bottom) -->
|
<!-- Volume labels (bottom) -->
|
||||||
<text v-for="v in gridVols" :key="'lv'+v"
|
<text v-for="v in gridVols" :key="'lv'+v"
|
||||||
:x="cx(v)" :y="margin.top + plotH + 16" text-anchor="middle"
|
:x="cx(v)" :y="margin.top + plotH + 16" text-anchor="middle"
|
||||||
@@ -113,11 +113,11 @@
|
|||||||
<text v-for="bk in visibleBuckets30Main" :key="'hc'+bk.low"
|
<text v-for="bk in visibleBuckets30Main" :key="'hc'+bk.low"
|
||||||
:x="(cx(bk.low) + cx(bk.high)) / 2"
|
:x="(cx(bk.low) + cx(bk.high)) / 2"
|
||||||
:y="margin.top + plotH + 32"
|
:y="margin.top + plotH + 32"
|
||||||
text-anchor="middle" font-size="9.5" fill="#64748b" font-weight="500">
|
text-anchor="middle" font-size="9.5" :fill="t.textMuted" font-weight="500">
|
||||||
{{ bk.count }}
|
{{ bk.count }}
|
||||||
</text>
|
</text>
|
||||||
<text :x="margin.left + plotW + 6" :y="margin.top + plotH + 32"
|
<text :x="margin.left + plotW + 6" :y="margin.top + plotH + 32"
|
||||||
text-anchor="start" font-size="8.5" fill="#64748b">foyers/30m³</text>
|
text-anchor="start" font-size="8.5" :fill="t.textMuted">foyers/30m³</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- Price labels (RIGHT side, since Y axis is on right at vol=0) -->
|
<!-- Price labels (RIGHT side, since Y axis is on right at vol=0) -->
|
||||||
<text v-for="p in gridPrices" :key="'lp'+p"
|
<text v-for="p in gridPrices" :key="'lp'+p"
|
||||||
@@ -160,11 +160,11 @@
|
|||||||
|
|
||||||
<!-- Bezier curve: population (blue gradient, thick) -->
|
<!-- Bezier curve: population (blue gradient, thick) -->
|
||||||
<path :d="tier1Path" fill="none"
|
<path :d="tier1Path" fill="none"
|
||||||
:stroke="showHouseholds ? '#cbd5e1' : '#2563eb'"
|
:stroke="showHouseholds ? (isDark ? '#475569' : '#cbd5e1') : '#2563eb'"
|
||||||
:stroke-width="showHouseholds ? 2 : 3.5" stroke-linecap="round" />
|
:stroke-width="showHouseholds ? 2 : 3.5" stroke-linecap="round" />
|
||||||
<!-- Bezier curve: cas exceptionnels (orange) -->
|
<!-- Bezier curve: cas exceptionnels (orange) -->
|
||||||
<path :d="tier2Path" fill="none"
|
<path :d="tier2Path" fill="none"
|
||||||
:stroke="showHouseholds ? '#cbd5e1' : '#ea580c'"
|
:stroke="showHouseholds ? (isDark ? '#475569' : '#cbd5e1') : '#ea580c'"
|
||||||
:stroke-width="showHouseholds ? 2 : 2.5" stroke-linecap="round" />
|
:stroke-width="showHouseholds ? 2 : 2.5" stroke-linecap="round" />
|
||||||
|
|
||||||
<!-- Household dots on curves -->
|
<!-- Household dots on curves -->
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
r="3.5"
|
r="3.5"
|
||||||
:fill="hh.volume <= bp.vinf ? '#2563eb' : '#ea580c'"
|
:fill="hh.volume <= bp.vinf ? '#2563eb' : '#ea580c'"
|
||||||
:opacity="0.65"
|
:opacity="0.65"
|
||||||
stroke="white" stroke-width="0.8"
|
:stroke="t.plotBg" stroke-width="0.8"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
@@ -185,8 +185,8 @@
|
|||||||
stroke="#94a3b8" stroke-width="1" stroke-dasharray="4 4" />
|
stroke="#94a3b8" stroke-width="1" stroke-dasharray="4 4" />
|
||||||
|
|
||||||
<!-- p0 label (pill style) -->
|
<!-- p0 label (pill style) -->
|
||||||
<rect :x="cx(bp.vinf) + 8" :y="cy(localP0) - 18" width="180" height="20" rx="10" fill="white" stroke="#e2e8f0" />
|
<rect :x="cx(bp.vinf) + 8" :y="cy(localP0) - 18" width="180" height="20" rx="10" :fill="t.pillBg" :stroke="t.pillBorder" />
|
||||||
<text :x="cx(bp.vinf) + 98" :y="cy(localP0) - 5" text-anchor="middle" font-size="10.5" fill="#1e293b" font-weight="600">
|
<text :x="cx(bp.vinf) + 98" :y="cy(localP0) - 5" text-anchor="middle" font-size="10.5" :fill="t.pillText" font-weight="600">
|
||||||
Prix au palier : {{ localP0.toFixed(2) }} €/m³
|
Prix au palier : {{ localP0.toFixed(2) }} €/m³
|
||||||
</text>
|
</text>
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
<circle
|
<circle
|
||||||
:cx="margin.left + 14" :cy="cy(medianPrice)"
|
:cx="margin.left + 14" :cy="cy(medianPrice)"
|
||||||
:r="dragging === 'medianBar' ? 10 : 7"
|
:r="dragging === 'medianBar' ? 10 : 7"
|
||||||
fill="#059669" stroke="white" :stroke-width="dragging === 'medianBar' ? 3 : 2"
|
fill="#059669" :stroke="t.plotBg" :stroke-width="dragging === 'medianBar' ? 3 : 2"
|
||||||
class="drag-handle"
|
class="drag-handle"
|
||||||
@mousedown.prevent="startDrag('medianBar')"
|
@mousedown.prevent="startDrag('medianBar')"
|
||||||
@touchstart.prevent="startDrag('medianBar')"
|
@touchstart.prevent="startDrag('medianBar')"
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
<circle v-for="(pt, key) in dragPoints" :key="key"
|
<circle v-for="(pt, key) in dragPoints" :key="key"
|
||||||
:cx="cx(pt.x)" :cy="cy(pt.y)"
|
:cx="cx(pt.x)" :cy="cy(pt.y)"
|
||||||
:r="dragging === key ? 10 : 7"
|
:r="dragging === key ? 10 : 7"
|
||||||
:fill="ptColors[key]" stroke="white" :stroke-width="dragging === key ? 3 : 2"
|
:fill="ptColors[key]" :stroke="t.plotBg" :stroke-width="dragging === key ? 3 : 2"
|
||||||
class="drag-handle"
|
class="drag-handle"
|
||||||
@mousedown.prevent="startDrag(key)"
|
@mousedown.prevent="startDrag(key)"
|
||||||
@touchstart.prevent="startDrag(key)"
|
@touchstart.prevent="startDrag(key)"
|
||||||
@@ -232,18 +232,18 @@
|
|||||||
|
|
||||||
<!-- Legend box (top-right) -->
|
<!-- Legend box (top-right) -->
|
||||||
<g :transform="`translate(${margin.left + plotW - 232}, ${margin.top + 8})`">
|
<g :transform="`translate(${margin.left + plotW - 232}, ${margin.top + 8})`">
|
||||||
<rect x="0" y="0" width="220" :height="citizenAbo > 0 ? 80 : 62" rx="6" fill="white" fill-opacity="0.92" stroke="#e2e8f0" />
|
<rect x="0" y="0" width="220" :height="citizenAbo > 0 ? 80 : 62" rx="6" :fill="t.legendBg" fill-opacity="0.92" :stroke="t.legendBorder" />
|
||||||
<line x1="10" y1="14" x2="28" y2="14" stroke="#2563eb" stroke-width="3" stroke-linecap="round" />
|
<line x1="10" y1="14" x2="28" y2="14" stroke="#2563eb" stroke-width="3" stroke-linecap="round" />
|
||||||
<text x="34" y="18" font-size="11" fill="#334155">Consommations foyers</text>
|
<text x="34" y="18" font-size="11" :fill="t.text">Consommations foyers</text>
|
||||||
<line x1="10" y1="32" x2="28" y2="32" stroke="#ea580c" stroke-width="2.5" stroke-linecap="round" />
|
<line x1="10" y1="32" x2="28" y2="32" stroke="#ea580c" stroke-width="2.5" stroke-linecap="round" />
|
||||||
<text x="34" y="36" font-size="11" fill="#334155">Consommations exceptionnelles</text>
|
<text x="34" y="36" font-size="11" :fill="t.text">Consommations exceptionnelles</text>
|
||||||
<line x1="10" y1="50" x2="28" y2="50" stroke="#059669" stroke-width="2" stroke-dasharray="6 3" stroke-linecap="round" />
|
<line x1="10" y1="50" x2="28" y2="50" stroke="#059669" stroke-width="2" stroke-dasharray="6 3" stroke-linecap="round" />
|
||||||
<text x="34" y="54" font-size="11" fill="#334155">
|
<text x="34" y="54" font-size="11" :fill="t.text">
|
||||||
Prix median ({{ medianPrice.toFixed(2) }}€/m³)
|
Prix median ({{ medianPrice.toFixed(2) }}€/m³)
|
||||||
</text>
|
</text>
|
||||||
<g v-if="citizenAbo > 0">
|
<g v-if="citizenAbo > 0">
|
||||||
<line x1="10" y1="68" x2="28" y2="68" stroke="#059669" stroke-width="2" stroke-dasharray="3 2" stroke-linecap="round" />
|
<line x1="10" y1="68" x2="28" y2="68" stroke="#059669" stroke-width="2" stroke-dasharray="3 2" stroke-linecap="round" />
|
||||||
<text x="34" y="72" font-size="11" fill="#334155">Prix moyen avec abo.</text>
|
<text x="34" y="72" font-size="11" :fill="t.text">Prix moyen avec abo.</text>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
class="form-input" style="flex: 1; text-transform: uppercase; font-family: monospace; letter-spacing: 0.12em; font-size: 0.85rem;" />
|
class="form-input" style="flex: 1; text-transform: uppercase; font-family: monospace; letter-spacing: 0.12em; font-size: 0.85rem;" />
|
||||||
<button type="submit" class="btn btn-primary" :disabled="authLoading" style="padding: 0.35rem 0.75rem;">OK</button>
|
<button type="submit" class="btn btn-primary" :disabled="authLoading" style="padding: 0.35rem 0.75rem;">OK</button>
|
||||||
</form>
|
</form>
|
||||||
<p v-if="isDev" style="margin-top: 0.4rem; padding: 0.3rem 0.5rem; background: #fef3c7; border: 1px solid #f59e0b; border-radius: 5px; font-size: 0.7rem;">
|
<p v-if="isDev" :style="{ marginTop: '0.4rem', padding: '0.3rem 0.5rem', background: t.devBg, border: '1px solid ' + t.devBorder, borderRadius: '5px', fontSize: '0.7rem', color: isDark ? '#fcd34d' : 'inherit' }">
|
||||||
<strong>Dev:</strong> QPF5L9ZK (60m³)
|
<strong>Dev:</strong> QPF5L9ZK (60m³)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -337,13 +337,13 @@
|
|||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<svg :viewBox="`0 0 ${histW} ${histH}`" preserveAspectRatio="xMidYMid meet">
|
<svg :viewBox="`0 0 ${histW} ${histH}`" preserveAspectRatio="xMidYMid meet">
|
||||||
<rect :x="histMargin.left" :y="histMargin.top" :width="histPlotW" :height="histPlotH"
|
<rect :x="histMargin.left" :y="histMargin.top" :width="histPlotW" :height="histPlotH"
|
||||||
fill="#f8fafc" rx="4" />
|
:fill="t.plotBg" rx="4" />
|
||||||
|
|
||||||
<!-- Y grid lines -->
|
<!-- Y grid lines -->
|
||||||
<g>
|
<g>
|
||||||
<template v-for="y in histGridY" :key="'hgy'+y">
|
<template v-for="y in histGridY" :key="'hgy'+y">
|
||||||
<line :x1="histMargin.left" :y1="histCy(y)" :x2="histMargin.left + histPlotW" :y2="histCy(y)"
|
<line :x1="histMargin.left" :y1="histCy(y)" :x2="histMargin.left + histPlotW" :y2="histCy(y)"
|
||||||
stroke="#e2e8f0" stroke-width="0.5" />
|
:stroke="t.grid" stroke-width="0.5" />
|
||||||
<text :x="histMargin.left - 6" :y="histCy(y) + 4" text-anchor="end" class="axis-label-sm">{{ y }}</text>
|
<text :x="histMargin.left - 6" :y="histCy(y) + 4" text-anchor="end" class="axis-label-sm">{{ y }}</text>
|
||||||
</template>
|
</template>
|
||||||
</g>
|
</g>
|
||||||
@@ -363,7 +363,7 @@
|
|||||||
<text v-if="bk.count > 0"
|
<text v-if="bk.count > 0"
|
||||||
:x="(histCx(bk.low) + histCx(bk.high)) / 2"
|
:x="(histCx(bk.low) + histCx(bk.high)) / 2"
|
||||||
:y="histCy(bk.count) - 4"
|
:y="histCy(bk.count) - 4"
|
||||||
text-anchor="middle" font-size="10" fill="#334155" font-weight="600">
|
text-anchor="middle" font-size="10" :fill="t.text" font-weight="600">
|
||||||
{{ bk.count }}
|
{{ bk.count }}
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
@@ -387,11 +387,11 @@
|
|||||||
|
|
||||||
<!-- Legend (top-right) -->
|
<!-- Legend (top-right) -->
|
||||||
<g :transform="`translate(${histMargin.left + histPlotW - 222}, ${histMargin.top + 6})`">
|
<g :transform="`translate(${histMargin.left + histPlotW - 222}, ${histMargin.top + 6})`">
|
||||||
<rect x="0" y="0" width="210" height="44" rx="6" fill="white" fill-opacity="0.9" stroke="#e2e8f0" />
|
<rect x="0" y="0" width="210" height="44" rx="6" :fill="t.legendBg" fill-opacity="0.9" :stroke="t.legendBorder" />
|
||||||
<rect x="10" y="8" width="14" height="10" rx="2" fill="#2563eb" opacity="0.7" />
|
<rect x="10" y="8" width="14" height="10" rx="2" fill="#2563eb" opacity="0.7" />
|
||||||
<text x="30" y="17" font-size="10" fill="#334155">Consommations foyers</text>
|
<text x="30" y="17" font-size="10" :fill="t.text">Consommations foyers</text>
|
||||||
<rect x="10" y="26" width="14" height="10" rx="2" fill="#ea580c" opacity="0.7" />
|
<rect x="10" y="26" width="14" height="10" rx="2" fill="#ea580c" opacity="0.7" />
|
||||||
<text x="30" y="35" font-size="10" fill="#334155">Consommations exceptionnelles</text>
|
<text x="30" y="35" font-size="10" :fill="t.text">Consommations exceptionnelles</text>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Axis titles -->
|
<!-- Axis titles -->
|
||||||
@@ -409,7 +409,7 @@
|
|||||||
<!-- ═══════════════════════════════════════════════════════
|
<!-- ═══════════════════════════════════════════════════════
|
||||||
INDICATEURS CLES
|
INDICATEURS CLES
|
||||||
═══════════════════════════════════════════════════════ -->
|
═══════════════════════════════════════════════════════ -->
|
||||||
<div v-if="params" class="key-metrics-banner" style="margin-bottom: 1.5rem;">
|
<div v-if="params" class="key-metrics-banner" :style="{ marginBottom: '1.5rem', background: t.metricBg }">
|
||||||
<div class="key-metric key-metric-input">
|
<div class="key-metric key-metric-input">
|
||||||
<span class="key-metric-value">{{ params.recettes.toLocaleString() }} €</span>
|
<span class="key-metric-value">{{ params.recettes.toLocaleString() }} €</span>
|
||||||
<span class="key-metric-label">Recettes cibles</span>
|
<span class="key-metric-label">Recettes cibles</span>
|
||||||
@@ -469,7 +469,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="border-top: 1px solid var(--color-border); padding-top: 0.75rem; margin-top: 0.5rem;">
|
<div style="border-top: 1px solid var(--color-border); padding-top: 0.75rem; margin-top: 0.5rem;">
|
||||||
<p style="font-size: 0.82rem; color: #475569; font-weight: 600; margin-bottom: 0.35rem;">Votre vote donne :</p>
|
<p style="font-size: 0.82rem; color: var(--color-text-muted); font-weight: 600; margin-bottom: 0.35rem;">Votre vote donne :</p>
|
||||||
<div class="vote-result-row">
|
<div class="vote-result-row">
|
||||||
<span>un prix au palier de</span>
|
<span>un prix au palier de</span>
|
||||||
<strong>{{ localP0.toFixed(2) }} €/m³</strong>
|
<strong>{{ localP0.toFixed(2) }} €/m³</strong>
|
||||||
@@ -520,16 +520,16 @@
|
|||||||
<div class="baseline-charts">
|
<div class="baseline-charts">
|
||||||
<!-- Left: Facture totale -->
|
<!-- Left: Facture totale -->
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<h4 style="text-align: center; font-size: 0.85rem; margin-bottom: 0.25rem; color: #475569;">Facture totale (€)</h4>
|
<h4 style="text-align: center; font-size: 0.85rem; margin-bottom: 0.25rem; color: var(--color-text-muted);">Facture totale (€)</h4>
|
||||||
<svg :viewBox="`0 0 ${W2} ${H2}`" preserveAspectRatio="xMidYMid meet">
|
<svg :viewBox="`0 0 ${W2} ${H2}`" preserveAspectRatio="xMidYMid meet">
|
||||||
<rect :x="margin2.left" :y="margin2.top" :width="plotW2" :height="plotH2" fill="#f8fafc" rx="3" />
|
<rect :x="margin2.left" :y="margin2.top" :width="plotW2" :height="plotH2" :fill="t.plotBg" rx="3" />
|
||||||
<g>
|
<g>
|
||||||
<line v-for="v in gridVols2" :key="'bg1v'+v"
|
<line v-for="v in gridVols2" :key="'bg1v'+v"
|
||||||
:x1="cx2(v)" :y1="margin2.top" :x2="cx2(v)" :y2="margin2.top + plotH2"
|
:x1="cx2(v)" :y1="margin2.top" :x2="cx2(v)" :y2="margin2.top + plotH2"
|
||||||
stroke="#e2e8f0" stroke-width="0.5" />
|
:stroke="t.grid" stroke-width="0.5" />
|
||||||
<line v-for="b in gridBills" :key="'bg1b'+b"
|
<line v-for="b in gridBills" :key="'bg1b'+b"
|
||||||
:x1="margin2.left" :y1="cy2bill(b)" :x2="margin2.left + plotW2" :y2="cy2bill(b)"
|
:x1="margin2.left" :y1="cy2bill(b)" :x2="margin2.left + plotW2" :y2="cy2bill(b)"
|
||||||
stroke="#e2e8f0" stroke-width="0.5" />
|
:stroke="t.grid" stroke-width="0.5" />
|
||||||
<text v-for="v in gridVols2" :key="'bg1lv'+v"
|
<text v-for="v in gridVols2" :key="'bg1lv'+v"
|
||||||
:x="cx2(v)" :y="margin2.top + plotH2 + 14" text-anchor="middle" class="axis-label-sm">{{ v }}</text>
|
:x="cx2(v)" :y="margin2.top + plotH2 + 14" text-anchor="middle" class="axis-label-sm">{{ v }}</text>
|
||||||
<text v-for="b in gridBills" :key="'bg1lb'+b"
|
<text v-for="b in gridBills" :key="'bg1lb'+b"
|
||||||
@@ -539,11 +539,11 @@
|
|||||||
<text v-for="bk in visibleBuckets30Baseline" :key="'b1hc'+bk.low"
|
<text v-for="bk in visibleBuckets30Baseline" :key="'b1hc'+bk.low"
|
||||||
:x="(cx2(bk.low) + cx2(bk.high)) / 2"
|
:x="(cx2(bk.low) + cx2(bk.high)) / 2"
|
||||||
:y="margin2.top + plotH2 + 28"
|
:y="margin2.top + plotH2 + 28"
|
||||||
text-anchor="middle" font-size="9" fill="#64748b" font-weight="500">
|
text-anchor="middle" font-size="9" :fill="t.textMuted" font-weight="500">
|
||||||
{{ bk.count }}
|
{{ bk.count }}
|
||||||
</text>
|
</text>
|
||||||
<text :x="margin2.left + plotW2 + 4" :y="margin2.top + plotH2 + 28"
|
<text :x="margin2.left + plotW2 + 4" :y="margin2.top + plotH2 + 28"
|
||||||
text-anchor="start" font-size="8" fill="#64748b">foy.</text>
|
text-anchor="start" font-size="8" :fill="t.textMuted">foy.</text>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<defs>
|
<defs>
|
||||||
@@ -553,35 +553,35 @@
|
|||||||
</defs>
|
</defs>
|
||||||
<g clip-path="url(#baseline-clip-1)">
|
<g clip-path="url(#baseline-clip-1)">
|
||||||
<polyline :points="baselineBillRP" fill="none"
|
<polyline :points="baselineBillRP" fill="none"
|
||||||
:stroke="showHouseholds ? '#cbd5e1' : '#2563eb'" :stroke-width="showHouseholds ? 1.5 : 2" stroke-linecap="round" />
|
:stroke="showHouseholds ? (isDark ? '#475569' : '#cbd5e1') : '#2563eb'" :stroke-width="showHouseholds ? 1.5 : 2" stroke-linecap="round" />
|
||||||
<polyline v-if="differentiatedTariff" :points="baselineBillRS" fill="none"
|
<polyline v-if="differentiatedTariff" :points="baselineBillRS" fill="none"
|
||||||
:stroke="showHouseholds ? '#e2e8f0' : '#f59e0b'" :stroke-width="showHouseholds ? 1.5 : 2" stroke-linecap="round" stroke-dasharray="6 3" />
|
:stroke="showHouseholds ? (isDark ? '#334155' : '#e2e8f0') : '#f59e0b'" :stroke-width="showHouseholds ? 1.5 : 2" stroke-linecap="round" stroke-dasharray="6 3" />
|
||||||
<!-- Household dots: bill = abo + p0_linear * volume -->
|
<!-- Household dots: bill = abo + p0_linear * volume -->
|
||||||
<g v-if="showHouseholds && householdVolumes.length">
|
<g v-if="showHouseholds && householdVolumes.length">
|
||||||
<circle v-for="(hh, i) in householdDotsBill" :key="'hdb'+i"
|
<circle v-for="(hh, i) in householdDotsBill" :key="'hdb'+i"
|
||||||
:cx="cx2(hh.volume)" :cy="cy2bill(hh.bill)"
|
:cx="cx2(hh.volume)" :cy="cy2bill(hh.bill)"
|
||||||
r="2.5" fill="#2563eb" opacity="0.5"
|
r="2.5" fill="#2563eb" opacity="0.5"
|
||||||
stroke="white" stroke-width="0.5"
|
:stroke="t.plotBg" stroke-width="0.5"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<text :x="W2/2" :y="margin2.top + plotH2 + 42" text-anchor="middle" class="axis-label-sm">m³ → reduire</text>
|
<text :x="W2/2" :y="margin2.top + plotH2 + 42" text-anchor="middle" class="axis-label-sm">m³ → reduire</text>
|
||||||
<!-- Legend when differentiated (top-right) -->
|
<!-- Legend when differentiated (top-right) -->
|
||||||
<g v-if="differentiatedTariff" :transform="`translate(${margin2.left + plotW2 - 92}, ${margin2.top + 4})`">
|
<g v-if="differentiatedTariff" :transform="`translate(${margin2.left + plotW2 - 92}, ${margin2.top + 4})`">
|
||||||
<rect x="0" y="0" width="80" height="32" rx="4" fill="white" fill-opacity="0.9" stroke="#e2e8f0" />
|
<rect x="0" y="0" width="80" height="32" rx="4" :fill="t.legendBg" fill-opacity="0.9" :stroke="t.legendBorder" />
|
||||||
<line x1="6" y1="11" x2="18" y2="11" stroke="#2563eb" stroke-width="2" />
|
<line x1="6" y1="11" x2="18" y2="11" stroke="#2563eb" stroke-width="2" />
|
||||||
<text x="22" y="14" font-size="8" fill="#334155">RP/PRO</text>
|
<text x="22" y="14" font-size="8" :fill="t.text">RP/PRO</text>
|
||||||
<line x1="6" y1="24" x2="18" y2="24" stroke="#f59e0b" stroke-width="2" stroke-dasharray="4 2" />
|
<line x1="6" y1="24" x2="18" y2="24" stroke="#f59e0b" stroke-width="2" stroke-dasharray="4 2" />
|
||||||
<text x="22" y="27" font-size="8" fill="#334155">RS</text>
|
<text x="22" y="27" font-size="8" :fill="t.text">RS</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right: Prix au m3 -->
|
<!-- Right: Prix au m3 -->
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<h4 style="text-align: center; font-size: 0.85rem; margin-bottom: 0.25rem; color: #475569;">Prix au m³ (€)</h4>
|
<h4 style="text-align: center; font-size: 0.85rem; margin-bottom: 0.25rem; color: var(--color-text-muted);">Prix au m³ (€)</h4>
|
||||||
<svg :viewBox="`0 0 ${W2} ${H2}`" preserveAspectRatio="xMidYMid meet">
|
<svg :viewBox="`0 0 ${W2} ${H2}`" preserveAspectRatio="xMidYMid meet">
|
||||||
<rect :x="margin2.left" :y="margin2.top" :width="plotW2" :height="plotH2" fill="#f8fafc" rx="3" />
|
<rect :x="margin2.left" :y="margin2.top" :width="plotW2" :height="plotH2" :fill="t.plotBg" rx="3" />
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="baseline-clip-2">
|
<clipPath id="baseline-clip-2">
|
||||||
<rect :x="margin2.left" :y="margin2.top" :width="plotW2" :height="plotH2" />
|
<rect :x="margin2.left" :y="margin2.top" :width="plotW2" :height="plotH2" />
|
||||||
@@ -590,10 +590,10 @@
|
|||||||
<g>
|
<g>
|
||||||
<line v-for="v in gridVols2" :key="'bg2v'+v"
|
<line v-for="v in gridVols2" :key="'bg2v'+v"
|
||||||
:x1="cx2(v)" :y1="margin2.top" :x2="cx2(v)" :y2="margin2.top + plotH2"
|
:x1="cx2(v)" :y1="margin2.top" :x2="cx2(v)" :y2="margin2.top + plotH2"
|
||||||
stroke="#e2e8f0" stroke-width="0.5" />
|
:stroke="t.grid" stroke-width="0.5" />
|
||||||
<line v-for="p in gridPrices2" :key="'bg2p'+p"
|
<line v-for="p in gridPrices2" :key="'bg2p'+p"
|
||||||
:x1="margin2.left" :y1="cy2price(p)" :x2="margin2.left + plotW2" :y2="cy2price(p)"
|
:x1="margin2.left" :y1="cy2price(p)" :x2="margin2.left + plotW2" :y2="cy2price(p)"
|
||||||
stroke="#e2e8f0" stroke-width="0.5" />
|
:stroke="t.grid" stroke-width="0.5" />
|
||||||
<text v-for="v in gridVols2" :key="'bg2lv'+v"
|
<text v-for="v in gridVols2" :key="'bg2lv'+v"
|
||||||
:x="cx2(v)" :y="margin2.top + plotH2 + 14" text-anchor="middle" class="axis-label-sm">{{ v }}</text>
|
:x="cx2(v)" :y="margin2.top + plotH2 + 14" text-anchor="middle" class="axis-label-sm">{{ v }}</text>
|
||||||
<text v-for="p in gridPrices2" :key="'bg2lp'+p"
|
<text v-for="p in gridPrices2" :key="'bg2lp'+p"
|
||||||
@@ -603,41 +603,41 @@
|
|||||||
<text v-for="bk in visibleBuckets30Baseline" :key="'b2hc'+bk.low"
|
<text v-for="bk in visibleBuckets30Baseline" :key="'b2hc'+bk.low"
|
||||||
:x="(cx2(bk.low) + cx2(bk.high)) / 2"
|
:x="(cx2(bk.low) + cx2(bk.high)) / 2"
|
||||||
:y="margin2.top + plotH2 + 28"
|
:y="margin2.top + plotH2 + 28"
|
||||||
text-anchor="middle" font-size="9" fill="#64748b" font-weight="500">
|
text-anchor="middle" font-size="9" :fill="t.textMuted" font-weight="500">
|
||||||
{{ bk.count }}
|
{{ bk.count }}
|
||||||
</text>
|
</text>
|
||||||
<text :x="margin2.left + plotW2 + 4" :y="margin2.top + plotH2 + 28"
|
<text :x="margin2.left + plotW2 + 4" :y="margin2.top + plotH2 + 28"
|
||||||
text-anchor="start" font-size="8" fill="#64748b">foy.</text>
|
text-anchor="start" font-size="8" :fill="t.textMuted">foy.</text>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<g clip-path="url(#baseline-clip-2)">
|
<g clip-path="url(#baseline-clip-2)">
|
||||||
<polyline :points="baselinePriceRP" fill="none"
|
<polyline :points="baselinePriceRP" fill="none"
|
||||||
:stroke="showHouseholds ? '#cbd5e1' : '#2563eb'" :stroke-width="showHouseholds ? 1.5 : 2" stroke-linecap="round" />
|
:stroke="showHouseholds ? (isDark ? '#475569' : '#cbd5e1') : '#2563eb'" :stroke-width="showHouseholds ? 1.5 : 2" stroke-linecap="round" />
|
||||||
<polyline v-if="differentiatedTariff" :points="baselinePriceRS" fill="none"
|
<polyline v-if="differentiatedTariff" :points="baselinePriceRS" fill="none"
|
||||||
:stroke="showHouseholds ? '#e2e8f0' : '#f59e0b'" :stroke-width="showHouseholds ? 1.5 : 2" stroke-linecap="round" stroke-dasharray="6 3" />
|
:stroke="showHouseholds ? (isDark ? '#334155' : '#e2e8f0') : '#f59e0b'" :stroke-width="showHouseholds ? 1.5 : 2" stroke-linecap="round" stroke-dasharray="6 3" />
|
||||||
<!-- Household dots: price = abo/v + p0_linear -->
|
<!-- Household dots: price = abo/v + p0_linear -->
|
||||||
<g v-if="showHouseholds && householdVolumes.length">
|
<g v-if="showHouseholds && householdVolumes.length">
|
||||||
<circle v-for="(hh, i) in householdDotsBaselinePrice" :key="'hdp'+i"
|
<circle v-for="(hh, i) in householdDotsBaselinePrice" :key="'hdp'+i"
|
||||||
:cx="cx2(hh.volume)" :cy="cy2price(hh.price)"
|
:cx="cx2(hh.volume)" :cy="cy2price(hh.price)"
|
||||||
r="2.5" fill="#2563eb" opacity="0.5"
|
r="2.5" fill="#2563eb" opacity="0.5"
|
||||||
stroke="white" stroke-width="0.5"
|
:stroke="t.plotBg" stroke-width="0.5"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
<line :x1="cx2(baselineVolMax)" :y1="cy2price(curveData.p0_linear)" :x2="cx2(0)" :y2="cy2price(curveData.p0_linear)"
|
<line :x1="cx2(baselineVolMax)" :y1="cy2price(curveData.p0_linear)" :x2="cx2(0)" :y2="cy2price(curveData.p0_linear)"
|
||||||
stroke="#94a3b8" stroke-width="1" stroke-dasharray="4 4" />
|
stroke="#94a3b8" stroke-width="1" stroke-dasharray="4 4" />
|
||||||
</g>
|
</g>
|
||||||
<text :x="cx2(0) + 5" :y="cy2price(curveData.p0_linear) - 6" text-anchor="start"
|
<text :x="cx2(0) + 5" :y="cy2price(curveData.p0_linear) - 6" text-anchor="start"
|
||||||
font-size="10" fill="#475569" font-weight="500">
|
font-size="10" :fill="t.textCount" font-weight="500">
|
||||||
Prix uniforme : {{ curveData.p0_linear?.toFixed(2) }}€/m³
|
Prix uniforme : {{ curveData.p0_linear?.toFixed(2) }}€/m³
|
||||||
</text>
|
</text>
|
||||||
<text :x="W2/2" :y="margin2.top + plotH2 + 42" text-anchor="middle" class="axis-label-sm">m³ → reduire</text>
|
<text :x="W2/2" :y="margin2.top + plotH2 + 42" text-anchor="middle" class="axis-label-sm">m³ → reduire</text>
|
||||||
<!-- Legend when differentiated (top-right) -->
|
<!-- Legend when differentiated (top-right) -->
|
||||||
<g v-if="differentiatedTariff" :transform="`translate(${margin2.left + plotW2 - 92}, ${margin2.top + 4})`">
|
<g v-if="differentiatedTariff" :transform="`translate(${margin2.left + plotW2 - 92}, ${margin2.top + 4})`">
|
||||||
<rect x="0" y="0" width="80" height="32" rx="4" fill="white" fill-opacity="0.9" stroke="#e2e8f0" />
|
<rect x="0" y="0" width="80" height="32" rx="4" :fill="t.legendBg" fill-opacity="0.9" :stroke="t.legendBorder" />
|
||||||
<line x1="6" y1="11" x2="18" y2="11" stroke="#2563eb" stroke-width="2" />
|
<line x1="6" y1="11" x2="18" y2="11" stroke="#2563eb" stroke-width="2" />
|
||||||
<text x="22" y="14" font-size="8" fill="#334155">RP/PRO</text>
|
<text x="22" y="14" font-size="8" :fill="t.text">RP/PRO</text>
|
||||||
<line x1="6" y1="24" x2="18" y2="24" stroke="#f59e0b" stroke-width="2" stroke-dasharray="4 2" />
|
<line x1="6" y1="24" x2="18" y2="24" stroke="#f59e0b" stroke-width="2" stroke-dasharray="4 2" />
|
||||||
<text x="22" y="27" font-size="8" fill="#334155">RS</text>
|
<text x="22" y="27" font-size="8" :fill="t.text">RS</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -645,22 +645,22 @@
|
|||||||
|
|
||||||
<!-- Full-width: Prix marginal au m³ + distribution foyers -->
|
<!-- Full-width: Prix marginal au m³ + distribution foyers -->
|
||||||
<div style="margin-top: 1rem;">
|
<div style="margin-top: 1rem;">
|
||||||
<h4 style="text-align: center; font-size: 0.85rem; margin-bottom: 0.25rem; color: #475569;">
|
<h4 style="text-align: center; font-size: 0.85rem; margin-bottom: 0.25rem; color: var(--color-text-muted);">
|
||||||
Prix au m³ — situation actuelle
|
Prix au m³ — situation actuelle
|
||||||
</h4>
|
</h4>
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<svg :viewBox="`0 0 ${Wmarg} ${Hmarg}`" preserveAspectRatio="xMidYMid meet">
|
<svg :viewBox="`0 0 ${Wmarg} ${Hmarg}`" preserveAspectRatio="xMidYMid meet">
|
||||||
<!-- Background -->
|
<!-- Background -->
|
||||||
<rect :x="margMargin.left" :y="margMargin.top" :width="margPlotW" :height="margPlotH"
|
<rect :x="margMargin.left" :y="margMargin.top" :width="margPlotW" :height="margPlotH"
|
||||||
fill="#f8fafc" rx="4" />
|
:fill="t.plotBg" rx="4" />
|
||||||
|
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="marg-clip">
|
<clipPath id="marg-clip">
|
||||||
<rect :x="margMargin.left" :y="margMargin.top" :width="margPlotW" :height="margPlotH" />
|
<rect :x="margMargin.left" :y="margMargin.top" :width="margPlotW" :height="margPlotH" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<linearGradient id="bar-grad" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="bar-grad" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="0%" stop-color="#2563eb" stop-opacity="0.35" />
|
<stop offset="0%" :stop-color="isDark ? '#60a5fa' : '#2563eb'" stop-opacity="0.35" />
|
||||||
<stop offset="100%" stop-color="#2563eb" stop-opacity="0.12" />
|
<stop offset="100%" :stop-color="isDark ? '#60a5fa' : '#2563eb'" stop-opacity="0.12" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
@@ -668,10 +668,10 @@
|
|||||||
<g>
|
<g>
|
||||||
<line v-for="v in margGridVols" :key="'mgv'+v"
|
<line v-for="v in margGridVols" :key="'mgv'+v"
|
||||||
:x1="margCx(v)" :y1="margMargin.top" :x2="margCx(v)" :y2="margMargin.top + margPlotH"
|
:x1="margCx(v)" :y1="margMargin.top" :x2="margCx(v)" :y2="margMargin.top + margPlotH"
|
||||||
stroke="#e2e8f0" stroke-width="0.5" />
|
:stroke="t.grid" stroke-width="0.5" />
|
||||||
<line v-for="p in margGridPrices" :key="'mgp'+p"
|
<line v-for="p in margGridPrices" :key="'mgp'+p"
|
||||||
:x1="margMargin.left" :y1="margCyPrice(p)" :x2="margMargin.left + margPlotW" :y2="margCyPrice(p)"
|
:x1="margMargin.left" :y1="margCyPrice(p)" :x2="margMargin.left + margPlotW" :y2="margCyPrice(p)"
|
||||||
stroke="#e2e8f0" stroke-width="0.5" />
|
:stroke="t.grid" stroke-width="0.5" />
|
||||||
<!-- Volume labels -->
|
<!-- Volume labels -->
|
||||||
<text v-for="v in margGridVols" :key="'mglv'+v"
|
<text v-for="v in margGridVols" :key="'mglv'+v"
|
||||||
:x="margCx(v)" :y="margMargin.top + margPlotH + 18" text-anchor="middle" class="axis-label-sm">{{ v }}m³</text>
|
:x="margCx(v)" :y="margMargin.top + margPlotH + 18" text-anchor="middle" class="axis-label-sm">{{ v }}m³</text>
|
||||||
@@ -683,7 +683,7 @@
|
|||||||
<!-- Household count labels (left axis) -->
|
<!-- Household count labels (left axis) -->
|
||||||
<text v-for="c in margGridCounts" :key="'mgc'+c"
|
<text v-for="c in margGridCounts" :key="'mgc'+c"
|
||||||
:x="margMargin.left - 4" :y="margCyCount(c) + 3" text-anchor="end"
|
:x="margMargin.left - 4" :y="margCyCount(c) + 3" text-anchor="end"
|
||||||
font-size="9.5" fill="#64748b" font-weight="500">{{ c }}</text>
|
font-size="9.5" :fill="t.textMuted" font-weight="500">{{ c }}</text>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Household histogram bars (behind the curve) -->
|
<!-- Household histogram bars (behind the curve) -->
|
||||||
@@ -699,14 +699,14 @@
|
|||||||
<text v-if="bk.count > 0"
|
<text v-if="bk.count > 0"
|
||||||
:x="(margCx(bk.low) + margCx(bk.high)) / 2"
|
:x="(margCx(bk.low) + margCx(bk.high)) / 2"
|
||||||
:y="margCyCount(bk.count) - 3"
|
:y="margCyCount(bk.count) - 3"
|
||||||
text-anchor="middle" font-size="9.5" fill="#475569" font-weight="600">
|
text-anchor="middle" font-size="9.5" :fill="t.textCount" font-weight="600">
|
||||||
{{ bk.count }}
|
{{ bk.count }}
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Price line (marginal) -->
|
<!-- Price line (marginal) -->
|
||||||
<polyline :points="margPriceLine" fill="none"
|
<polyline :points="margPriceLine" fill="none"
|
||||||
:stroke="showHouseholds ? '#cbd5e1' : '#1e40af'"
|
:stroke="showHouseholds ? (isDark ? '#475569' : '#cbd5e1') : (isDark ? '#93c5fd' : '#1e40af')"
|
||||||
:stroke-width="showHouseholds ? 1.5 : 2.5" stroke-linecap="round" />
|
:stroke-width="showHouseholds ? 1.5 : 2.5" stroke-linecap="round" />
|
||||||
|
|
||||||
<!-- Household dots on marginal price chart -->
|
<!-- Household dots on marginal price chart -->
|
||||||
@@ -714,8 +714,8 @@
|
|||||||
<circle v-for="(hh, i) in householdDotsMarg" :key="'hm'+i"
|
<circle v-for="(hh, i) in householdDotsMarg" :key="'hm'+i"
|
||||||
:cx="margCx(hh.volume)" :cy="margCyPrice(hh.price)"
|
:cx="margCx(hh.volume)" :cy="margCyPrice(hh.price)"
|
||||||
r="3"
|
r="3"
|
||||||
fill="#1e40af" opacity="0.5"
|
:fill="isDark ? '#93c5fd' : '#1e40af'" opacity="0.5"
|
||||||
stroke="white" stroke-width="0.6"
|
:stroke="t.plotBg" stroke-width="0.6"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
@@ -726,7 +726,7 @@
|
|||||||
|
|
||||||
<!-- p0 label -->
|
<!-- p0 label -->
|
||||||
<text :x="margMargin.left + margPlotW + 4" :y="margCyPrice(curveData.p0_linear) - 5" text-anchor="start"
|
<text :x="margMargin.left + margPlotW + 4" :y="margCyPrice(curveData.p0_linear) - 5" text-anchor="start"
|
||||||
font-size="9" fill="#475569" font-weight="600">
|
font-size="9" :fill="t.textCount" font-weight="600">
|
||||||
{{ curveData.p0_linear?.toFixed(2) }}€/m³
|
{{ curveData.p0_linear?.toFixed(2) }}€/m³
|
||||||
</text>
|
</text>
|
||||||
|
|
||||||
@@ -746,11 +746,11 @@
|
|||||||
|
|
||||||
<!-- Legend -->
|
<!-- Legend -->
|
||||||
<g :transform="`translate(${margMargin.left + 8}, ${margMargin.top + 6})`">
|
<g :transform="`translate(${margMargin.left + 8}, ${margMargin.top + 6})`">
|
||||||
<rect x="0" y="0" width="230" height="44" rx="6" fill="white" fill-opacity="0.92" stroke="#e2e8f0" />
|
<rect x="0" y="0" width="230" height="44" rx="6" :fill="t.legendBg" fill-opacity="0.92" :stroke="t.legendBorder" />
|
||||||
<line x1="10" y1="14" x2="28" y2="14" stroke="#1e40af" stroke-width="2.5" stroke-linecap="round" />
|
<line x1="10" y1="14" x2="28" y2="14" :stroke="isDark ? '#93c5fd' : '#1e40af'" stroke-width="2.5" stroke-linecap="round" />
|
||||||
<text x="34" y="18" font-size="10" fill="#334155" font-weight="500">Prix au m³ avec abonnement (€/m³)</text>
|
<text x="34" y="18" font-size="10" :fill="t.text" font-weight="500">Prix au m³ avec abonnement (€/m³)</text>
|
||||||
<rect x="10" y="25" width="14" height="10" rx="2" fill="#2563eb" fill-opacity="0.25" />
|
<rect x="10" y="25" width="14" height="10" rx="2" :fill="isDark ? '#60a5fa' : '#2563eb'" fill-opacity="0.25" />
|
||||||
<text x="34" y="34" font-size="10" fill="#334155" font-weight="500">Foyers par tranche de 30m³</text>
|
<text x="34" y="34" font-size="10" :fill="t.text" font-weight="500">Foyers par tranche de 30m³</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -787,6 +787,26 @@ import {
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
const isDark = useState('theme-dark', () => false)
|
||||||
|
|
||||||
|
// Reactive SVG theme colors (light/dark aware)
|
||||||
|
const t = computed(() => isDark.value ? {
|
||||||
|
plotBg: '#1e293b', grid: '#334155', legendBg: '#1e293b', legendBorder: '#475569',
|
||||||
|
text: '#cbd5e1', textDark: '#f1f5f9', textMuted: '#94a3b8', textCount: '#94a3b8',
|
||||||
|
pillBg: '#1e293b', pillBorder: '#475569', pillText: '#f1f5f9',
|
||||||
|
gradStart: 'rgba(96,165,250,0.3)', gradEnd: 'rgba(96,165,250,0.08)',
|
||||||
|
metricBg: 'linear-gradient(135deg, #1e293b, #162032)',
|
||||||
|
highlightBg: 'rgba(52,211,153,0.1)',
|
||||||
|
devBg: '#451a03', devBorder: '#92400e',
|
||||||
|
} : {
|
||||||
|
plotBg: '#f8fafc', grid: '#e2e8f0', legendBg: 'white', legendBorder: '#e2e8f0',
|
||||||
|
text: '#334155', textDark: '#1e293b', textMuted: '#64748b', textCount: '#475569',
|
||||||
|
pillBg: 'white', pillBorder: '#e2e8f0', pillText: '#1e293b',
|
||||||
|
gradStart: 'rgba(37,99,235,0.35)', gradEnd: 'rgba(37,99,235,0.12)',
|
||||||
|
metricBg: 'linear-gradient(135deg, #eff6ff, #f0fdf4)',
|
||||||
|
highlightBg: 'rgba(5,150,105,0.06)',
|
||||||
|
devBg: '#fef3c7', devBorder: '#f59e0b',
|
||||||
|
})
|
||||||
|
|
||||||
const slug = route.params.slug as string
|
const slug = route.params.slug as string
|
||||||
const commune = ref<any>(null)
|
const commune = ref<any>(null)
|
||||||
@@ -1576,8 +1596,8 @@ function renderMarkdown(md: string): string {
|
|||||||
|
|
||||||
/* ── Chart cards ── */
|
/* ── Chart cards ── */
|
||||||
.chart-card {
|
.chart-card {
|
||||||
background: white;
|
background: var(--color-surface);
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid var(--color-border);
|
||||||
border-radius: clamp(8px, 2vw, 12px);
|
border-radius: clamp(8px, 2vw, 12px);
|
||||||
padding: clamp(0.75rem, 2vw, 1.5rem);
|
padding: clamp(0.75rem, 2vw, 1.5rem);
|
||||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.02);
|
box-shadow: 0 1px 4px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.02);
|
||||||
@@ -1595,13 +1615,13 @@ function renderMarkdown(md: string): string {
|
|||||||
.chart-title {
|
.chart-title {
|
||||||
font-size: clamp(0.95rem, 2.5vw, 1.1rem);
|
font-size: clamp(0.95rem, 2.5vw, 1.1rem);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1e293b;
|
color: var(--color-text);
|
||||||
margin-bottom: 0.15rem;
|
margin-bottom: 0.15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-subtitle {
|
.chart-subtitle {
|
||||||
font-size: clamp(0.72rem, 1.8vw, 0.82rem);
|
font-size: clamp(0.72rem, 1.8vw, 0.82rem);
|
||||||
color: #64748b;
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Zoom bar ── */
|
/* ── Zoom bar ── */
|
||||||
@@ -1615,7 +1635,7 @@ function renderMarkdown(md: string): string {
|
|||||||
|
|
||||||
.zoom-info {
|
.zoom-info {
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
color: #94a3b8;
|
color: var(--color-text-muted);
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1639,7 +1659,7 @@ function renderMarkdown(md: string): string {
|
|||||||
.zoom-separator {
|
.zoom-separator {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
background: #e2e8f0;
|
background: var(--color-border);
|
||||||
margin: 0 0.25rem;
|
margin: 0 0.25rem;
|
||||||
}
|
}
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
@@ -1658,12 +1678,13 @@ function renderMarkdown(md: string): string {
|
|||||||
.abo-input {
|
.abo-input {
|
||||||
width: 56px;
|
width: 56px;
|
||||||
padding: 0.15rem 0.35rem;
|
padding: 0.15rem 0.35rem;
|
||||||
border: 1px solid #cbd5e1;
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
background: white;
|
background: var(--color-surface);
|
||||||
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
.abo-input:focus {
|
.abo-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -1672,9 +1693,9 @@ function renderMarkdown(md: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── SVG axes ── */
|
/* ── SVG axes ── */
|
||||||
.axis-label { font-size: 10.5px; fill: #475569; font-weight: 500; font-family: system-ui, -apple-system, sans-serif; }
|
.axis-label { font-size: 10.5px; fill: var(--svg-text); font-weight: 500; font-family: system-ui, -apple-system, sans-serif; }
|
||||||
.axis-label-sm { font-size: 10px; fill: #64748b; font-weight: 500; font-family: system-ui, -apple-system, sans-serif; }
|
.axis-label-sm { font-size: 10px; fill: var(--svg-text-light); font-weight: 500; font-family: system-ui, -apple-system, sans-serif; }
|
||||||
.axis-title { font-size: 11px; fill: #64748b; font-weight: 600; font-family: system-ui, -apple-system, sans-serif; }
|
.axis-title { font-size: 11px; fill: var(--svg-text-light); font-weight: 600; font-family: system-ui, -apple-system, sans-serif; }
|
||||||
|
|
||||||
/* ── Chart containers ── */
|
/* ── Chart containers ── */
|
||||||
.chart-container svg {
|
.chart-container svg {
|
||||||
@@ -1757,10 +1778,10 @@ function renderMarkdown(md: string): string {
|
|||||||
.section-title {
|
.section-title {
|
||||||
font-size: clamp(1rem, 2.5vw, 1.15rem);
|
font-size: clamp(1rem, 2.5vw, 1.15rem);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1e293b;
|
color: var(--color-text);
|
||||||
margin: clamp(1rem, 3vw, 1.5rem) 0 clamp(0.75rem, 2vw, 1rem);
|
margin: clamp(1rem, 3vw, 1.5rem) 0 clamp(0.75rem, 2vw, 1rem);
|
||||||
padding-bottom: 0.4rem;
|
padding-bottom: 0.4rem;
|
||||||
border-bottom: 2px solid #e2e8f0;
|
border-bottom: 2px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Params + impacts ── */
|
/* ── Params + impacts ── */
|
||||||
@@ -1804,8 +1825,7 @@ function renderMarkdown(md: string): string {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, 1fr);
|
grid-template-columns: repeat(5, 1fr);
|
||||||
gap: clamp(0.5rem, 2vw, 1rem);
|
gap: clamp(0.5rem, 2vw, 1rem);
|
||||||
background: linear-gradient(135deg, #eff6ff, #f0fdf4);
|
border: 1px solid var(--color-border);
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: clamp(8px, 2vw, 12px);
|
border-radius: clamp(8px, 2vw, 12px);
|
||||||
padding: clamp(0.75rem, 2vw, 1.25rem) clamp(0.75rem, 2vw, 1.5rem);
|
padding: clamp(0.75rem, 2vw, 1.25rem) clamp(0.75rem, 2vw, 1.5rem);
|
||||||
}
|
}
|
||||||
@@ -1823,11 +1843,11 @@ function renderMarkdown(md: string): string {
|
|||||||
.key-metric-value {
|
.key-metric-value {
|
||||||
font-size: clamp(1.1rem, 3vw, 1.4rem);
|
font-size: clamp(1.1rem, 3vw, 1.4rem);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1e293b;
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
.key-metric-label {
|
.key-metric-label {
|
||||||
font-size: clamp(0.68rem, 1.6vw, 0.78rem);
|
font-size: clamp(0.68rem, 1.6vw, 0.78rem);
|
||||||
color: #64748b;
|
color: var(--color-text-muted);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.key-metric-tag {
|
.key-metric-tag {
|
||||||
@@ -1837,14 +1857,15 @@ function renderMarkdown(md: string): string {
|
|||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
padding: 0.1rem 0.5rem;
|
padding: 0.1rem 0.5rem;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
background: #f1f5f9;
|
background: var(--color-bg);
|
||||||
color: #64748b;
|
color: var(--color-text-muted);
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
.key-metric-tag-calc {
|
.key-metric-tag-calc {
|
||||||
background: #eff6ff;
|
background: var(--color-primary);
|
||||||
color: #2563eb;
|
color: white;
|
||||||
border-color: #bfdbfe;
|
border-color: var(--color-primary);
|
||||||
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
.key-metric-input {
|
.key-metric-input {
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
@@ -1866,12 +1887,12 @@ function renderMarkdown(md: string): string {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: clamp(0.72rem, 1.8vw, 0.8rem);
|
font-size: clamp(0.72rem, 1.8vw, 0.8rem);
|
||||||
padding: 0.2rem 0;
|
padding: 0.2rem 0;
|
||||||
color: #475569;
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
.vote-result-row strong {
|
.vote-result-row strong {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
color: #1e293b;
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Alerts ── */
|
/* ── Alerts ── */
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Dev credentials hint -->
|
<!-- Dev credentials hint -->
|
||||||
<div v-if="isDev" style="margin-top: 1rem; padding: 0.75rem; background: #fef3c7; border: 1px solid #f59e0b; border-radius: 6px; font-size: 0.8rem;">
|
<div v-if="isDev" class="dev-hint">
|
||||||
<strong>Dev:</strong> superadmin@sejeteralo.fr / superadmin
|
<strong>Dev:</strong> superadmin@sejeteralo.fr / superadmin
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Dev credentials hint -->
|
<!-- Dev credentials hint -->
|
||||||
<div v-if="isDev" style="margin-top: 1rem; padding: 0.75rem; background: #fef3c7; border: 1px solid #f59e0b; border-radius: 6px; font-size: 0.8rem;">
|
<div v-if="isDev" class="dev-hint">
|
||||||
<strong>Dev:</strong> saou@sejeteralo.fr / saou2024
|
<strong>Dev:</strong> saou@sejeteralo.fr / saou2024
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user