Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db964d44b2 | |||
| b5d58911fb | |||
| 8b1dd9b170 | |||
| daad6bb1d5 | |||
| 129c9cfc7b |
@@ -87,13 +87,6 @@ pinia@^3.0.2 # State management
|
||||
typescript@^5.8.2 # Typage statique
|
||||
```
|
||||
|
||||
Le frontend étend le layer partagé **`../../yvv-nuxt-base`** (`extends` dans
|
||||
`nuxt.config.ts`) : le composable `useMood` y centralise l'application DOM des
|
||||
palettes (variables CSS + classe `.palette-dark`), la persistance localStorage et
|
||||
le light/dark. Les palettes propres à SejeteralO (6 ambiances + variables SVG des
|
||||
graphiques) sont définies côté projet dans `app/composables/usePalette.ts`, qui
|
||||
délègue à `useMood` et synchronise l'état partagé `useState('theme-dark')`.
|
||||
|
||||
## Base de donnees
|
||||
|
||||
**SQLite** en dev/production legere, configurable via `DATABASE_URL`.
|
||||
|
||||
@@ -5,12 +5,22 @@ Computes the element-wise median of (vinf, a, b, c, d, e) across all active vote
|
||||
This parametric median is chosen over geometric median because:
|
||||
- It's transparent and politically explainable
|
||||
- The result is itself a valid set of Bézier parameters
|
||||
|
||||
Post-processing: the median is sanitized to guarantee monotonicity of tier 2.
|
||||
The condition for tier 2 to be non-decreasing is e <= 1/3.
|
||||
If the raw median violates this, e is clamped to E_MAX_MONOTONE.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
# Maximum value of e that guarantees tier-2 price monotonicity.
|
||||
# prix_m3_2 = p0 + (pmax-p0) * ((1-3e)t³ + 3e t²)
|
||||
# The cubic is monotone non-decreasing on [0,1] iff (1-3e) >= 0, i.e. e <= 1/3.
|
||||
E_MAX_MONOTONE = 1.0 / 3.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class VoteParams:
|
||||
"""The 6 citizen-adjustable parameters."""
|
||||
@@ -22,23 +32,49 @@ class VoteParams:
|
||||
e: float
|
||||
|
||||
|
||||
def _is_tier2_monotone(e: float) -> bool:
|
||||
"""Check if tier-2 price curve is monotone non-decreasing."""
|
||||
return e <= E_MAX_MONOTONE
|
||||
|
||||
|
||||
def sanitize_median(params: "VoteParams") -> "VoteParams":
|
||||
"""
|
||||
Ensure the median curve is physically valid:
|
||||
- Tier 2 must be monotone non-decreasing (penalizes high consumption)
|
||||
- If e violates monotonicity, clamp it to E_MAX_MONOTONE
|
||||
"""
|
||||
e = params.e
|
||||
if not _is_tier2_monotone(e):
|
||||
e = E_MAX_MONOTONE
|
||||
|
||||
return VoteParams(
|
||||
vinf=params.vinf,
|
||||
a=params.a,
|
||||
b=params.b,
|
||||
c=params.c,
|
||||
d=params.d,
|
||||
e=e,
|
||||
)
|
||||
|
||||
|
||||
def compute_median(votes: list[VoteParams]) -> VoteParams | None:
|
||||
"""
|
||||
Compute element-wise median of vote parameters.
|
||||
|
||||
Returns None if no votes provided.
|
||||
The result is sanitized for tier-2 monotonicity.
|
||||
"""
|
||||
if not votes:
|
||||
return None
|
||||
|
||||
vinfs = [v.vinf for v in votes]
|
||||
a_s = [v.a for v in votes]
|
||||
b_s = [v.b for v in votes]
|
||||
c_s = [v.c for v in votes]
|
||||
d_s = [v.d for v in votes]
|
||||
e_s = [v.e for v in votes]
|
||||
a_s = [v.a for v in votes]
|
||||
b_s = [v.b for v in votes]
|
||||
c_s = [v.c for v in votes]
|
||||
d_s = [v.d for v in votes]
|
||||
e_s = [v.e for v in votes]
|
||||
|
||||
return VoteParams(
|
||||
raw = VoteParams(
|
||||
vinf=float(np.median(vinfs)),
|
||||
a=float(np.median(a_s)),
|
||||
b=float(np.median(b_s)),
|
||||
@@ -46,3 +82,5 @@ def compute_median(votes: list[VoteParams]) -> VoteParams | None:
|
||||
d=float(np.median(d_s)),
|
||||
e=float(np.median(e_s)),
|
||||
)
|
||||
|
||||
return sanitize_median(raw)
|
||||
@@ -0,0 +1,47 @@
|
||||
# docker-compose.jetson.yml
|
||||
# Ports décalés (8001/3001) pour ne pas entrer en conflit
|
||||
# avec les services existants sur le Jetson.
|
||||
# Nginx fait le reverse proxy depuis sejeteraleau.nicolasboyer.com
|
||||
|
||||
services:
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/backend.Dockerfile
|
||||
target: production
|
||||
environment:
|
||||
DATABASE_URL: sqlite+aiosqlite:////data/sejeteralo.db
|
||||
SECRET_KEY: CHANGEZ-MOI-cle-longue-et-aleatoire-32-chars-min
|
||||
DEBUG: "false"
|
||||
CORS_ORIGINS: '["https://sejeteraleau.nicolasboyer.com"]'
|
||||
ports:
|
||||
- "127.0.0.1:8010:8000"
|
||||
volumes:
|
||||
- backend-data:/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/frontend.Dockerfile
|
||||
target: production
|
||||
args:
|
||||
# URL vue depuis le navigateur du visiteur
|
||||
NUXT_PUBLIC_API_BASE: https://sejeteraleau.nicolasboyer.com/api/v1
|
||||
environment:
|
||||
NUXT_PUBLIC_API_BASE: https://sejeteraleau.nicolasboyer.com/api/v1
|
||||
PORT: "3000"
|
||||
ports:
|
||||
- "127.0.0.1:3010:3000"
|
||||
depends_on:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
backend-data:
|
||||
@@ -61,8 +61,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Palette : identité + application DOM/persistance/dark déléguées à useMood.
|
||||
const { paletteList, currentPalette, setPalette, initPalette } = usePalette()
|
||||
// Global shared dark mode state (accessible from any component via useState)
|
||||
const isDark = useState('theme-dark', () => false)
|
||||
|
||||
const selectorRef = ref<HTMLElement>()
|
||||
const isOpen = ref(false)
|
||||
@@ -101,6 +101,92 @@ function setDensity(d: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Palette definitions ──
|
||||
interface Palette {
|
||||
name: string; label: string; dark: boolean
|
||||
primary: string; accent: string
|
||||
bg: string; surface: string; text: string; textMuted: string; border: string
|
||||
// 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')
|
||||
|
||||
function setPalette(name: string) {
|
||||
currentPalette.value = name
|
||||
const p = paletteList.find(x => x.name === name)
|
||||
if (!p || !import.meta.client) return
|
||||
localStorage.setItem('sej-palette', name)
|
||||
|
||||
const root = document.documentElement
|
||||
const s = root.style
|
||||
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 {
|
||||
const r = parseInt(hex.slice(1, 3), 16)
|
||||
const g = parseInt(hex.slice(3, 5), 16)
|
||||
const b = parseInt(hex.slice(5, 7), 16)
|
||||
const f = 0.82
|
||||
return `#${Math.round(r * f).toString(16).padStart(2, '0')}${Math.round(g * f).toString(16).padStart(2, '0')}${Math.round(b * f).toString(16).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// ── Init from localStorage ──
|
||||
onMounted(() => {
|
||||
if (!import.meta.client) return
|
||||
@@ -108,8 +194,8 @@ onMounted(() => {
|
||||
if (savedFont) setFontSize(savedFont)
|
||||
const savedDensity = localStorage.getItem('sej-density')
|
||||
if (savedDensity) setDensity(savedDensity)
|
||||
// Palette : restauration + application déléguées au composable partagé.
|
||||
initPalette()
|
||||
const savedPalette = localStorage.getItem('sej-palette')
|
||||
if (savedPalette) setPalette(savedPalette)
|
||||
})
|
||||
|
||||
// ── Click outside to close ──
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import type { MoodDef } from '#imports'
|
||||
|
||||
/**
|
||||
* Palettes SejeteralO — identité couleur du projet.
|
||||
* L'application DOM (variables CSS + classe .palette-dark), la persistance et
|
||||
* le light/dark sont délégués au mécanisme partagé `useMood` (@yvv/nuxt-base).
|
||||
* Chaque palette injecte AUSSI ses variables SVG (thème des graphiques) et un
|
||||
* `--color-primary-dark` dérivé — d'où le précalcul en `vars`.
|
||||
*/
|
||||
export interface Palette {
|
||||
name: string
|
||||
label: string
|
||||
dark: boolean
|
||||
primary: string
|
||||
accent: string
|
||||
bg: string
|
||||
surface: string
|
||||
text: string
|
||||
textMuted: string
|
||||
border: string
|
||||
// SVG-specific (thème des graphiques)
|
||||
svgPlotBg: string
|
||||
svgGrid: string
|
||||
svgLegendBg: string
|
||||
svgText: string
|
||||
svgTextLight: string
|
||||
}
|
||||
|
||||
export 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',
|
||||
},
|
||||
]
|
||||
|
||||
/** Assombrit une couleur hex (facteur 0.82) — variante `--color-primary-dark`. */
|
||||
function darken(hex: string): string {
|
||||
const r = parseInt(hex.slice(1, 3), 16)
|
||||
const g = parseInt(hex.slice(3, 5), 16)
|
||||
const b = parseInt(hex.slice(5, 7), 16)
|
||||
const f = 0.82
|
||||
return `#${Math.round(r * f).toString(16).padStart(2, '0')}${Math.round(g * f).toString(16).padStart(2, '0')}${Math.round(b * f).toString(16).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const paletteMoods: MoodDef[] = paletteList.map(p => ({
|
||||
id: p.name,
|
||||
label: p.label,
|
||||
isDark: p.dark,
|
||||
vars: {
|
||||
'--color-primary': p.primary,
|
||||
'--color-primary-dark': darken(p.primary),
|
||||
'--color-secondary': p.accent,
|
||||
'--color-bg': p.bg,
|
||||
'--color-surface': p.surface,
|
||||
'--color-text': p.text,
|
||||
'--color-text-muted': p.textMuted,
|
||||
'--color-border': p.border,
|
||||
// Thème SVG (graphiques)
|
||||
'--svg-plot-bg': p.svgPlotBg,
|
||||
'--svg-grid': p.svgGrid,
|
||||
'--svg-legend-bg': p.svgLegendBg,
|
||||
'--svg-text': p.svgText,
|
||||
'--svg-text-light': p.svgTextLight,
|
||||
},
|
||||
}))
|
||||
|
||||
export function usePalette() {
|
||||
const { currentMood, setMood, initMood } = useMood(paletteMoods, {
|
||||
storageKey: 'sej-palette',
|
||||
defaultId: 'eau',
|
||||
syncColorMode: false, // pas de @nuxtjs/color-mode
|
||||
lightDarkClasses: false, // le CSS ne stylise que .palette-dark
|
||||
darkClass: 'palette-dark',
|
||||
})
|
||||
|
||||
// État dark partagé, lu par la page graphiques (commune/[slug]) pour le thème SVG.
|
||||
const isDark = useState('theme-dark', () => false)
|
||||
|
||||
function syncDark(id: string) {
|
||||
const m = paletteMoods.find(x => x.id === id)
|
||||
if (m) isDark.value = m.isDark
|
||||
}
|
||||
|
||||
const currentPalette = computed(() => currentMood.value || 'eau')
|
||||
|
||||
function setPalette(name: string) {
|
||||
setMood(name)
|
||||
syncDark(name)
|
||||
}
|
||||
|
||||
function initPalette() {
|
||||
initMood()
|
||||
syncDark(currentMood.value || 'eau')
|
||||
}
|
||||
|
||||
return { paletteList, currentPalette, setPalette, initPalette, isDark }
|
||||
}
|
||||
@@ -231,7 +231,7 @@
|
||||
</text>
|
||||
|
||||
<!-- Legend box (top-right) -->
|
||||
<g :transform="`translate(${margin.left + plotW - 232}, ${margin.top + 8})`">
|
||||
<g :transform="`translate(${margin.left + 8}, ${margin.top + plotH - 90})`">
|
||||
<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" />
|
||||
<text x="34" y="18" font-size="11" :fill="t.text">Consommations foyers</text>
|
||||
@@ -1295,14 +1295,16 @@ const gridPrices2 = computed(() => {
|
||||
return arr
|
||||
})
|
||||
|
||||
function toPolyline(vols: number[] | undefined, vals: number[] | undefined, cyFn: (v: number) => number): string {
|
||||
function toPolyline(vols: number[] | undefined, vals: number[] | undefined, cyFn: (v: number) => number, minVol = 15): string {
|
||||
if (!vols?.length || !vals?.length) return ''
|
||||
const pts: string[] = []
|
||||
for (let i = 0; i < vols.length; i += 4) {
|
||||
pts.push(`${cx2(vols[i]!)},${cyFn(vals[i]!)}`)
|
||||
const v = vols[i]!
|
||||
if (v < minVol) continue
|
||||
pts.push(`${cx2(v)},${cyFn(vals[i]!)}`)
|
||||
}
|
||||
const last = vols.length - 1
|
||||
if (last % 4 !== 0) pts.push(`${cx2(vols[last]!)},${cyFn(vals[last]!)}`)
|
||||
if (last % 4 !== 0 && vols[last]! >= minVol) pts.push(`${cx2(vols[last]!)},${cyFn(vals[last]!)}`)
|
||||
return pts.join(' ')
|
||||
}
|
||||
|
||||
@@ -1391,7 +1393,7 @@ const margPriceLine = computed(() => {
|
||||
const pts: string[] = []
|
||||
for (let i = 0; i < vols.length; i += 3) {
|
||||
const v = vols[i]!
|
||||
if (v > margVolMax.value) break
|
||||
if (v < 15 || v > margVolMax.value) continue // ← changer break par continue, et ajouter v < 15
|
||||
pts.push(`${margCx(v)},${margCyPrice(prices[i]!)}`)
|
||||
}
|
||||
return pts.join(' ')
|
||||
@@ -1406,7 +1408,7 @@ const margAvgPriceWithAboLine = computed(() => {
|
||||
const pts: string[] = []
|
||||
for (let i = 0; i < vols.length; i += 3) {
|
||||
const v = vols[i]!
|
||||
if (v < 1 || v > margVolMax.value) continue
|
||||
if (v < 15 || v > margVolMax.value) continue
|
||||
const avgP = citizenAbo.value / v + p0lin
|
||||
if (avgP <= margPriceMax.value * 1.5) {
|
||||
pts.push(`${margCx(v)},${margCyPrice(avgP)}`)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
export default defineNuxtConfig({
|
||||
extends: ['@yvv/nuxt-base'],
|
||||
|
||||
compatibilityDate: '2025-01-01',
|
||||
future: { compatibilityVersion: 4 },
|
||||
|
||||
|
||||
Generated
-5
@@ -8,7 +8,6 @@
|
||||
"name": "sejeteralo-frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@yvv/nuxt-base": "git+ssh://gitea@git.open.us.org/yvv/yvv-nuxt-base.git#v0.1.0",
|
||||
"nuxt": "^4.3.1",
|
||||
"vue": "^3.5.28",
|
||||
"vue-router": "^5.0.3"
|
||||
@@ -3833,10 +3832,6 @@
|
||||
"integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@yvv/nuxt-base": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "git+ssh://gitea@git.open.us.org/yvv/yvv-nuxt-base.git#b1c6a2f28052564d505dd05b363169cb97fb81b3"
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz",
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"generate": "nuxt generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@yvv/nuxt-base": "git+ssh://gitea@git.open.us.org/yvv/yvv-nuxt-base.git#v0.1.0",
|
||||
"nuxt": "^4.3.1",
|
||||
"vue": "^3.5.28",
|
||||
"vue-router": "^5.0.3"
|
||||
|
||||
Reference in New Issue
Block a user