This commit is contained in:
2026-09-30 23:49:38 +00:00
parent 749bd0f86a
commit 491f464242
519 changed files with 36530 additions and 0 deletions

206
DEFENSE_SIMULATION.md Normal file
View File

@@ -0,0 +1,206 @@
# Generalized Projectile Defense Simulation
## Overview
This simulation models a realistic defense scenario where an attack projectile (drone, missile, or ballistic re-entry vehicle) approaches a city defended by a ring of radars and interceptor drones. The simulation uses proper physics, realistic flight profiles, and validated error models.
It is **generalized** — the attack profile is selected via **presets** for different projectile types. Each preset defines the speed, cruise altitude band, descent geometry, and terminal altitude. The default preset is the jet-powered **Shahed-238 (Geran-3)**. All attack parameters are also editable manually for custom scenarios.
## Projectile Presets
| Preset | Type | Speed | Cruise Alt | Descent Start | Terminal Alt | Notes |
|--------|------|-------|-----------|---------------|--------------|-------|
| `shahed-136` | Prop drone (Geran-2) | 50 m/s | 100–300 m | 5 km | 0 m | Slow, low-altitude |
| `shahed-238` | Jet drone (Geran-3) | 150 m/s | 1000–2000 m | 8 km | 200 m | Default; fast, mid-altitude |
| `cruise-missile` | Cruise missile | 300 m/s | 50–150 m | 5 km | 0 m | Fast, sea-skimming |
| `ballistic-reentry` | Ballistic re-entry | 800 m/s | 60000–80000 m | 40 km | 0 m | Very fast, high-altitude entry |
| `prop-drone` | Small UAS | 30 m/s | 100–300 m | 3 km | 0 m | Slow, low-altitude |
### Shahed-238 Specifications (Jet-Powered Variant)
Based on research of open-source intelligence and technical analysis:
| Parameter | Value | Notes |
|-----------|-------|-------|
| Speed (cruise) | ~150 m/s (540 km/h) | 3-4x faster than Shahed-136 |
| Cruise altitude | 1000-2000 m | Typical operational band |
| Terminal altitude | 200 m | Descent target altitude |
| Descent start | ~8 km from target | Gradual descent begins |
| Warhead | 30-50 kg | High explosive |
| Guidance | GPS/INS + terminal seeker | Autonomous |
| Range | ~1500-2000 km | One-way attack |
Sources:
- [CSIS Missile Threat - Shahed-238](https://missilethreat.csis.org/missile/shahed-238/)
- [BattlePolicy - Shahed-238](https://www.battlepolicy.com/shahed-238/)
- [Wikipedia - HESA Shahed 136](https://en.wikipedia.org/wiki/HESA_Shahed_136)
- [drone-warfare.com - Shahed variants](https://drone-warfare.com/research/shahed-136/)
## Flight Profile
Each preset follows a realistic two-phase flight profile:
1. **Cruise phase**: Maintains constant altitude (within the preset's cruise band, randomized per run) at cruise speed
2. **Descent phase**: Begins `descent_start` distance from the target, gradually descends to the preset's terminal altitude
The descent is gradual (not a nosedive), consistent with reported loitering-munition attack patterns. The projectile uses GPS/INS navigation to follow a pre-planned waypoint course, with a terminal seeker for the final approach. For the ballistic re-entry preset, the high cruise altitude and long descent start model a high-altitude entry with a terminal pull-down.
## Defense System
### Radar Network
- **8 AERIS-10 radars** positioned in a ring around the city (2 km outside city radius)
- Each radar has **20 km range**
- Single-measurement accuracy: **±20 m (1σ)**
- Detection requires target to be within radar range
- Multiple radars improve position accuracy: σ_combined = σ_single / √n
### Interceptor Drones
- **24 drones** positioned in a ring around the city (8 km outside city radius)
- Drone speed: **100 m/s** (slower than most presets' cruise speed, e.g. Shahed-238 at 150 m/s)
- IR homing range: **5 km**
- IR tracking accuracy: **±3 m (1σ)**
- Kill radius: **20 m** (3D)
- **3 closest drones** launch per threat (selected by fastest viable intercept time)
## Physics Models
### 1. Radar Detection
Each radar measures:
- Range (with Gaussian noise, σ = 20 m)
- Azimuth (with noise proportional to 1/distance)
- Elevation (with noise proportional to 1/distance)
The noise model reflects real radar behavior: angular accuracy improves with range, but absolute position error grows.
### 2. Position Estimation
The 3D position is estimated by averaging the individual 3D point estimates from each radar (bearing + range). This is a least-squares estimate that properly constrains all three coordinates (x, y, z), including altitude.
**Key insight**: With coplanar radars (all at z=0), range measurements alone cannot determine altitude. The bearing (azimuth + elevation) measurements are required to constrain the z-coordinate.
### 3. Trajectory Estimation
The velocity is estimated by linear regression of position vs. time:
```
pos(t) = pos0 + vel * t
```
The slope (vel) is the estimated velocity. R² measures fit quality. The standard error of the slope decreases as more data points accumulate: σ_vel = σ_pos / √(Sxx), where Sxx = Σ(t - t̄)².
**This is the key mechanism by which error decreases with more data points**: the best-fit line converges as more observations are added.
### 4. Intercept Calculation
The drone intercept solves the classic pursuer-target problem:
```
|drone_pos + v_d * t - target_pos - v_s * t| = 0
```
where v_d = drone_speed * d (d is a unit vector).
This reduces to a quadratic in t:
```
(|v_s|² - v_d²) * t² + 2*(rel_pos · v_s) * t + |rel_pos|² = 0
```
The **smallest positive root** gives the earliest intercept time. The intercept point is where the target will be at that time.
**Critical insight**: A slower pursuer (drone at 100 m/s vs a faster target such as Shahed-238 at 150 m/s) can only intercept if:
- The target is moving TOWARD the pursuer (rel_pos · v_s < 0)
- The geometry is favorable (drones positioned in the approach corridor)
- The intercept happens before the target reaches the city
### 5. Drone Guidance
Drones fly in 3D straight lines toward the computed intercept point. The intercept point is recomputed every 5 seconds as new trajectory estimates become available (proportional navigation). When within 5 km, the drone switches to IR homing for final precision.
### 6. Error Propagation
- Radar position error: σ_pos = 20/√n m (decreases with data points)
- 95% confidence interval: 1.96 * σ_pos
- IR error: σ_IR = 3/√n_drones m (decreases with tracking drones)
- Combined error: σ_combined = √(σ_radar² + σ_IR²)
## Validation Tests
The simulation is validated against known analytical solutions:
| Test | Description | Result |
|------|-------------|--------|
| Trajectory continuity | Target speed is constant, altitude profile correct | PASS |
| 1D intercept | Analytical solution: t = 48 s for specific geometry | PASS (t = 48.0 s) |
| No-solution case | Target moving away, no intercept possible | PASS (graceful fallback) |
| 3D intercept | Intercept altitude matches target altitude | PASS |
| Radar detection | In-range detected, out-of-range not | PASS |
| Position estimation | Error < 30 m with 3 radars | PASS (10.2 m) |
| Trajectory estimation | Velocity error < 10 m/s | PASS (0.00 m/s) |
| Simulation runs | Full simulation completes without errors | PASS |
| Interception rate | Rate is within [0, 100]% for default preset | PASS (~67%) |
| Presets vary rates | Slow/low target >= fast/high target intercept rate | PASS |
| Drone selection | Only viable drones launched | PASS |
**All 11 tests pass.**
## Monte Carlo Results (100 Trials per Preset)
| Preset | Interception Rate | Avg Detect | Avg Collision | Avg Impact |
|--------|-------------------|-----------|---------------|-----------|
| `shahed-136` (prop, low) | **100%** | 7.0 s | 88.5 s | — |
| `shahed-238` (jet, mid) | **68%** | 27.2 s | 85.8 s | 170.0 s |
| `cruise-missile` (fast, low) | **36%** | 64.7 s | 98.9 s | 137.6 s |
| `ballistic-reentry` (very fast, high) | **0%** | 110.9 s | — | 114.6 s |
| `prop-drone` (slow, low) | **100%** | 1.0 s | 44.2 s | — |
(Exact values are regenerated each run via `montecarlo_stats.json`; the table shows representative values.)
## Key Findings
1. **Interception rate scales with target speed and altitude.** Slow, low-altitude projectiles (Shahed-136, small UAS) are nearly always intercepted, while fast, high-altitude ones (ballistic re-entry) are effectively unstoppable by this drone defense. The jet-powered Shahed-238 sits in the middle at ~68%.
2. **Detection happens before the target is in the danger zone.** The target enters radar range (20 km) and is detected after accumulating 3+ observations. Faster targets get detected later in absolute terms but with less margin.
3. **The interception window is the gap between detection and impact.** When interception succeeds, it occurs well before the impact time. When it fails, the target reaches the city. This gap is the decision time available to the defense.
4. **The interception rate depends strongly on geometry.** Approaches from directions where the drones are well-positioned in the approach corridor have higher interception rates. Approaches from directions where the drones are on the opposite side of the city fail.
5. **Altitude matters.** Higher cruise altitudes give the drones more time to climb, but also increase the distance they must cover. The optimal altitude depends on the approach geometry.
## Why the Rates Are Realistic
The per-preset interception rates reflect the fundamental physics of a slower defender (100 m/s drones) vs. faster or equal-speed attackers:
- A slow target (30–50 m/s) is easily caught — the drones have ample time to close the gap, so interception is near 100%.
- A mid-speed jet target (150 m/s) outruns part of the pursuit; only favorable geometries allow a timely intercept, giving ~68%.
- A fast sea-skimming cruise missile (300 m/s) is hard to catch, especially at low altitude where climb time hurts; ~36%.
- A very fast high-altitude ballistic re-entry (800 m/s) is far beyond the drones' capability; 0%.
This is consistent with real-world observations that drone-vs-drone interception is challenging against faster targets, and that the interception rate depends heavily on the target's speed, altitude, approach direction, and the positioning of the defenders.
## Files
| File | Description |
|------|-------------|
| `defense_sim.py` | Python simulation with proper physics and projectile presets |
| `test_defense_sim.py` | Validation test suite (11 tests) |
| `defense_sim.html` | Interactive HTML visualization with preset dropdown |
| `montecarlo_stats.json` | Monte Carlo results (100 trials per preset) |
## Running the Simulation
```bash
# Run the Python simulation (runs all presets)
python3 defense_sim.py
# Run the validation tests
python3 test_defense_sim.py
# Open the HTML viewer in a browser (serve over HTTP for the stats table)
python3 -m http.server 8000
# then open http://localhost:8000/defense_sim.html
```
### Selecting a Projectile Preset
- **In the HTML:** use the **Projectile Preset** dropdown at the top of the Attack Parameters panel. Selecting a preset prefills all attack parameters (start distance, speed, cruise altitude band, descent start, terminal altitude) and highlights that preset's row in the stats table. Choose **Custom** to set all parameters manually.
- **In Python:** pass `preset='shahed-238'` (or any preset name) to `run_simulation()` or `monte_carlo()`. Omit it to use the default (`shahed-238`).
## References
1. CSIS Missile Threat Project - Shahed-238: https://missilethreat.csis.org/missile/shahed-238/
2. BattlePolicy - Shahed-238: https://www.battlepolicy.com/shahed-238/
3. Wikipedia - HESA Shahed 136: https://en.wikipedia.org/wiki/HESA_Shahed_136
4. drone-warfare.com - Shahed-136 and Geran variants: https://drone-warfare.com/research/shahed-136/
5. AERIS-10 radar: 10.5 GHz X-band FMCW phased array, 20 km range

57
debug2.js Normal file
View File

@@ -0,0 +1,57 @@
const fs = require('fs');
const html = fs.readFileSync('defense_sim.html', 'utf8');
const script = html.match(/<script>([\s\S]*?)<\/script>/)[1];
function extractFn(src, name) {
const re = new RegExp('function ' + name + '\\(', 'g');
const m = re.exec(src);
if (!m) return null;
let i = m.index, depth = 0, start = src.indexOf('{', i);
for (let j = start; j < src.length; j++) {
if (src[j] === '{') depth++;
else if (src[j] === '}') { depth--; if (depth === 0) return src.slice(i, j + 1); }
}
return null;
}
const fns = ['targetTrajectory','detectFromRadars','estimatePosition3D','estimateTrajectory','computeInterceptPoint','perpDistToLine','gaussianNoise','kmToPixels'];
eval(fns.map(n => extractFn(script, n)).filter(Boolean).join('\n'));
const MEASUREMENT_ERROR = 20.0, IR_ERROR = 3.0, SCALE = 10;
// Single prop-drone trial, fixed geometry for reproducibility
const P = { startDist:15, speed:30, cruiseMin:200, cruiseMax:200, descentStart:3, terminalAlt:0 };
const numRadars=8, numDrones=24, radarRange=20, droneSpeed=100, irRange=5, cityRadius=10, droneLine=8, dt=0.5;
const startAngle = Math.PI/3; // fixed
const startPos = [P.startDist*Math.cos(startAngle), P.startDist*Math.sin(startAngle)];
const targetAngle = Math.PI/3; // straight radial approach
const targetDist = 2;
const targetPos = [targetDist*Math.cos(targetAngle), targetDist*Math.sin(targetAngle)];
const cruiseAlt = 200;
const trajectory = targetTrajectory(startPos, targetPos, cruiseAlt, P.speed, P.descentStart, P.terminalAlt);
const radarPositions = [];
for (let i=0;i<numRadars;i++){const a=2*Math.PI*i/numRadars,d=cityRadius+2;radarPositions.push([d*Math.cos(a),d*Math.sin(a),0]);}
let time=0, detectionTime=null, observations=[];
function getTruePos(t){let b=trajectory[0],bd=1e18;for(const p of trajectory){const d=Math.abs(p.t-t);if(d<bd){bd=d;b=p;}}return b;}
for (let step=0; step<20 && detectionTime===null; step++) {
time += dt;
const tp = getTruePos(time);
const pos = [tp.x, tp.y, tp.z/1000];
const detections = detectFromRadars(pos, radarPositions, radarRange, gaussianNoise);
if (detections.length>0) observations.push({x:pos[0],y:pos[1],z:pos[2]*1000,t:time});
if (observations.length>=3) {
detectionTime = time;
const posEst = estimatePosition3D(detections, radarPositions);
if (posEst) observations[observations.length-1] = {x:posEst[0]/1000,y:posEst[1]/1000,z:posEst[2],t:time};
console.log(`=== detection at t=${time.toFixed(1)}s, ${observations.length} observations ===`);
console.log('true target pos (km):', pos.map(v=>v.toFixed(3)));
if (posEst) console.log('posEst (km):', posEst.map(v=>v.toFixed(3)));
console.log('observations (x km, y km, z m, t s):');
for (const o of observations) console.log(' ', o.x.toFixed(4), o.y.toFixed(4), o.z.toFixed(2), o.t.toFixed(1));
const traj = estimateTrajectory(observations);
const velMs = [traj.vel[0]*1000, traj.vel[1]*1000, traj.vel[2]];
console.log('vel (raw):', traj.vel.map(v=>v.toExponential(3)));
console.log('velMs:', velMs.map(v=>v.toFixed(2)));
console.log('speedEst:', Math.sqrt(velMs[0]**2+velMs[1]**2+velMs[2]**2).toFixed(1), 'R2:', traj.r2.toFixed(3));
break;
}
}

162
debug_sim.js Normal file
View File

@@ -0,0 +1,162 @@
const fs = require('fs');
const html = fs.readFileSync('defense_sim.html', 'utf8');
const script = html.match(/<script>([\s\S]*?)<\/script>/)[1];
function extractFn(src, name) {
const re = new RegExp('function ' + name + '\\(', 'g');
const m = re.exec(src);
if (!m) return null;
let i = m.index, depth = 0, start = src.indexOf('{', i);
for (let j = start; j < src.length; j++) {
if (src[j] === '{') depth++;
else if (src[j] === '}') { depth--; if (depth === 0) return src.slice(i, j + 1); }
}
return null;
}
function extractConst(src, name) {
const m = new RegExp('const ' + name + ' = ([^;\\n]+);').exec(src);
return m ? m[1].trim() : null;
}
const fns = ['targetTrajectory', 'detectFromRadars', 'estimatePosition3D', 'estimateTrajectory',
'computeInterceptPoint', 'perpDistToLine', 'gaussianNoise', 'kmToPixels'];
eval(fns.map(n => extractFn(script, n)).filter(Boolean).join('\n'));
const MEASUREMENT_ERROR = parseFloat(extractConst(script, 'MEASUREMENT_ERROR'));
const IR_ERROR = parseFloat(extractConst(script, 'IR_ERROR'));
const SCALE = parseFloat(extractConst(script, 'SCALE'));
console.log('MEASUREMENT_ERROR', MEASUREMENT_ERROR, 'IR_ERROR', IR_ERROR, 'SCALE', SCALE);
function runTrial(preset, verbose) {
const P = {
'shahed-136': { startDist:30, speed:50, cruiseMin:100, cruiseMax:300, descentStart:5, terminalAlt:0 },
'shahed-238': { startDist:35, speed:150, cruiseMin:1000, cruiseMax:2000, descentStart:8, terminalAlt:200 },
'prop-drone': { startDist:15, speed:30, cruiseMin:100, cruiseMax:300, descentStart:3, terminalAlt:0 },
}[preset];
const numRadars = 8, numDrones = 24, radarRange = 20, droneSpeed = 100, irRange = 5, cityRadius = 10, droneLine = 8;
const dt = 0.5;
const startAngle = Math.random() * 2 * Math.PI;
const startPos = [P.startDist * Math.cos(startAngle), P.startDist * Math.sin(startAngle)];
const targetAngle = Math.random() * 2 * Math.PI;
const targetDist = Math.random() * cityRadius * 0.8;
const targetPos = [targetDist * Math.cos(targetAngle), targetDist * Math.sin(targetAngle)];
const cruiseAlt = P.cruiseMin + Math.random() * (P.cruiseMax - P.cruiseMin);
const trajectory = targetTrajectory(startPos, targetPos, cruiseAlt, P.speed, P.descentStart, P.terminalAlt);
const radarPositions = [];
for (let i = 0; i < numRadars; i++) {
const a = 2 * Math.PI * i / numRadars, d = cityRadius + 2;
radarPositions.push([d * Math.cos(a), d * Math.sin(a), 0]);
}
const dronePositions = [];
for (let i = 0; i < numDrones; i++) {
const a = 2 * Math.PI * i / numDrones, d = cityRadius + droneLine;
dronePositions.push({ x: d * Math.cos(a), y: d * Math.sin(a), z: 0, state: 'waiting', intercept: null });
}
let time = 0, detectionTime = null, launched = [], collision = false, impact = false, observations = [];
function getTruePos(t) {
let best = trajectory[0], bd = Infinity;
for (const p of trajectory) { const d = Math.abs(p.t - t); if (d < bd) { bd = d; best = p; } }
return best;
}
while (time < 1200) {
time += dt;
const tp = getTruePos(time);
const pos = [tp.x, tp.y, tp.z / 1000];
const detections = detectFromRadars(pos, radarPositions, radarRange, gaussianNoise);
if (detections.length > 0) observations.push({ x: pos[0], y: pos[1], z: pos[2] * 1000, t: time });
if (detectionTime === null && observations.length >= 3) {
detectionTime = time;
const posEst = estimatePosition3D(detections, radarPositions);
if (posEst) observations[observations.length - 1] = { x: posEst[0]/1000, y: posEst[1]/1000, z: posEst[2], t: time };
const traj = estimateTrajectory(observations);
if (traj) {
const velMs = [traj.vel[0]*1000, traj.vel[1]*1000, traj.vel[2]];
const speedEst = Math.sqrt(velMs[0]**2 + velMs[1]**2 + velMs[2]**2);
const horizDist = Math.sqrt(pos[0]**2 + pos[1]**2);
const tti = horizDist / (speedEst / 1000);
const velKs = [velMs[0]/1000, velMs[1]/1000, velMs[2]/1000];
const posM = [pos[0]*1000, pos[1]*1000, pos[2]*1000];
const cands = [];
for (let i = 0; i < numDrones; i++) {
const dp = dronePositions[i];
const dpM = [dp.x*1000, dp.y*1000, dp.z*1000];
const r = computeInterceptPoint(dpM, posM, velMs, droneSpeed);
const dist = perpDistToLine(pos, velKs, [dp.x, dp.y, dp.z]);
const viable = (r.t > 0 && r.t < tti);
cands.push({ i, t: r.t, intercept: [r.intercept[0]/1000, r.intercept[1]/1000, r.intercept[2]/1000], dist, viable });
}
const vi = cands.filter(c => c.viable).sort((a,b)=>a.dist-b.dist);
const nv = cands.filter(c=>!c.viable).sort((a,b)=>a.dist-b.dist);
const chosen = vi.slice(0,3);
for (const c of nv) { if (chosen.length >= 3) break; chosen.push(c); }
launched = chosen.map(c=>c.i);
for (const c of chosen) { dronePositions[c.i].intercept = c.intercept; dronePositions[c.i].state = 'flying'; }
if (verbose) console.log(`[detect t=${time.toFixed(1)}s] horiz=${horizDist.toFixed(1)}km speedEst=${speedEst.toFixed(0)} tti=${tti.toFixed(0)}s viable=${vi.length} chosen=${launched.join(',')} R2=${traj.r2.toFixed(3)}`);
}
}
if (detectionTime !== null && !collision && observations.length >= 2) {
const traj = estimateTrajectory(observations);
if (traj) {
const velMs = [traj.vel[0]*1000, traj.vel[1]*1000, traj.vel[2]];
const speedEst = Math.sqrt(velMs[0]**2+velMs[1]**2+velMs[2]**2);
const horizDist = Math.sqrt(pos[0]**2+pos[1]**2);
const tti = horizDist / (speedEst/1000);
if ((time - detectionTime) % 5 < dt) {
const posM = [pos[0]*1000, pos[1]*1000, pos[2]*1000];
for (const i of launched) {
const dr = dronePositions[i];
if (dr.state === 'flying') {
const dpM = [dr.x*1000, dr.y*1000, dr.z*1000];
const r = computeInterceptPoint(dpM, posM, velMs, droneSpeed);
dr.intercept = [r.intercept[0]/1000, r.intercept[1]/1000, r.intercept[2]/1000];
}
}
}
}
}
if (launched.length > 0 && !collision) {
for (const i of launched) {
const dr = dronePositions[i];
if (dr.state === 'flying' || dr.state === 'ir-tracking') {
const it = dr.intercept;
if (it) {
const d = [it[0]-dr.x, it[1]-dr.y, it[2]-dr.z];
const L = Math.sqrt(d[0]**2+d[1]**2+d[2]**2);
if (L > 0.001) { const s = droneSpeed*dt/1000; dr.x += d[0]/L*s; dr.y += d[1]/L*s; dr.z += d[2]/L*s; }
}
const dx = pos[0]-dr.x, dy = pos[1]-dr.y, dz = pos[2]-dr.z;
const dist3D = Math.sqrt(dx*dx+dy*dy+dz*dz);
if (dr.state === 'flying' && dist3D < irRange) dr.state = 'ir-tracking';
if (dist3D*1000 < 20) { collision = true; if (verbose) console.log(`[COLLISION t=${time.toFixed(1)}s drone ${i}]`); }
}
}
}
const horizDist = Math.sqrt(pos[0]**2 + pos[1]**2);
if (horizDist < cityRadius && !collision) { impact = true; if (verbose) console.log(`[IMPACT t=${time.toFixed(1)}s]`); break; }
if (collision) break;
}
return { collision, impact, detectionTime, observations: observations.length, launched: launched.length, time };
}
console.log('\n=== prop-drone verbose trials ===');
for (let k = 0; k < 3; k++) {
console.log(`--- trial ${k} ---`);
const r = runTrial('prop-drone', true);
console.log(` => collision=${r.collision} impact=${r.impact} detect=${r.detectionTime ? r.detectionTime.toFixed(1)+'s' : 'none'} obs=${r.observations} launched=${r.launched}`);
}
console.log('\n=== summary (30 trials each) ===');
for (const preset of ['shahed-136','shahed-238','prop-drone']) {
let c = 0, im = 0;
for (let k = 0; k < 30; k++) { const r = runTrial(preset, false); if (r.collision) c++; else if (r.impact) im++; }
console.log(`${preset.padEnd(14)} collision=${c}/30 (${(c/30*100).toFixed(0)}%) impact=${im}/30`);
}

1368
defense_sim.html Normal file

File diff suppressed because it is too large Load Diff

701
defense_sim.py Normal file
View File

@@ -0,0 +1,701 @@
#!/usr/bin/env python3
"""
Generalized Projectile Defense Simulation
==========================================
Models a projectile/drone attack profile and defender radar/drone
intercept with proper physics and error propagation.
Supports multiple projectile types via presets:
- Shahed-136 (Geran-2): prop, ~50 m/s, low altitude
- Shahed-238 (Geran-3): jet, ~150 m/s, 1-2 km cruise (default)
- Cruise Missile: ~300 m/s, sea-skimming
- Ballistic (Re-entry): ~800 m/s, high altitude entry
- Small UAS / Prop Drone: ~30 m/s, low altitude
Defender:
- 8 AERIS-10 radars (20 km range, ±20 m single-shot accuracy)
- 24 interceptor drones (100 m/s, IR homing within 5 km)
"""
import numpy as np
import json
import sys
# =============================================================================
# Projectile Presets
# =============================================================================
# Each preset defines the attack profile:
# start_dist: km - distance from city center at launch
# speed: m/s - cruise speed
# cruise_alt_min / cruise_alt_max: m - cruise altitude range (randomized within)
# descent_start: km - distance from target at which descent begins
# terminal_alt: m - altitude at impact
PRESETS = {
'shahed-136': {'start_dist': 30, 'speed': 50, 'cruise_alt_min': 100, 'cruise_alt_max': 300, 'descent_start': 5, 'terminal_alt': 0},
'shahed-238': {'start_dist': 35, 'speed': 150, 'cruise_alt_min': 1000, 'cruise_alt_max': 2000, 'descent_start': 8, 'terminal_alt': 200},
'cruise-missile': {'start_dist': 50, 'speed': 300, 'cruise_alt_min': 50, 'cruise_alt_max': 150, 'descent_start': 5, 'terminal_alt': 0},
'ballistic-reentry':{'start_dist': 100, 'speed': 800, 'cruise_alt_min': 60000, 'cruise_alt_max': 80000, 'descent_start': 40, 'terminal_alt': 0},
'prop-drone': {'start_dist': 15, 'speed': 30, 'cruise_alt_min': 100, 'cruise_alt_max': 300, 'descent_start': 3, 'terminal_alt': 0},
}
DEFAULT_PRESET = 'shahed-238'
def get_preset(name=None):
"""Return the preset dict for a given name, or the default."""
if name is None:
name = DEFAULT_PRESET
return PRESETS.get(name, PRESETS[DEFAULT_PRESET])
# =============================================================================
# Defender constants
# =============================================================================
# Radar parameters
RADAR_RANGE = 20000.0 # m
RADAR_ERROR_1SIGMA = 20.0 # m (single measurement, 1 sigma)
NUM_RADARS = 8
# Drone parameters
DRONE_SPEED = 100.0 # m/s
DRONE_MAX_ALT = 4000.0 # m
DRONE_DEFENSE_LINE = 8000.0 # m from city center
IR_RANGE = 5000.0 # m
IR_ERROR_1SIGMA = 3.0 # m
NUM_DRONES = 24
CITY_RADIUS = 10000.0 # m
# Simulation
DT = 0.5 # s (time step, coarser for speed)
SIM_DURATION = 400.0 # s
# =============================================================================
# Flight Profile
# =============================================================================
def target_trajectory(start_pos, target_pos, cruise_alt, speed, descent_start, terminal_alt):
"""
Generate a realistic projectile flight trajectory.
- Cruise at cruise_alt until `descent_start` distance from target
- Gradual descent to `terminal_alt` over the remaining distance
- Straight horizontal path (simplified for 2D projection)
descent_start: distance (m) from target at which descent begins
terminal_alt: altitude (m) at impact
Returns: list of (x, y, z, t) tuples
"""
start_pos = np.array(start_pos)
target_pos = np.array(target_pos)
# Horizontal direction
dx = target_pos[0] - start_pos[0]
dy = target_pos[1] - start_pos[1]
horiz_dist = np.sqrt(dx**2 + dy**2)
if horiz_dist < 1e-6:
return []
dir_x = dx / horiz_dist
dir_y = dy / horiz_dist
# Descent begins at `descent_start` distance from target
# So descent begins at (horiz_dist - descent_start) from start
descent_start_dist = max(0.0, horiz_dist - descent_start)
# Descent distance (clamped to available distance)
descent_dist = max(0.0, min(descent_start, horiz_dist))
# Total duration
total_duration = horiz_dist / speed
trajectory = []
t = 0.0
while t <= total_duration + DT:
# Distance traveled
dist_traveled = speed * t
# Determine altitude
if descent_dist <= 0 or dist_traveled < descent_start_dist:
# Cruise phase
z = cruise_alt
else:
# Descent phase: linear interpolation from cruise_alt to terminal_alt
descent_progress = min(1.0, (dist_traveled - descent_start_dist) / descent_dist)
z = cruise_alt + (terminal_alt - cruise_alt) * descent_progress
# Horizontal position
x = start_pos[0] + dir_x * dist_traveled
y = start_pos[1] + dir_y * dist_traveled
trajectory.append((x, y, z, t))
t += DT
return trajectory
# =============================================================================
# Radar Detection Model
# =============================================================================
def radar_positions(num_radars, city_radius):
"""Generate radar positions on a circle around the city."""
positions = []
for i in range(num_radars):
angle = 2 * np.pi * i / num_radars
dist = city_radius + 2000 # 2 km outside city
x = dist * np.cos(angle)
y = dist * np.sin(angle)
positions.append(np.array([x, y, 0.0]))
return positions
def drone_positions(num_drones, city_radius, defense_line):
"""Generate drone positions on a circle around the city."""
positions = []
for i in range(num_drones):
angle = 2 * np.pi * i / num_drones
dist = city_radius + defense_line
x = dist * np.cos(angle)
y = dist * np.sin(angle)
positions.append({
'pos': np.array([x, y, 0.0]),
'state': 'waiting',
'index': i
})
return positions
def detect_from_radars(pos, radar_pos_list, radar_range, rng):
"""
Detect target position from all radars in range.
Returns list of (range, azimuth, elevation, radar_index) and estimated 3D position.
"""
detections = []
for i, rpos in enumerate(radar_pos_list):
dist = np.linalg.norm(pos - rpos)
if dist < radar_range:
# Add measurement noise
range_meas = dist + rng.normal(0, RADAR_ERROR_1SIGMA)
azimuth_true = np.arctan2(pos[1] - rpos[1], pos[0] - rpos[0])
elevation_true = np.arctan2(pos[2] - rpos[2], np.sqrt((pos[0]-rpos[0])**2 + (pos[1]-rpos[1])**2))
azimuth_meas = azimuth_true + rng.normal(0, RADAR_ERROR_1SIGMA / max(100, dist))
elevation_meas = elevation_true + rng.normal(0, RADAR_ERROR_1SIGMA / max(100, dist))
detections.append({
'range': range_meas,
'azimuth': azimuth_meas,
'elevation': elevation_meas,
'radar_index': i
})
return detections
def estimate_position_3d(detections, radar_pos_list, rng):
"""
Estimate 3D position from multiple radar detections.
Each radar provides azimuth, elevation, and range, which together define a 3D point.
The estimated position is the least-squares fit of these 3D point estimates.
Returns (x, y, z) estimate and covariance.
"""
if len(detections) < 2:
return None, None
# Each radar's bearing + range defines a 3D point estimate
estimates = []
for det in detections:
ri = radar_pos_list[det['radar_index']]
az = det['azimuth']
el = det['elevation']
r = det['range']
# Convert bearing + range to 3D offset from radar
dx = r * np.cos(el) * np.cos(az)
dy = r * np.cos(el) * np.sin(az)
dz = r * np.sin(el)
p_est = ri + np.array([dx, dy, dz])
estimates.append(p_est)
# Least-squares estimate is the mean of the point estimates
p = np.mean(estimates, axis=0)
# Estimate covariance from the spread of estimates
if len(estimates) > 1:
spread = np.std(np.array(estimates), axis=0)
cov = np.diag(spread**2)
else:
sigma_pos = RADAR_ERROR_1SIGMA
cov = np.eye(3) * sigma_pos**2
return p, cov
# =============================================================================
# Trajectory Estimation (Linear Regression)
# =============================================================================
def estimate_trajectory(observations, rng=None):
"""
Estimate trajectory (velocity) from a series of position observations.
Each observation is (x, y, z, t).
Returns (vx, vy, vz) and covariance.
"""
if len(observations) < 2:
return None, None
times = np.array([o[3] for o in observations])
xs = np.array([o[0] for o in observations])
ys = np.array([o[1] for o in observations])
zs = np.array([o[2] for o in observations])
n = len(observations)
# Linear regression for each axis: pos = pos0 + vel * t
vel = np.zeros(3)
pos0 = np.zeros(3)
for axis in range(3):
pos = np.array([o[axis] for o in observations])
if n >= 2:
# Least squares fit: pos = pos0 + vel*t
A = np.vstack([np.ones(n), times]).T
coeffs, _, _, _ = np.linalg.lstsq(A, pos, rcond=None)
pos0[axis] = coeffs[0]
vel[axis] = coeffs[1]
# Covariance estimate
# For linear regression, Var(v) = sigma^2 / Sxx where Sxx = sum((t - t_mean)^2)
t_mean = np.mean(times)
Sxx = np.sum((times - t_mean)**2)
sigma_vel = RADAR_ERROR_1SIGMA / np.sqrt(Sxx) if Sxx > 0 else RADAR_ERROR_1SIGMA
cov = np.eye(3) * sigma_vel**2
return vel, cov
# =============================================================================
# Drone Intercept Logic
# =============================================================================
def compute_intercept_point(drone_pos, target_pos, target_vel, drone_speed):
"""
Compute the intercept point for a drone chasing a moving target in 3D.
Solves the classic pursuer-target problem:
|drone_pos + v_d*t - target_pos - v_s*t| = 0
where v_d = drone_speed * unit_vector(direction to intercept)
This works even when the drone is slower than the target,
as long as the drone is positioned close enough to the intercept path.
Returns: intercept point (3D) and estimated time
"""
drone_pos = np.array(drone_pos, dtype=float)
target_pos = np.array(target_pos, dtype=float)
target_vel = np.array(target_vel, dtype=float)
# Relative position and velocity
rel_pos = target_pos - drone_pos # where target is relative to drone
rel_vel = target_vel # relative velocity (drone direction is variable)
# We need to find a direction d (unit vector) and time t such that:
# drone_speed * t * d = rel_pos + target_vel * t
# => d = (rel_pos + target_vel * t) / (drone_speed * t)
# And |d| = 1, so:
# |rel_pos + target_vel * t| = drone_speed * t
# Let's expand:
# |rel_pos + v_s*t|^2 = v_d^2 * t^2
# |rel_pos|^2 + 2*t*(rel_pos·v_s) + t^2*(v_s·v_s) = v_d^2 * t^2
# |rel_pos|^2 + 2*t*(rel_pos·v_s) + t^2*(|v_s|^2 - v_d^2) = 0
a = np.dot(target_vel, target_vel) - drone_speed**2
b = 2 * np.dot(rel_pos, target_vel)
c = np.dot(rel_pos, rel_pos)
# Solve quadratic: a*t^2 + b*t + c = 0
discriminant = b*b - 4*a*c
if a == 0:
# Linear equation: b*t + c = 0
if abs(b) < 1e-10:
# No solution
t = np.linalg.norm(rel_pos) / drone_speed
intercept = drone_pos + rel_pos
return intercept, t
t = -c / b
if t <= 0:
t = np.linalg.norm(rel_pos) / drone_speed
intercept = drone_pos + rel_pos
return intercept, t
target_future = target_pos + target_vel * t
intercept = target_future
return intercept, t
if discriminant < 0:
# No intercept possible with this geometry
# Fallback: aim at current position
t = np.linalg.norm(rel_pos) / drone_speed
intercept = drone_pos + rel_pos
return intercept, t
# Compute both roots, pick the smallest positive one
sqrt_disc = np.sqrt(discriminant)
t_candidates = [
(-b + sqrt_disc) / (2 * a),
(-b - sqrt_disc) / (2 * a)
]
positive_roots = [t for t in t_candidates if t > 0]
if not positive_roots:
# No valid positive intercept
t = np.linalg.norm(rel_pos) / drone_speed
intercept = drone_pos + rel_pos
return intercept, t
# The smallest positive root is the earliest intercept
t = min(positive_roots)
# Intercept point: where target will be at time t
target_future = target_pos + target_vel * t
intercept = target_future
return intercept, t
def drone_trajectory(drone_start, intercept_point, drone_speed):
"""
Generate a smooth 3D trajectory for the drone from start to intercept.
Simple: linear interpolation with altitude management.
"""
drone_start = np.array(drone_start)
intercept = np.array(intercept_point)
total_dist = np.linalg.norm(intercept - drone_start)
duration = total_dist / drone_speed
# Generate waypoints
num_steps = max(2, int(duration / DT))
trajectory = []
for i in range(num_steps + 1):
frac = i / num_steps
pos = drone_start + frac * (intercept - drone_start)
t = i * DT
trajectory.append((pos[0], pos[1], pos[2], t))
return trajectory
# =============================================================================
# Main Simulation
# =============================================================================
def run_simulation(seed=None, debug=False, preset=None):
"""Run one full simulation and return results.
preset: name of a projectile preset (or None for default).
"""
rng = np.random.default_rng(seed)
preset_name = preset if preset is not None else DEFAULT_PRESET
preset_data = get_preset(preset)
# Attack parameters from preset (with some randomization)
start_dist = preset_data['start_dist'] * 1000.0 * rng.uniform(0.9, 1.1) # m, ±10%
start_angle = rng.uniform(0, 2 * np.pi)
target_angle = rng.uniform(0, 2 * np.pi)
target_dist = rng.uniform(0, CITY_RADIUS * 0.8)
start_pos = np.array([
start_dist * np.cos(start_angle),
start_dist * np.sin(start_angle),
0
])
terminal_alt = preset_data['terminal_alt']
target_pos = np.array([
target_dist * np.cos(target_angle),
target_dist * np.sin(target_angle),
terminal_alt # terminal altitude
])
cruise_alt = rng.uniform(preset_data['cruise_alt_min'], preset_data['cruise_alt_max'])
speed = preset_data['speed']
descent_start = preset_data['descent_start'] * 1000.0 # m
if debug:
print(f" [DEBUG] preset={preset_data} start_dist={start_dist:.0f}m, cruise_alt={cruise_alt:.0f}m")
print(f" [DEBUG] start_pos={start_pos}, target_pos={target_pos}")
# Generate trajectory
trajectory = target_trajectory(start_pos, target_pos, cruise_alt, speed, descent_start, terminal_alt)
if not trajectory:
return None
# Setup radar and drones
radar_pos_list = radar_positions(NUM_RADARS, CITY_RADIUS)
drone_pos_list = drone_positions(NUM_DRONES, CITY_RADIUS, DRONE_DEFENSE_LINE)
# Simulation state
time = 0.0
detection_time = None
launched_drones = []
collision = False
observations = []
# Pre-compute true target position at each time step
true_positions = {t: np.array([x, y, z]) for x, y, z, t in trajectory}
def get_true_pos(t):
# Find closest trajectory point
times = sorted(true_positions.keys())
# Binary search for closest
idx = np.searchsorted(times, t)
if idx >= len(times):
return true_positions[times[-1]]
if idx == 0:
return true_positions[times[0]]
# Choose closer of two adjacent
t1, t2 = times[idx-1], times[idx]
if abs(t - t1) < abs(t - t2):
return true_positions[t1]
return true_positions[t2]
# Main loop
while time < SIM_DURATION and not collision:
true_pos = get_true_pos(time)
# Check radar detection
detections = detect_from_radars(true_pos, radar_pos_list, RADAR_RANGE, rng)
active_radars = [d['radar_index'] for d in detections]
if active_radars:
observations.append((true_pos[0], true_pos[1], true_pos[2], time))
# First detection triggers alert
if detection_time is None and len(observations) >= 3:
detection_time = time
if debug:
print(f" [DEBUG] Detection at t={time:.1f}s, true_pos={true_pos}")
# Estimate 3D position (only once at detection)
pos_est, cov = estimate_position_3d(detections, radar_pos_list, rng)
if pos_est is not None:
observations[-1] = (pos_est[0], pos_est[1], pos_est[2], time)
if debug:
print(f" [DEBUG] pos_est={pos_est}, true={true_pos}, err={np.linalg.norm(pos_est-true_pos):.1f}m")
# Estimate trajectory (only once at detection)
vel_est, vel_cov = estimate_trajectory(observations, rng)
if vel_est is not None:
if debug:
print(f" [DEBUG] vel_est={vel_est}, true_vel approx={speed} m/s")
# Compute time to impact for this trajectory
# Target distance to city center
target_horiz_dist = np.sqrt(true_pos[0]**2 + true_pos[1]**2)
speed_est = np.linalg.norm(vel_est)
t_to_impact = target_horiz_dist / speed_est if speed_est > 0 else float('inf')
if debug:
print(f" [DEBUG] t_to_impact={t_to_impact:.1f}s, target_horiz_dist={target_horiz_dist:.0f}m")
# Launch drones that can intercept before impact
drone_intercepts = []
for i in range(NUM_DRONES):
drone_pos = drone_pos_list[i]['pos']
intercept, t_int = compute_intercept_point(
drone_pos, true_pos, vel_est, DRONE_SPEED
)
# Only viable if intercept happens before impact
if t_int < t_to_impact:
drone_intercepts.append((t_int, i, intercept))
drone_intercepts.sort(key=lambda x: x[0])
launched_drones = [d[1] for d in drone_intercepts[:3]]
if debug:
print(f" [DEBUG] viable drones: {len(drone_intercepts)}, launched: {launched_drones}")
for d in drone_intercepts[:3]:
print(f" [DEBUG] drone {d[1]} t_int={d[0]:.1f}s intercept=({d[2][0]:.0f},{d[2][1]:.0f},{d[2][2]:.0f})")
# Compute intercept points
for d in drone_intercepts[:3]:
i = d[1]
drone_pos_list[i]['intercept'] = d[2]
drone_pos_list[i]['state'] = 'flying'
# Update drones
if launched_drones:
# Re-target drones periodically (proportional navigation)
if detection_time is not None and (time - detection_time) % 5.0 < DT:
vel_est_new, _ = estimate_trajectory(observations, rng)
if vel_est_new is not None:
# Recompute intercept points for launched drones
for i in launched_drones:
drone = drone_pos_list[i]
if drone['state'] in ('flying', 'ir_tracking'):
intercept, t_int = compute_intercept_point(
drone['pos'], true_pos, vel_est_new, DRONE_SPEED
)
drone['intercept'] = intercept
for i in launched_drones:
drone = drone_pos_list[i]
if drone['state'] in ('flying', 'ir_tracking'):
intercept = drone.get('intercept')
if intercept is not None:
delta = intercept - drone['pos']
dist = np.linalg.norm(delta)
if dist > 0.01:
step = DRONE_SPEED * DT
drone['pos'] = drone['pos'] + (delta / dist) * step
else:
drone['pos'] = intercept
# Check IR range
target_dist_local = np.linalg.norm(true_pos - drone['pos'])
if drone['state'] == 'flying' and target_dist_local < IR_RANGE:
drone['state'] = 'ir_tracking'
if debug:
print(f" [DEBUG] t={time:.1f}s drone {i} -> IR tracking, dist={target_dist_local:.0f}m")
# Check collision
if target_dist_local < 20.0: # 20 m kill radius
collision = True
if debug:
print(f" [DEBUG] COLLISION at t={time:.1f}s drone {i}")
break
elif debug and time % 10 < DT:
print(f" [DEBUG] t={time:.1f}s drone {i} pos=({drone['pos'][0]:.0f},{drone['pos'][1]:.0f},{drone['pos'][2]:.0f}), target=({true_pos[0]:.0f},{true_pos[1]:.0f},{true_pos[2]:.0f}), dist={target_dist_local:.0f}m")
# Check if target reached city
horiz_dist = np.sqrt(true_pos[0]**2 + true_pos[1]**2)
if horiz_dist < CITY_RADIUS and not collision:
break
time += DT
# Calculate results
result = {
'preset': preset_name,
'start_dist': start_dist,
'cruise_alt': cruise_alt,
'speed': speed,
'terminal_alt': terminal_alt,
'detection_time': detection_time,
'collision': collision,
'time_of_collision': time if collision else None,
'time_of_impact': time if not collision else None,
'num_observations': len(observations),
'launched_drones': launched_drones,
'seed': seed
}
return result
# =============================================================================
# Monte Carlo Validation
# =============================================================================
def monte_carlo(n_trials=100, seed_base=42, preset=None):
"""Run Monte Carlo simulation and return statistics.
preset: name of a projectile preset (or None for default).
"""
results = []
for i in range(n_trials):
result = run_simulation(seed=seed_base + i, preset=preset)
if result is not None:
results.append(result)
if not results:
return None
# Statistics
collisions = sum(1 for r in results if r['collision'])
detection_times = [r['detection_time'] for r in results if r['detection_time'] is not None]
collision_times = [r['time_of_collision'] for r in results if r['collision']]
impact_times = [r['time_of_impact'] for r in results if not r['collision']]
num_obs = [r['num_observations'] for r in results]
cruise_alts = [r['cruise_alt'] for r in results]
preset_name = results[0]['preset']
stats = {
'preset': preset_name,
'total_trials': len(results),
'collisions': collisions,
'interception_rate': collisions / len(results) * 100,
'avg_detection_time': np.mean(detection_times) if detection_times else None,
'avg_collision_time': np.mean(collision_times) if collision_times else None,
'avg_impact_time': np.mean(impact_times) if impact_times else None,
'avg_observations': np.mean(num_obs),
'min_observations': np.min(num_obs),
'max_observations': np.max(num_obs),
'avg_cruise_alt': np.mean(cruise_alts),
'min_cruise_alt': np.min(cruise_alts),
'max_cruise_alt': np.max(cruise_alts),
}
return stats, results
# =============================================================================
# Main
# =============================================================================
if __name__ == '__main__':
print("=" * 80)
print("Generalized Projectile Defense Simulation")
print("=" * 80)
print()
# Run single simulation for demo (default preset)
print(f"Running single simulation (preset={DEFAULT_PRESET})...")
result = run_simulation(seed=42)
if result:
print(f" Preset: {result['preset']}")
print(f" Start distance: {result['start_dist']/1000:.1f} km")
print(f" Speed: {result['speed']:.0f} m/s")
print(f" Cruise altitude: {result['cruise_alt']:.0f} m")
print(f" Detection time: {result['detection_time']:.1f} s" if result['detection_time'] else " Detection time: N/A")
print(f" Collision: {'YES' if result['collision'] else 'NO'}")
if result['collision']:
print(f" Collision time: {result['time_of_collision']:.1f} s")
else:
print(f" Impact time: {result['time_of_impact']:.1f} s")
print(f" Observations: {result['num_observations']}")
print()
# Run Monte Carlo for each preset
print("Running Monte Carlo (100 trials per preset)...")
all_stats = {}
for preset_name in PRESETS.keys():
stats, results = monte_carlo(n_trials=100, seed_base=100, preset=preset_name)
if stats:
all_stats[preset_name] = stats
print(f" [{preset_name}]")
print(f" Interception rate: {stats['interception_rate']:.1f}%")
if stats['avg_detection_time']:
print(f" Avg detection time: {stats['avg_detection_time']:.1f} s")
if stats['avg_collision_time']:
print(f" Avg collision time: {stats['avg_collision_time']:.1f} s")
if stats['avg_impact_time']:
print(f" Avg impact time: {stats['avg_impact_time']:.1f} s")
print(f" Avg cruise altitude: {stats['avg_cruise_alt']:.0f} m")
print()
# Save stats to JSON for the HTML viewer (per preset)
import json
json_stats = {
preset_name: {
'total_trials': stats['total_trials'],
'collisions': stats['collisions'],
'interception_rate': stats['interception_rate'],
'avg_detection_time': stats['avg_detection_time'],
'avg_collision_time': stats['avg_collision_time'],
'avg_impact_time': stats['avg_impact_time'],
'avg_observations': stats['avg_observations'],
}
for preset_name, stats in all_stats.items()
}
with open('montecarlo_stats.json', 'w') as f:
json.dump(json_stats, f, indent=2)
print()
print("Stats saved to montecarlo_stats.json")
print()
print("=" * 80)
print("Simulation complete")
print("=" * 80)

5
defense_sim_v2/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.venv/
__pycache__/
*.py[cod]
*.egg-info/
.pytest_cache/

View File

@@ -0,0 +1,105 @@
# Defense Sim V2: Project Specification
Generic preset extension: `projectile_preset` selects `custom`, `preset_a`, `preset_b`, `preset_c`, or uniform `random` selection. A/B/C are synthetic unpowered point-mass demonstrations (high-drag drop, dense drop, lofted launch); they are not calibrated vehicle profiles. Backend sampling determines initial conditions and physical parameters once per run. `scenario.projectile_profile` records realized values and `scenario.projectile_preset_requested` records the selection mode. Existing gravity, quadratic drag relative to wind, RK4 integration, and terrain contact determine subsequent motion. No preset supplies forced cruise velocity or a prescribed descent trajectory.
Status: authoritative initial backend specification. This specification follows source inspection of all three requested repositories; it will be revised when validation or domain requirements expose a conflict.
## 1. Goals
Build a backend-authoritative, reproducible, configurable physical simulation of projectiles and multirotor/aircraft entities, with noisy observation and control, environmental effects, interception events, Monte Carlo results, and a transport-independent state stream. The system must preserve ground truth separately from observations and expose enough state for a later renderer to draw entities and trajectories without calculating physics.
## 2. Non-goals
No client-side simulation, combat doctrine, operational deployment, or claim of flight-qualified fidelity. A Vue 3 + Vite bootstrap and telemetry console have since been added under `frontend/`; a full 2D/3D visualization remains outside this baseline until renderer, map, and camera requirements are supplied. This is a research/engineering simulation framework with explicit model limits, not a certified 6-DOF aerodynamic model of any real vehicle.
## 3. Architecture and backend responsibilities
Python package `defense_sim` under `src/`. Simulation code owns configuration, random generators, state, physics, environment, sensing, guidance, event detection, raw runs, and statistics. Socket.IO is an adapter that publishes engine snapshots/events; it must not mutate or calculate simulation reality. Pure model modules must be usable without the server.
Proposed modules: `simulation/{engine,state,scenarios}`, `physics/{vector,integrator,atmosphere,environment,turbulence}`, `entities/{projectile,drone}`, `sensors/{models,tracking}`, `guidance/{controllers,interception}`, `statistics/{metrics,monte_carlo}`, `server/{socketio_server,events}`. Shared immutable configuration dataclasses and explicit SI units are preferred over per-entity ad hoc constants.
## 4. Coordinate frames and state
Use SI units and a world NED frame: x north, y east, z down; altitude is `-z`. Body-frame quantities are marked as such. Ground-truth rigid-body state includes position, velocity, acceleration, quaternion orientation `[w,x,y,z]`, body angular rates, and actuator state. Observations and estimates are separate timestamped values with covariance/error metadata. Quaternion normalization occurs after integration. Events and serialized vectors include frame and units in schema/version metadata.
## 5. Projectile model
Point-mass translational equations: `p_dot=v`; `v_dot=g + (F_thrust + F_aero)/m`. Drag is opposite relative air velocity and initially quadratic, `F_d=-0.5*rho*Cd*A*|v_air|*v_air`; mass, area, drag coefficient, gravity, launch state, optional thrust curve, and uncertainty distributions are configuration. A launch command initializes one sampled initial condition; no later velocity or position edits are permitted. Ground/impact and interception are continuous within-step events, refined by interpolation/root finding. Projectile attitude is omitted unless a later body/aero model needs it.
Reference finding: `georgeh1ll/Projectile-Motion-Simulator` contains `simulate_projectile_motion`, which decomposes launch speed into x/y components and advances position at 0.01 s with constant gravity. It is useful as a basic vacuum trajectory sanity check and launch-angle UI concept. It has no drag, atmosphere, uncertainty, or collision model and no LICENSE file in the cloned revision; no code is copied. Correct public URL is `https://github.com/georgeh1ll/Projectile-Motion-Simulator` (the supplied `...Simulatoryou` URL does not resolve).
## 6. Attack drone and defense drone models
Both drone classes use the same physical dynamics and differ only in configuration, mission/guidance policy, and sensors. Initial multirotor model: rigid body with mass, diagonal inertia (configurable), quaternion attitude, motor thrust/torque lag, rotor thrust and torque limits, gravity, translational aerodynamic drag, and moments; actuator commands pass through delay, lag, saturation, and noise before forces/moments. Position/velocity targets become bounded acceleration/attitude/thrust commands through a cascaded controller, never direct state assignments. Rate, acceleration, speed, tilt, climb/descent, and motor limits are explicit. Fixed-wing configuration may later use aerodynamic coefficients and control surfaces; it is a separate model, not a hidden approximation.
Reference finding: `riverjiang187/PyFlightSim` source has a 13-element rigid-body state (NED position, body velocity, quaternion, body rates), Newton-Euler force/moment dynamics including gravity and gyroscopic terms, quaternion kinematics, RK4, configurable aerodynamic coefficients, cascaded PID autopilot, ISA, Dryden gust shaping, and IMU/GPS/air-data/estimator modules. We adapt the separations, state representation, RK4 and quaternion approach, atmosphere, turbulence concepts, and bounded cascaded control. Its source is MIT licensed (Copyright Haiang Jiang, 2026); no code is copied in this initial implementation, so attribution is documentation only. Its model is fixed-wing oriented, has simplified diagonal inertia/aerodynamics, and must be independently checked before reuse.
Reference finding: `nb-programmer/dronesim` source separates physics engine, actor, action/control interface, and sensors; `SimpleUAVDronePhysics` maps RC commands through velocity/altitude/yaw PID-like controls to thrust and includes operation states. Its implementation mixes centimeter-like implicit units, per-tick motion, direct angle/velocity updates and simple multiplicative drag; sensor base and IMU are mostly stubs. We adapt the command/actuator/interface separation and lifecycle ideas, not its dynamics or numeric constants. MIT license (Copyright imaprogrammer, 2022); no code is copied in this initial implementation.
## 7. Environment, atmosphere and turbulence
Environment configuration provides altitude-aware ISA temperature, pressure, density, speed of sound, and dynamic viscosity, with documented valid altitude range and configurable deviations. Wind is a deterministic configured vector/field plus optional seeded variation. Air-relative velocity drives aerodynamic loads. Dryden turbulence is a stateful stochastic filter driven by the run RNG and configured intensity/scale lengths; gust velocity changes air-relative force calculations. It is never applied as visual-only position noise. Wind/turbulence update cadence is distinct from physics integration cadence.
## 8. Sensors, tracking, uncertainty and randomness
Each run owns a seeded generator; entity-specific generators use stable seed derivation so call ordering in unrelated entities does not alter the result. Noise configuration supports bias, standard deviation, correlation/AR processes, quantization, latency, missed observations, and actuator variation. Actual-minus-estimated errors are reported as vectors and norms, with sensor, tracking, guidance, and actuator error kept distinct. Controllers consume estimates only. A deterministic mode disables stochastic terms but retains explicitly configured biases/delays.
## 9. Guidance and control
Guidance consumes timestamped track estimates. Commands pass through configurable command latency, controller, actuator dynamics/saturation, and then physical force/moment evaluation. The baseline controller is bounded position/velocity-to-acceleration control for multirotors; interceptor guidance is bounded pursuit/lead guidance using estimated target state and configured speed/turn/acceleration limits. No prediction is written into physical position. Record command, actuator output, and tracking/control error.
## 10. Interception
Interception is a swept relative-motion sphere test across each integration interval, with event time/position refined within the step. A hit occurs when separation is within the configured combined collision radius while both entities are active. Record IDs, time, interpolated positions/velocities, miss distance, estimated time-to-intercept, and relevant observation/guidance errors. Closest approach is measured for misses. There is no magical proximity event or teleportation.
### City-circle defender activation
For V1 compatibility, a configurable horizontal city circle defaults to 10 km radius and center `(0,0)` in NED x/y. For each incoming projectile, the backend rolls out its noisy track through the configured projectile/environment model and finds the first inward crossing of that circle. It ranks available defense drones by 3D distance from their current positions to the crossing point and activates up to the three nearest. Selection uses the estimated track for the crossing, not hidden true projectile state. The activation event records the crossing and selected IDs. This replaces selection based only on shortest predicted intercept time.
## 11. Simulation loop and integration
Fixed-step simulation clock. At each physics step: update scheduled environment stochastic states; deliver due sensor samples and update tracks; update guidance/control on their own configured rates; advance actuator states; integrate all coupled physical entities with RK4 or a justified method; normalize attitude; find ground/collision/interception events over the step; update run metrics; publish immutable snapshots at an independent output cadence. Step ordering and event queue use simulation time only. RK stages evaluate forces from stage state and the same held actuator/environment sample so stochastic noise is not redrawn within a single step.
## 12. Monte Carlo and statistics
Scenario configuration is data. `run_many` derives one reproducible child seed per run and retains configuration, seed, final/event summaries, and optional sampled trajectories. Aggregation reports run count; hit/interception probability; miss probability and distance mean/median/std/min/max; interception-time mean/median; detection/track timing; and error summaries. Results are grouped by projectile ID/type under `interception_by_projectile` and are transport-neutral. Raw runs remain serializable so aggregates can be recomputed.
## 13. Backend event/state schema
Every packet has `schema_version`, `run_id`, `sequence`, `simulation_time`, and `kind`. A state packet includes entity identity/type/status, position, velocity, acceleration, quaternion, angular velocity, actuator/control summary, and trajectory history or incremental trajectory samples; environment includes atmosphere/wind/turbulence; observations/tracks include estimate, timestamp, uncertainty and ground-truth error only in simulation/debug output; statistics are structured sections `overall`, `scenario`, `timing`, `error`, `interception_by_projectile`. Event packets include typed collision/interception/impact/detection/error events. JSON values use finite numbers only. Socket.IO event names are defined in `server/events.py`; clients subscribe to `simulation_state`, `simulation_event`, `simulation_statistics`, and `simulation_error`. Streaming can throttle/coalesce snapshots without affecting physics.
## 14. Configuration
Validated dataclasses (and JSON/YAML serialization at the boundary) define integration/control/sensor/stream rates, environment, body/motor/projectile parameters, uncertainty distributions, scenario entities, collision radii, and seed. Invalid units, negative mass, nonpositive timestep, and inconsistent limits fail before a run. Defaults are educational example values, never described as representative of deployed equipment.
## 15. Determinism and replay
Same software version, configuration, seed, step schedule, and platform floating-point behavior must reproduce results within documented numerical tolerance. RNG state/seed and configuration hash are kept in raw run metadata. No wall clock drives physics. Socket client timing cannot affect engine time.
## 16. Validation strategy
Tests cover analytical vacuum projectile motion; quadratic drag energy/speed behavior; ISA anchor values and monotonicity; RK4 convergence and constant acceleration; quaternion normalization; force/actuator bounds; drone turn/acceleration/altitude constraints; sensor error and seed reproducibility; Dryden finite output/statistical bounds; swept collision/interception edge cases; estimation error signs; Monte Carlo aggregate recomputation; event/state serialization. Golden analytic solutions and physically reasonable bounds are preferred to self-referential expected values. CI will run unit tests and a small performance smoke benchmark. These tests are planned deliverables; do not claim fidelity beyond tested envelopes.
## 17. Performance
Physics, controller, navigation sensor, radar scan, wireless link, and state-stream rates are distinct simulation-time schedules. The normal operator workflow does not expose integrator steps or random seeds: a run creates and records its own seed, and ends at physical interception/ground impact. Physics rate and model timing remain backend configuration. Snapshots are streamed independently at 20 Hz and immediately on new events. Physics benchmarks remain necessary for larger entity counts and Monte Carlo; networking must not change fixed simulation time.
## 18. Transport architecture
`python-socketio` ASGI server adapts the engine's async snapshot/event stream. It supports starting/stopping runs, state subscription, and server-side errors; it accepts scenario and device configuration but does not accept client-authored entity state or compute physics. Engine APIs work without Socket.IO. CORS is configurable through `SIM_ALLOWED_ORIGINS`. The Vue client uses `socket.io-client` to submit scenario/device configuration and render received state/events.
## 19. Known limitations
Initial core is a configurable engineering approximation. ISA layering, simplified turbulence, rigid-body multirotor rotor model, aerodynamic coefficients, sensor covariance, guidance and ground interaction require independent calibration. No real vehicle validation data is currently supplied. Ground collision uses a flat datum. Interception models geometric contact, not blast/fragment effects. Python floating-point determinism is not guaranteed bit-for-bit across platforms.
### Device timing and link assumptions
The AERIS-10 profile is based on repository firmware in `9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp`. With 16 chirps at PRI1=167 μs and PRI2=175 μs, a 175.4 μs guard, three phase patterns for each of 15 elevation positions, and four 1 ms stepper pulses, one azimuth frame is 258.133 ms. Fifty azimuth positions imply a 12.90665 s same-direction revisit. The model checks a 50-position azimuth sector and a 31-beam elevation fan; finite horizontal sector width (7.2° per mechanical step) and 3° elevation spacing are approximations pending measured antenna beam patterns. AERIS-10N/10X maximum range is configured as 3 km/20 km from the root README.
The repository's `simulate_trajectory_precision.py` uses 10 Hz observations, 2 m range sigma, 0.5° azimuth/elevation sigma, 1 m/s Doppler sigma, and 7.1 ms fixed latency. Those values are adopted as R&D sensor/error and processing-delay assumptions, not verified AERIS-10 performance. The firmware-derived scan cadence supersedes that script's uniform 10 Hz update assumption for radar detections.
The drone navigation sensor defaults to 10 Hz with 2 m position sigma, 0.4 m/s velocity sigma, and 10 ms onboard processing. Flight control defaults to 50 Hz; physics integrates at 100 Hz; actuator lag uses the existing 0.25 s time constant. Drone command and telemetry share a configurable, half-duplex, serialized wireless R&D link: 20 Hz send slots, 250 kbit/s, 64-byte telemetry / 48-byte command payloads, 24-byte framing overhead, 50 ms one-way delay, 5 ms Gaussian jitter, 10 ms command computation, and zero packet loss. These are explicit baselines for field tuning, not claims about a selected drone radio. Wired radar-to-control reporting uses the 7.1 ms fixed-delay assumption. Link queueing and airtime are added to propagation latency.
## 20. Frontend bootstrap and remaining requirements
`frontend/` is bootstrapped from `git@git.naori.cloud:thebadking/vue-template.git` (Vue 3, Vite, TypeScript, UnoCSS, Supabase Auth/storage). It includes Socket.IO scenario configuration, Three.js orbit/zoom rendering, backend-provided trajectories, a fixed-height terminal telemetry/event log with scrollback, and a Supabase `simulation_runs` migration with owner-scoped RLS. The client renders backend state without local physics or prediction. Keep `Interception Stats by Projectile` below other statistics when Monte Carlo results are exposed. Terrain, antenna patterns, drone airframe models, field-calibrated device profiles, and additional detailed renderer requirements remain future work.

53
defense_sim_v2/README.md Normal file
View File

@@ -0,0 +1,53 @@
# Defense Sim V2
Real-world physics simulation with backend-owned state, dynamics, sensing, device timing, random variation, interception events, and statistics. The Vue 3 client is an operator-configuration and visualization layer. Three.js renders backend-provided state and trajectory samples; it does not calculate physics or predictions.
## Quick start
```bash
cd defense_sim_v2
python -m pip install -e '.[server,dev]'
python -m defense_sim.cli --steps 500 --seed 7
```
The backend provides SI physics, an RK4 point-mass projectile, configurable environment and seeded Dryden gusts, actuator-limited drone point-mass dynamics, noisy observations, swept interception checks, run aggregation, JSON snapshots, and Socket.IO transport. Threat detection uses AERIS-10 hardware timing and measurement configuration. Drone navigation, control, wireless telemetry, command delivery, and actuator response have separate update schedules and latencies. The backend predicts each threat's first inward city-circle crossing and selects the three available defense drones nearest that point using positions received by ground control.
All coordinates use NED world axes (m, m/s, m/s²; altitude is negative down). Fixed simulation time never follows wall-clock/network timing.
## Generic projectile presets
The selector follows the previous preset interaction with synthetic unpowered objects. A is a high-drag drop (10–20 m/s, 30–60 m altitude); B is a dense drop (25–45 m/s, 60–100 m); C is a lofted launch (45–65 m/s, 10–20 m, 35–55° elevation). These arbitrary physics demonstrations can hit terrain before reaching the protected area.
`projectile_preset` accepts `custom`, `preset_a`, `preset_b`, `preset_c`, or `random`. Random mode selects uniformly from A/B/C once per run. Backend sampling varies initial speed, altitude, elevation, mass, area, and drag coefficient within documented bounds in `simulation/presets.py`. RK4 integrates gravity and wind-relative drag without enforcing cruise speed. The requested selection, realized parameters, and seed are stored in `scenario` for replay; the UI renders the realized values.
## Device timing defaults
The AERIS-10 scan sequence derives from `9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp`: 16 chirps in each long/short train, PRI values of 167/175 μs, a 175.4 μs guard, three phase patterns at each of 15 elevation positions, four 1 ms stepper pulses, and 50 azimuth positions. The model yields about 258.133 ms per azimuth frame and 12.90665 s per full azimuth revisit. AERIS-10N and AERIS-10X range limits (3 km and 20 km) follow the repository hardware README. Range/bearing/Doppler uncertainty and the 7.1 ms fixed processing delay follow `simulate_trajectory_precision.py`; these are model inputs, not bench-validated performance claims.
Drone navigation defaults to 10 Hz, flight control to 50 Hz, and actuator acceleration response to a 0.25 s time constant. The wireless drone link is an explicit, tunable R&D assumption: 20 Hz send slots, 250 kbit/s shared half-duplex channel, 64-byte telemetry and 48-byte command payloads plus 24 bytes framing, 50 ms one-way delay, 5 ms latency jitter, 10 ms command computation, and 0% packet loss. Packets are serialized, with airtime and queueing included. Radar reporting is modeled as wired with the configured 7.1 ms fixed delay. Drone-link defaults are not specifications for a selected radio and should be tuned during field R&D.
## Layout and frontend
`src/defense_sim` contains transport-independent simulation modules. The `server` package only adapts simulation state and events to Socket.IO. `tests` focuses on physical invariants and reproducibility. See `PROJECT_SPEC.md` for model assumptions and limitations.
The Vue 3 + Vite client in `frontend/` connects with `socket.io-client`, renders backend entity positions and trajectory history in Three.js, provides scenario and device configuration, and displays bounded terminal-style telemetry and event scrollback. Snapshot persistence uses a Supabase migration for per-user row-level security. Configure `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` before applying `frontend/supabase/migrations/20260930000000_simulation_runs.sql`.
## Run the server
```bash
python -m defense_sim.server.socketio_server
```
Socket.IO emits `simulation_state`, `simulation_event`, `simulation_statistics`, and `simulation_error`. It accepts scenario/device configuration, not client-authored entity state. CORS is configurable through `SIM_ALLOWED_ORIGINS`.
## Deployment
The site is served at `https://defense.naori.cloud`. The Vue app listens on `192.168.1.194:8088`; the Python Socket.IO backend listens on `192.168.1.194:8000`. Traefik at `192.168.1.10` routes the site and `/socket.io` over HTTPS. User services are defined under `deploy/` and enabled with systemd lingering.
Rebuild the client with:
```bash
VITE_SIM_SOCKET_URL=https://defense.naori.cloud pnpm --dir frontend build
```
Restart backend changes with `systemctl --user restart defense-sim-backend.service`. Traefik's watched dynamic route is `/etc/traefik/conf.d/defense-sim-v2.yml` in LXC 100.

View File

@@ -0,0 +1,17 @@
[Unit]
Description=Defense Sim V2 Socket.IO backend
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/dre/git/plfm/defense_sim_v2
Environment=SIM_BIND_HOST=0.0.0.0
Environment=SIM_PORT=8000
Environment=SIM_ALLOWED_ORIGINS=https://defense.naori.cloud,http://localhost:3333
ExecStart=/home/dre/git/plfm/defense_sim_v2/.venv/bin/python -m defense_sim.server.socketio_server
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,14 @@
[Unit]
Description=Defense Sim V2 static Vue frontend
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/dre/git/plfm/defense_sim_v2/frontend
ExecStart=/usr/bin/python3 -m http.server 8088 --bind 0.0.0.0 --directory /home/dre/git/plfm/defense_sim_v2/frontend/dist
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,29 @@
http:
routers:
defense-sim-v2-web:
rule: "Host(`defense.naori.cloud`)"
entryPoints:
- websecure
service: defense-sim-v2-web
priority: 10
tls:
certResolver: letsencrypt
defense-sim-v2-socketio:
rule: "Host(`defense.naori.cloud`) && PathPrefix(`/socket.io`)"
entryPoints:
- websecure
service: defense-sim-v2-socketio
priority: 100
tls:
certResolver: letsencrypt
services:
defense-sim-v2-web:
loadBalancer:
servers:
- url: "http://192.168.1.194:8088"
defense-sim-v2-socketio:
loadBalancer:
servers:
- url: "http://192.168.1.194:8000"

View File

@@ -0,0 +1,2 @@
node_modules
dist

View File

@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

View File

@@ -0,0 +1,16 @@
# Copy to .env.local (gitignored). Vite loads this over .env when running pnpm dev.
# Supabase project URL and anon key
VITE_SUPABASE_URL=http://localhost:54321
VITE_SUPABASE_ANON_KEY=your-anon-key-here
# Python Socket.IO simulation backend
# Empty uses the current site origin (recommended behind Traefik).
VITE_SIM_SOCKET_URL=
# Optional: restrict available locales (comma-separated, e.g. "en,fr,de")
# Leave empty to allow all locales found in locales/
VITE_ALLOWED_LOCALES=
# Brand name shown in the sidebar and page titles
VITE_BRAND_NAME=Defense Sim V2

View File

@@ -0,0 +1,2 @@
open_collective: antfu
github: [antfu]

View File

@@ -0,0 +1,96 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: pnpm
- name: Install
run: pnpm install
- name: Lint
run: pnpm run lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: pnpm
- name: Install
run: pnpm install
- name: Typecheck
run: pnpm run typecheck
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [20.x, 22.x]
os: [ubuntu-latest]
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/
cache: pnpm
- run: pnpm install
- run: pnpm run test:unit
test-e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: |
~/.cache
key: cypress-cache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- uses: pnpm/action-setup@v3
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
registry-url: https://registry.npmjs.org/
cache: pnpm
- run: pnpm install
- name: Cypress PNPM Patch
run: cp pnpm-lock.yaml package-lock.json
- name: Cypress
uses: cypress-io/github-action@v6
with:
install-command: echo
build: pnpm run build
start: npx vite preview --port 3333

11
defense_sim_v2/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
.DS_Store
.vite-ssg-dist
.vite-ssg-temp
*.local
dist
dist-ssr
node_modules
.idea/
*.log
cypress/downloads
public/assets/fonts

View File

@@ -0,0 +1 @@
shamefully-hoist=true

View File

@@ -0,0 +1,13 @@
{
"recommendations": [
"antfu.iconify",
"antfu.unocss",
"antfu.vite",
"antfu.goto-alias",
"csstools.postcss",
"dbaeumer.vscode-eslint",
"vue.volar",
"lokalise.i18n-ally",
"streetsidesoftware.code-spell-checker"
]
}

View File

@@ -0,0 +1,48 @@
{
"cSpell.words": ["Vitesse", "Vite", "unocss", "vitest", "vueuse", "pinia", "demi", "antfu", "iconify", "intlify", "vitejs", "unplugin", "pnpm"],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": "locales",
"i18n-ally.sortKeys": true,
// Disable the default formatter
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off" },
{ "rule": "format/*", "severity": "off" },
{ "rule": "*-indent", "severity": "off" },
{ "rule": "*-spacing", "severity": "off" },
{ "rule": "*-spaces", "severity": "off" },
{ "rule": "*-order", "severity": "off" },
{ "rule": "*-dangle", "severity": "off" },
{ "rule": "*-newline", "severity": "off" },
{ "rule": "*quotes", "severity": "off" },
{ "rule": "*semi", "severity": "off" }
],
// The following is optional.
// It's better to put under project setting `.vscode/settings.json`
// to avoid conflicts with working with different eslint configs
// that does not support all formats.
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml"
]
}

View File

@@ -0,0 +1,18 @@
FROM node:20-alpine AS build-stage
WORKDIR /app
RUN corepack enable
COPY .npmrc package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN --mount=type=cache,id=pnpm-store,target=/root/.pnpm-store \
pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM nginx:stable-alpine AS production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-PRESENT Anthony Fu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,93 @@
# Vue Template
Opinionated Vue 3 starter based on [Vitesse](https://github.com/antfu-collective/vitesse), extended with shadcn-vue components, Supabase auth, and UnoCSS Wind4.
## Stack
- **Vue 3** + **TypeScript** + **Vite**
- **UnoCSS** with `presetWind4` (Tailwind v4-compatible utilities)
- **shadcn-vue** components powered by **reka-ui**
- **Supabase** for auth and data persistence (PKCE flow)
- **Pinia** stores
- **vue-i18n** with locale auto-detection
- **ViteSSG** static site generation
- **dev-i18n-audit** tool at `/__dev__/i18n-audit` (dev-only)
## Setup
```bash
cp .env.example .env.local
# Fill in VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY
pnpm install
pnpm dev
```
## Environment Variables
| Variable | Description |
| ------------------------ | ------------------------------------------------------- |
| `VITE_SUPABASE_URL` | Your Supabase project URL |
| `VITE_SUPABASE_ANON_KEY` | Your Supabase anon/public key |
| `VITE_ALLOWED_LOCALES` | Comma-separated locale list (e.g. `en,fr`). Empty = all |
| `VITE_BRAND_NAME` | App name shown in sidebar and page titles |
## Adding shadcn-vue Components
The full shadcn-vue component set is already installed in `src/components/ui/`. You can add more via the CLI:
```bash
npx shadcn-vue@latest add <component-name>
```
All components use the `@/` alias pointing to `src/`. The `cn()` utility lives in `src/lib/utils.ts`.
## Auth
Auth is handled via Supabase PKCE. The `useAuth()` composable exposes `loginWithEmail`, `loginWithProvider`, and `signUpWithEmail`. Protected routes use `meta: { requiresAuth: true }` in their route definition.
The `useAuthStore` (Pinia) holds the current user and session state.
## Layouts
| Layout | Description |
| -------------- | --------------------------- |
| `default` | Bare router-view |
| `with-sidebar` | Sidebar + main content area |
| `login` | Centered card layout |
## i18n Audit (dev only)
Navigate to `http://localhost:3333/__dev__/i18n-audit` to scan your source files for i18n keys, audit locale YAML files, and optionally use LM Studio for AI-assisted translation.
## Project Structure
```text
src/
├── components/
│ ├── ui/ # shadcn-vue primitives
│ ├── AppSidebar.vue
│ ├── DarkModeSwitcher.vue
│ └── LanguageSwitcher.vue
├── composables/
│ ├── useAuth.ts
│ └── useLogout.ts
├── devtools/ # i18n-audit (dev only)
├── layouts/
│ ├── default.vue
│ ├── login.vue
│ └── with-sidebar.vue
├── lib/
│ ├── auth/ # auth helpers
│ ├── i18n-audit/ # audit lib
│ ├── supabaseClient.ts
│ └── utils.ts # cn() utility
├── modules/ # auto-loaded Vite SSG modules
├── pages/
│ ├── index.vue
│ └── login.vue
├── stores/
│ ├── auth.ts
│ └── adminAccess.ts
└── types/
└── database.ts # Supabase DB types (replace with your own)
```

View File

@@ -0,0 +1,20 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "default",
"typescript": true,
"tailwind": {
"config": "",
"css": "src/styles/main.css",
"baseColor": "neutral",
"cssVariables": true
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"composables": "@/composables",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/composables"
}
}

View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'cypress'
import vitePreprocessor from 'cypress-vite'
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3333',
chromeWebSecurity: false,
specPattern: 'cypress/e2e/**/*.spec.*',
supportFile: false,
setupNodeEvents(on) {
on('file:preprocessor', vitePreprocessor())
},
},
})

View File

@@ -0,0 +1,36 @@
context('Basic', () => {
beforeEach(() => {
cy.visit('/')
})
it('basic nav', () => {
cy.url()
.should('eq', 'http://localhost:3333/')
cy.contains('[Home Layout]')
.should('exist')
cy.get('#input')
.type('Vitesse{Enter}')
.url()
.should('eq', 'http://localhost:3333/hi/Vitesse')
cy.contains('[Default Layout]')
.should('exist')
cy.get('[btn]')
.click()
.url()
.should('eq', 'http://localhost:3333/')
})
it('markdown', () => {
cy.get('[data-test-id="about"]')
.click()
.url()
.should('eq', 'http://localhost:3333/about')
cy.get('.shiki')
.should('exist')
})
})

View File

@@ -0,0 +1,12 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": [
"cypress"
]
},
"include": [
"**/*.ts"
],
"exclude": []
}

View File

@@ -0,0 +1,16 @@
// @ts-check
import antfu from '@antfu/eslint-config'
export default antfu(
{
unocss: true,
formatters: true,
pnpm: true,
},
{
files: ['README.md', 'README.zh-CN.md'],
rules: {
'markdown/heading-increment': 'off',
},
},
)

View File

@@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="apple-touch-icon" href="/pwa-192x192.png" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#00aba9" />
<meta name="msapplication-TileColor" content="#00aba9" />
<script>
;(function () {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const setting = localStorage.getItem('vueuse-color-scheme') || 'auto'
if (setting === 'dark' || (prefersDark && setting !== 'light'))
document.documentElement.classList.toggle('dark', true)
})()
</script>
</head>
<body class="font-sans">
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<noscript> This website requires JavaScript to function properly. Please enable JavaScript to continue. </noscript>
</body>
</html>

View File

@@ -0,0 +1,7 @@
## i18n
This directory is to serve your locale translation files. YAML under this folder would be loaded automatically and register with their filenames as locale code.
Check out [`vue-i18n`](https://github.com/intlify/vue-i18n-next) for more details.
If you are using VS Code, [`i18n Ally`](https://github.com/lokalise/i18n-ally) is recommended to make the i18n experience better.

View File

@@ -0,0 +1,13 @@
button:
back: Back
home: Home
toggle_dark: Toggle dark mode
intro:
desc: Opinionated Vite Starter Template
themeToggle:
dark: Dark Mode
light: Light Mode
not-found: Not found

View File

@@ -0,0 +1,17 @@
[build]
publish = "dist"
command = "pnpm run build"
[build.environment]
NODE_VERSION = "22"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[[headers]]
for = "/manifest.webmanifest"
[headers.values]
Content-Type = "application/manifest+json"

View File

@@ -0,0 +1,101 @@
{
"type": "module",
"private": true,
"packageManager": "pnpm@10.30.2",
"scripts": {
"build": "vite-ssg build",
"dev": "vite --port 3333 --open",
"lint": "eslint .",
"preview": "vite preview",
"preview-https": "serve dist",
"test": "vitest",
"test:e2e": "cypress open",
"test:unit": "vitest",
"typecheck": "vue-tsc --noEmit",
"up": "taze major -I",
"postinstall": "npx simple-git-hooks",
"sizecheck": "npx vite-bundle-visualizer"
},
"dependencies": {
"@supabase/supabase-js": "catalog:frontend",
"@unhead/vue": "catalog:frontend",
"@unocss/reset": "catalog:frontend",
"@unovis/ts": "catalog:frontend",
"@unovis/vue": "catalog:frontend",
"@vee-validate/zod": "catalog:frontend",
"@vueuse/core": "catalog:frontend",
"class-variance-authority": "catalog:frontend",
"clsx": "catalog:frontend",
"embla-carousel-vue": "catalog:frontend",
"js-yaml": "catalog:frontend",
"lucide-vue-next": "catalog:frontend",
"nprogress": "catalog:frontend",
"pinia": "catalog:frontend",
"reka-ui": "catalog:frontend",
"socket.io-client": "^4.8.1",
"tailwind-merge": "catalog:frontend",
"three": "^0.186.1",
"vaul-vue": "catalog:frontend",
"vee-validate": "catalog:frontend",
"vue": "catalog:frontend",
"vue-i18n": "catalog:frontend",
"vue-router": "catalog:frontend",
"vue-sonner": "catalog:frontend",
"zod": "catalog:frontend"
},
"devDependencies": {
"@antfu/eslint-config": "catalog:dev",
"@iconify-json/carbon": "catalog:dev",
"@intlify/unplugin-vue-i18n": "catalog:build",
"@shikijs/markdown-it": "catalog:build",
"@types/js-yaml": "catalog:types",
"@types/markdown-it-link-attributes": "catalog:types",
"@types/nprogress": "catalog:types",
"@types/three": "^0.186.0",
"@unocss/eslint-config": "catalog:build",
"@unocss/preset-wind4": "catalog:build",
"@vitejs/plugin-vue": "catalog:build",
"@vue-macros/volar": "catalog:dev",
"@vue/test-utils": "catalog:dev",
"beasties": "catalog:build",
"cypress": "catalog:dev",
"cypress-vite": "catalog:dev",
"eslint": "catalog:dev",
"eslint-plugin-cypress": "catalog:dev",
"eslint-plugin-format": "catalog:dev",
"https-localhost": "catalog:dev",
"lint-staged": "catalog:dev",
"markdown-it-link-attributes": "catalog:build",
"rollup": "catalog:build",
"shiki": "catalog:build",
"simple-git-hooks": "catalog:dev",
"taze": "catalog:dev",
"typescript": "catalog:dev",
"unocss": "catalog:build",
"unplugin-auto-import": "catalog:build",
"unplugin-vue-components": "catalog:build",
"unplugin-vue-macros": "catalog:build",
"unplugin-vue-markdown": "catalog:build",
"vite": "catalog:build",
"vite-bundle-visualizer": "catalog:build",
"vite-plugin-inspect": "catalog:build",
"vite-plugin-pwa": "catalog:build",
"vite-plugin-vue-devtools": "catalog:build",
"vite-plugin-vue-layouts": "catalog:build",
"vite-ssg": "catalog:build",
"vite-ssg-sitemap": "catalog:build",
"vitest": "catalog:dev",
"vue-tsc": "catalog:dev"
},
"resolutions": {
"unplugin": "catalog:build",
"vite": "catalog:build",
"vite-plugin-inspect": "catalog:build"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},
"lint-staged": {
"*": "eslint --fix"
}
}

15294
defense_sim_v2/frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,95 @@
shellEmulator: true
trustPolicy: no-downgrade
trustPolicyExclude:
- undici-types@6.21.0
- '@oxc-resolver/binding-win32-x64-msvc@4.2.0'
- '@oxc-resolver/binding-darwin-x64@4.2.0'
- '@oxc-resolver/binding-darwin-arm64@4.2.0'
- '@oxc-resolver/binding-linux-x64-gnu@4.2.0'
- '@oxc-resolver/binding-linux-arm64-gnu@4.2.0'
- '@oxc-resolver/binding-win32-arm64-msvc@4.2.0'
- '@trickfilm400/rollup-plugin-off-main-thread@3.0.0-pre1'
- semver@6.3.1
packages: []
catalogs:
build:
'@intlify/unplugin-vue-i18n': ^11.0.7
'@shikijs/markdown-it': ^3.23.0
'@unocss/eslint-config': ^66.6.1
'@unocss/preset-wind4': ^66.6.1
'@vitejs/plugin-vue': ^6.0.4
beasties: ^0.4.1
markdown-it-link-attributes: ^4.0.1
rollup: ^4.59.0
shiki: ^3.23.0
unocss: ^66.6.1
unplugin: ^2.3.11
unplugin-auto-import: ^21.0.0
unplugin-vue-components: ^31.0.0
unplugin-vue-macros: ^2.14.5
unplugin-vue-markdown: ^29.2.0
vite: ^7.3.1
vite-bundle-visualizer: ^1.2.1
vite-plugin-inspect: ^11.3.3
vite-plugin-pwa: ^1.2.0
vite-plugin-vue-devtools: ^8.0.6
vite-plugin-vue-layouts: ^0.11.0
vite-ssg: ^28.3.0
vite-ssg-sitemap: ^0.10.0
dev:
'@antfu/eslint-config': ^7.6.1
'@iconify-json/carbon': ^1.2.18
'@vue-macros/volar': ^3.1.2
'@vue/test-utils': ^2.4.6
cypress: ^15.10.0
cypress-vite: ^1.8.0
eslint: ^10.0.2
eslint-plugin-cypress: ^6.1.0
eslint-plugin-format: ^2.0.1
https-localhost: ^4.7.1
lint-staged: ^16.2.7
simple-git-hooks: ^2.13.1
taze: ^19.9.2
typescript: ^5.8.2
vitest: ^4.0.18
vue-tsc: ^3.2.5
frontend:
'@supabase/supabase-js': ^2.47.10
'@unhead/vue': ^2.1.7
'@unocss/reset': ^66.6.1
'@unovis/ts': ^1.6.5
'@unovis/vue': ^1.6.5
'@vee-validate/zod': ^4.15.1
'@vueuse/core': ^14.2.1
class-variance-authority: ^0.7.1
clsx: ^2.1.1
embla-carousel-vue: ^8.6.0
js-yaml: ^4.1.1
lucide-vue-next: ^0.511.0
nprogress: ^0.2.0
pinia: ^3.0.4
reka-ui: ^2.2.0
tailwind-merge: ^3.3.0
vaul-vue: ^0.4.1
vee-validate: ^4.15.1
vue: ^3.5.29
vue-i18n: ^11.2.8
vue-router: ^5.0.3
vue-sonner: ^2.0.9
zod: ^4.4.3
types:
'@types/js-yaml': ^4.0.9
'@types/markdown-it-link-attributes': ^3.0.5
'@types/nprogress': ^0.2.3
onlyBuiltDependencies:
- cypress
- esbuild
- simple-git-hooks

View File

@@ -0,0 +1,3 @@
/assets/*
cache-control: max-age=31536000
cache-control: immutable

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.562 26L17.17 8.928l2.366-3.888L17.828 4L16 7.005L14.17 4l-1.708 1.04l2.366 3.888L4.438 26H2v2h28v-2zM16 10.85L25.22 26H17v-8h-2v8H6.78z" fill="white" />
</svg>

After

Width:  |  Height:  |  Size: 311 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.562 26L17.17 8.928l2.366-3.888L17.828 4L16 7.005L14.17 4l-1.708 1.04l2.366 3.888L4.438 26H2v2h28v-2zM16 10.85L25.22 26H17v-8h-2v8H6.78z" fill="#222" />
</svg>

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2916 6015 c-93 -57 -173 -108 -178 -113 -6 -6 7 -36 33 -78 23 -38
86 -141 139 -229 54 -88 135 -221 180 -295 46 -74 94 -155 108 -180 14 -25 29
-52 35 -60 7 -12 -9 -45 -62 -130 -39 -63 -85 -140 -103 -170 -18 -30 -117
-194 -222 -365 -104 -170 -199 -326 -210 -346 -12 -19 -61 -102 -111 -183 -49
-81 -101 -166 -115 -189 -14 -23 -39 -64 -55 -90 -17 -27 -77 -126 -134 -220
-57 -95 -127 -210 -156 -257 -194 -315 -325 -533 -325 -541 0 -5 -4 -9 -10 -9
-5 0 -10 -4 -10 -9 0 -5 -55 -98 -121 -207 -247 -404 -403 -660 -416 -684 -8
-14 -58 -97 -112 -185 l-98 -160 -189 -2 c-104 -1 -225 -2 -269 -2 l-80 -1 1
-210 c0 -116 4 -213 8 -218 11 -11 6107 -9 6114 2 8 13 8 406 0 419 -4 7 -88
10 -265 9 l-259 -2 -50 77 c-27 43 -54 87 -60 98 -6 11 -62 103 -124 205 -62
102 -120 197 -129 212 -9 16 -85 142 -170 280 -85 139 -160 262 -165 273 -6
11 -13 22 -16 25 -3 3 -30 46 -59 95 -30 50 -102 169 -161 265 -59 96 -240
393 -402 660 -163 267 -371 609 -463 760 -92 151 -194 318 -225 370 -31 52
-101 167 -155 255 l-97 160 27 50 c16 27 32 55 36 61 5 5 38 59 74 120 36 60
69 116 74 124 5 8 75 122 155 253 81 131 144 242 141 247 -4 7 -114 76 -183
115 -10 6 -52 32 -95 58 -42 27 -81 46 -87 42 -8 -5 -94 -140 -140 -219 -19
-33 -221 -365 -246 -404 -15 -22 -18 -18 -111 135 -52 87 -123 203 -157 258
-67 108 -67 110 -111 184 -16 28 -34 51 -40 50 -5 0 -86 -47 -179 -104z m739
-1642 c319 -526 519 -854 637 -1046 43 -70 78 -130 78 -133 0 -2 5 -10 10 -17
6 -7 69 -109 140 -227 72 -118 134 -222 139 -230 5 -8 55 -89 111 -180 56 -91
105 -172 110 -180 9 -14 52 -84 270 -445 54 -88 135 -221 180 -295 46 -74 91
-148 100 -165 9 -16 31 -53 48 -81 18 -28 32 -54 32 -57 0 -3 -403 -6 -895 -5
l-895 0 0 81 c-1 45 -1 439 -1 875 l0 792 -37 1 c-57 1 -344 1 -374 0 l-27 -1
0 -832 c0 -458 0 -852 0 -875 l-1 -42 -895 1 c-492 0 -895 3 -895 5 0 9 115
198 122 201 5 2 8 7 8 12 0 5 23 46 51 92 28 46 78 128 112 183 33 55 70 116
82 135 12 19 132 215 265 435 133 220 266 438 295 485 65 105 206 338 220 362
6 10 172 284 370 608 198 325 387 635 420 690 33 55 62 100 65 100 3 0 73
-111 155 -247z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { useDark } from '@vueuse/core'
const isDark = useDark()
const BRAND_NAME = import.meta.env.VITE_BRAND_NAME || 'Defense Sim V2'
useHead({
titleTemplate: (title?: string) => {
if (!title)
return BRAND_NAME
return title.includes(BRAND_NAME) ? title : `${title} · ${BRAND_NAME}`
},
htmlAttrs: {
class: () => isDark.value ? 'dark' : '',
},
})
</script>
<template>
<RouterView />
</template>

View File

@@ -0,0 +1,644 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue').EffectScope
const asyncComputed: typeof import('@vueuse/core').asyncComputed
const autoResetRef: typeof import('@vueuse/core').autoResetRef
const computed: typeof import('vue').computed
const computedAsync: typeof import('@vueuse/core').computedAsync
const computedEager: typeof import('@vueuse/core').computedEager
const computedInject: typeof import('@vueuse/core').computedInject
const computedWithControl: typeof import('@vueuse/core').computedWithControl
const controlledComputed: typeof import('@vueuse/core').controlledComputed
const controlledRef: typeof import('@vueuse/core').controlledRef
const createApp: typeof import('vue').createApp
const createEventHook: typeof import('@vueuse/core').createEventHook
const createGlobalState: typeof import('@vueuse/core').createGlobalState
const createInjectionState: typeof import('@vueuse/core').createInjectionState
const createReactiveFn: typeof import('@vueuse/core').createReactiveFn
const createRef: typeof import('@vueuse/core').createRef
const createReusableTemplate: typeof import('@vueuse/core').createReusableTemplate
const createSharedComposable: typeof import('@vueuse/core').createSharedComposable
const createTemplatePromise: typeof import('@vueuse/core').createTemplatePromise
const createUnrefFn: typeof import('@vueuse/core').createUnrefFn
const customRef: typeof import('vue').customRef
const debouncedRef: typeof import('@vueuse/core').debouncedRef
const debouncedWatch: typeof import('@vueuse/core').debouncedWatch
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const definePage: typeof import('vue-router/experimental').definePage
const eagerComputed: typeof import('@vueuse/core').eagerComputed
const effectScope: typeof import('vue').effectScope
const extendRef: typeof import('@vueuse/core').extendRef
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const h: typeof import('vue').h
const ignorableWatch: typeof import('@vueuse/core').ignorableWatch
const inject: typeof import('vue').inject
const injectHead: typeof import('@unhead/vue').injectHead
const injectLocal: typeof import('@vueuse/core').injectLocal
const isDark: typeof import('./composables/dark').isDark
const isDefined: typeof import('@vueuse/core').isDefined
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const isShallow: typeof import('vue').isShallow
const makeDestructurable: typeof import('@vueuse/core').makeDestructurable
const markRaw: typeof import('vue').markRaw
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onClickOutside: typeof import('@vueuse/core').onClickOutside
const onDeactivated: typeof import('vue').onDeactivated
const onElementRemoval: typeof import('@vueuse/core').onElementRemoval
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onKeyStroke: typeof import('@vueuse/core').onKeyStroke
const onLongPress: typeof import('@vueuse/core').onLongPress
const onMounted: typeof import('vue').onMounted
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onStartTyping: typeof import('@vueuse/core').onStartTyping
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const pausableWatch: typeof import('@vueuse/core').pausableWatch
const preferredDark: typeof import('./composables/dark').preferredDark
const provide: typeof import('vue').provide
const provideLocal: typeof import('@vueuse/core').provideLocal
const reactify: typeof import('@vueuse/core').reactify
const reactifyObject: typeof import('@vueuse/core').reactifyObject
const reactive: typeof import('vue').reactive
const reactiveComputed: typeof import('@vueuse/core').reactiveComputed
const reactiveOmit: typeof import('@vueuse/core').reactiveOmit
const reactivePick: typeof import('@vueuse/core').reactivePick
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const refAutoReset: typeof import('@vueuse/core').refAutoReset
const refDebounced: typeof import('@vueuse/core').refDebounced
const refDefault: typeof import('@vueuse/core').refDefault
const refManualReset: typeof import('@vueuse/core').refManualReset
const refThrottled: typeof import('@vueuse/core').refThrottled
const refWithControl: typeof import('@vueuse/core').refWithControl
const resolveComponent: typeof import('vue').resolveComponent
const resolveRef: typeof import('@vueuse/core').resolveRef
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const syncRef: typeof import('@vueuse/core').syncRef
const syncRefs: typeof import('@vueuse/core').syncRefs
const templateRef: typeof import('@vueuse/core').templateRef
const throttledRef: typeof import('@vueuse/core').throttledRef
const throttledWatch: typeof import('@vueuse/core').throttledWatch
const toRaw: typeof import('vue').toRaw
const toReactive: typeof import('@vueuse/core').toReactive
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const toggleDark: typeof import('./composables/dark').toggleDark
const triggerRef: typeof import('vue').triggerRef
const tryOnBeforeMount: typeof import('@vueuse/core').tryOnBeforeMount
const tryOnBeforeUnmount: typeof import('@vueuse/core').tryOnBeforeUnmount
const tryOnMounted: typeof import('@vueuse/core').tryOnMounted
const tryOnScopeDispose: typeof import('@vueuse/core').tryOnScopeDispose
const tryOnUnmounted: typeof import('@vueuse/core').tryOnUnmounted
const unref: typeof import('vue').unref
const unrefElement: typeof import('@vueuse/core').unrefElement
const until: typeof import('@vueuse/core').until
const useActiveElement: typeof import('@vueuse/core').useActiveElement
const useAdminAccessStore: typeof import('./stores/adminAccess').useAdminAccessStore
const useAnimate: typeof import('@vueuse/core').useAnimate
const useArrayDifference: typeof import('@vueuse/core').useArrayDifference
const useArrayEvery: typeof import('@vueuse/core').useArrayEvery
const useArrayFilter: typeof import('@vueuse/core').useArrayFilter
const useArrayFind: typeof import('@vueuse/core').useArrayFind
const useArrayFindIndex: typeof import('@vueuse/core').useArrayFindIndex
const useArrayFindLast: typeof import('@vueuse/core').useArrayFindLast
const useArrayIncludes: typeof import('@vueuse/core').useArrayIncludes
const useArrayJoin: typeof import('@vueuse/core').useArrayJoin
const useArrayMap: typeof import('@vueuse/core').useArrayMap
const useArrayReduce: typeof import('@vueuse/core').useArrayReduce
const useArraySome: typeof import('@vueuse/core').useArraySome
const useArrayUnique: typeof import('@vueuse/core').useArrayUnique
const useAsyncQueue: typeof import('@vueuse/core').useAsyncQueue
const useAsyncState: typeof import('@vueuse/core').useAsyncState
const useAttrs: typeof import('vue').useAttrs
const useAuth: typeof import('./composables/useAuth').useAuth
const useAuthStore: typeof import('./stores/auth').useAuthStore
const useBase64: typeof import('@vueuse/core').useBase64
const useBattery: typeof import('@vueuse/core').useBattery
const useBluetooth: typeof import('@vueuse/core').useBluetooth
const useBreakpoints: typeof import('@vueuse/core').useBreakpoints
const useBroadcastChannel: typeof import('@vueuse/core').useBroadcastChannel
const useBrowserLocation: typeof import('@vueuse/core').useBrowserLocation
const useCached: typeof import('@vueuse/core').useCached
const useClipboard: typeof import('@vueuse/core').useClipboard
const useClipboardItems: typeof import('@vueuse/core').useClipboardItems
const useCloned: typeof import('@vueuse/core').useCloned
const useColorMode: typeof import('@vueuse/core').useColorMode
const useConfirmDialog: typeof import('@vueuse/core').useConfirmDialog
const useCountdown: typeof import('@vueuse/core').useCountdown
const useCounter: typeof import('@vueuse/core').useCounter
const useCssModule: typeof import('vue').useCssModule
const useCssSupports: typeof import('@vueuse/core').useCssSupports
const useCssVar: typeof import('@vueuse/core').useCssVar
const useCssVars: typeof import('vue').useCssVars
const useCurrentElement: typeof import('@vueuse/core').useCurrentElement
const useCycleList: typeof import('@vueuse/core').useCycleList
const useDark: typeof import('@vueuse/core').useDark
const useDateFormat: typeof import('@vueuse/core').useDateFormat
const useDebounce: typeof import('@vueuse/core').useDebounce
const useDebounceFn: typeof import('@vueuse/core').useDebounceFn
const useDebouncedRefHistory: typeof import('@vueuse/core').useDebouncedRefHistory
const useDeviceMotion: typeof import('@vueuse/core').useDeviceMotion
const useDeviceOrientation: typeof import('@vueuse/core').useDeviceOrientation
const useDevicePixelRatio: typeof import('@vueuse/core').useDevicePixelRatio
const useDevicesList: typeof import('@vueuse/core').useDevicesList
const useDisplayMedia: typeof import('@vueuse/core').useDisplayMedia
const useDocumentVisibility: typeof import('@vueuse/core').useDocumentVisibility
const useDraggable: typeof import('@vueuse/core').useDraggable
const useDropZone: typeof import('@vueuse/core').useDropZone
const useElementBounding: typeof import('@vueuse/core').useElementBounding
const useElementByPoint: typeof import('@vueuse/core').useElementByPoint
const useElementHover: typeof import('@vueuse/core').useElementHover
const useElementSize: typeof import('@vueuse/core').useElementSize
const useElementVisibility: typeof import('@vueuse/core').useElementVisibility
const useEventBus: typeof import('@vueuse/core').useEventBus
const useEventListener: typeof import('@vueuse/core').useEventListener
const useEventSource: typeof import('@vueuse/core').useEventSource
const useEyeDropper: typeof import('@vueuse/core').useEyeDropper
const useFavicon: typeof import('@vueuse/core').useFavicon
const useFetch: typeof import('@vueuse/core').useFetch
const useFileDialog: typeof import('@vueuse/core').useFileDialog
const useFileSystemAccess: typeof import('@vueuse/core').useFileSystemAccess
const useFocus: typeof import('@vueuse/core').useFocus
const useFocusWithin: typeof import('@vueuse/core').useFocusWithin
const useFps: typeof import('@vueuse/core').useFps
const useFullscreen: typeof import('@vueuse/core').useFullscreen
const useGamepad: typeof import('@vueuse/core').useGamepad
const useGeolocation: typeof import('@vueuse/core').useGeolocation
const useHead: typeof import('@unhead/vue').useHead
const useHeadSafe: typeof import('@unhead/vue').useHeadSafe
const useI18n: typeof import('vue-i18n').useI18n
const useId: typeof import('vue').useId
const useIdle: typeof import('@vueuse/core').useIdle
const useImage: typeof import('@vueuse/core').useImage
const useInfiniteScroll: typeof import('@vueuse/core').useInfiniteScroll
const useIntersectionObserver: typeof import('@vueuse/core').useIntersectionObserver
const useInterval: typeof import('@vueuse/core').useInterval
const useIntervalFn: typeof import('@vueuse/core').useIntervalFn
const useKeyModifier: typeof import('@vueuse/core').useKeyModifier
const useLastChanged: typeof import('@vueuse/core').useLastChanged
const useLink: typeof import('vue-router/auto').useLink
const useLocalStorage: typeof import('@vueuse/core').useLocalStorage
const useLogout: typeof import('./composables/useLogout').useLogout
const useMagicKeys: typeof import('@vueuse/core').useMagicKeys
const useManualRefHistory: typeof import('@vueuse/core').useManualRefHistory
const useMediaControls: typeof import('@vueuse/core').useMediaControls
const useMediaQuery: typeof import('@vueuse/core').useMediaQuery
const useMemoize: typeof import('@vueuse/core').useMemoize
const useMemory: typeof import('@vueuse/core').useMemory
const useModel: typeof import('vue').useModel
const useMounted: typeof import('@vueuse/core').useMounted
const useMouse: typeof import('@vueuse/core').useMouse
const useMouseInElement: typeof import('@vueuse/core').useMouseInElement
const useMousePressed: typeof import('@vueuse/core').useMousePressed
const useMutationObserver: typeof import('@vueuse/core').useMutationObserver
const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage
const useNetwork: typeof import('@vueuse/core').useNetwork
const useNow: typeof import('@vueuse/core').useNow
const useObjectUrl: typeof import('@vueuse/core').useObjectUrl
const useOffsetPagination: typeof import('@vueuse/core').useOffsetPagination
const useOnline: typeof import('@vueuse/core').useOnline
const usePageLeave: typeof import('@vueuse/core').usePageLeave
const useParallax: typeof import('@vueuse/core').useParallax
const useParentElement: typeof import('@vueuse/core').useParentElement
const usePerformanceObserver: typeof import('@vueuse/core').usePerformanceObserver
const usePermission: typeof import('@vueuse/core').usePermission
const usePointer: typeof import('@vueuse/core').usePointer
const usePointerLock: typeof import('@vueuse/core').usePointerLock
const usePointerSwipe: typeof import('@vueuse/core').usePointerSwipe
const usePreferredColorScheme: typeof import('@vueuse/core').usePreferredColorScheme
const usePreferredContrast: typeof import('@vueuse/core').usePreferredContrast
const usePreferredDark: typeof import('@vueuse/core').usePreferredDark
const usePreferredLanguages: typeof import('@vueuse/core').usePreferredLanguages
const usePreferredReducedMotion: typeof import('@vueuse/core').usePreferredReducedMotion
const usePreferredReducedTransparency: typeof import('@vueuse/core').usePreferredReducedTransparency
const usePrevious: typeof import('@vueuse/core').usePrevious
const useRafFn: typeof import('@vueuse/core').useRafFn
const useRefHistory: typeof import('@vueuse/core').useRefHistory
const useResizeObserver: typeof import('@vueuse/core').useResizeObserver
const useRoute: typeof import('vue-router').useRoute
const useRouter: typeof import('vue-router').useRouter
const useSSRWidth: typeof import('@vueuse/core').useSSRWidth
const useScreenOrientation: typeof import('@vueuse/core').useScreenOrientation
const useScreenSafeArea: typeof import('@vueuse/core').useScreenSafeArea
const useScriptTag: typeof import('@vueuse/core').useScriptTag
const useScroll: typeof import('@vueuse/core').useScroll
const useScrollLock: typeof import('@vueuse/core').useScrollLock
const useSeoMeta: typeof import('@unhead/vue').useSeoMeta
const useServerHead: typeof import('@unhead/vue').useServerHead
const useServerHeadSafe: typeof import('@unhead/vue').useServerHeadSafe
const useServerSeoMeta: typeof import('@unhead/vue').useServerSeoMeta
const useSessionStorage: typeof import('@vueuse/core').useSessionStorage
const useShare: typeof import('@vueuse/core').useShare
const useSimulationSocket: typeof import('./composables/useSimulationSocket').useSimulationSocket
const useSlots: typeof import('vue').useSlots
const useSorted: typeof import('@vueuse/core').useSorted
const useSpeechRecognition: typeof import('@vueuse/core').useSpeechRecognition
const useSpeechSynthesis: typeof import('@vueuse/core').useSpeechSynthesis
const useStepper: typeof import('@vueuse/core').useStepper
const useStorage: typeof import('@vueuse/core').useStorage
const useStorageAsync: typeof import('@vueuse/core').useStorageAsync
const useStyleTag: typeof import('@vueuse/core').useStyleTag
const useSupported: typeof import('@vueuse/core').useSupported
const useSwipe: typeof import('@vueuse/core').useSwipe
const useTemplateRef: typeof import('vue').useTemplateRef
const useTemplateRefsList: typeof import('@vueuse/core').useTemplateRefsList
const useTextDirection: typeof import('@vueuse/core').useTextDirection
const useTextSelection: typeof import('@vueuse/core').useTextSelection
const useTextareaAutosize: typeof import('@vueuse/core').useTextareaAutosize
const useThrottle: typeof import('@vueuse/core').useThrottle
const useThrottleFn: typeof import('@vueuse/core').useThrottleFn
const useThrottledRefHistory: typeof import('@vueuse/core').useThrottledRefHistory
const useTimeAgo: typeof import('@vueuse/core').useTimeAgo
const useTimeAgoIntl: typeof import('@vueuse/core').useTimeAgoIntl
const useTimeout: typeof import('@vueuse/core').useTimeout
const useTimeoutFn: typeof import('@vueuse/core').useTimeoutFn
const useTimeoutPoll: typeof import('@vueuse/core').useTimeoutPoll
const useTimestamp: typeof import('@vueuse/core').useTimestamp
const useTitle: typeof import('@vueuse/core').useTitle
const useToNumber: typeof import('@vueuse/core').useToNumber
const useToString: typeof import('@vueuse/core').useToString
const useToggle: typeof import('@vueuse/core').useToggle
const useTransition: typeof import('@vueuse/core').useTransition
const useUrlSearchParams: typeof import('@vueuse/core').useUrlSearchParams
const useUserMedia: typeof import('@vueuse/core').useUserMedia
const useUserStore: typeof import('./stores/user').useUserStore
const useVModel: typeof import('@vueuse/core').useVModel
const useVModels: typeof import('@vueuse/core').useVModels
const useVibrate: typeof import('@vueuse/core').useVibrate
const useVirtualList: typeof import('@vueuse/core').useVirtualList
const useWakeLock: typeof import('@vueuse/core').useWakeLock
const useWebNotification: typeof import('@vueuse/core').useWebNotification
const useWebSocket: typeof import('@vueuse/core').useWebSocket
const useWebWorker: typeof import('@vueuse/core').useWebWorker
const useWebWorkerFn: typeof import('@vueuse/core').useWebWorkerFn
const useWindowFocus: typeof import('@vueuse/core').useWindowFocus
const useWindowScroll: typeof import('@vueuse/core').useWindowScroll
const useWindowSize: typeof import('@vueuse/core').useWindowSize
const watch: typeof import('vue').watch
const watchArray: typeof import('@vueuse/core').watchArray
const watchAtMost: typeof import('@vueuse/core').watchAtMost
const watchDebounced: typeof import('@vueuse/core').watchDebounced
const watchDeep: typeof import('@vueuse/core').watchDeep
const watchEffect: typeof import('vue').watchEffect
const watchIgnorable: typeof import('@vueuse/core').watchIgnorable
const watchImmediate: typeof import('@vueuse/core').watchImmediate
const watchOnce: typeof import('@vueuse/core').watchOnce
const watchPausable: typeof import('@vueuse/core').watchPausable
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
const watchThrottled: typeof import('@vueuse/core').watchThrottled
const watchTriggerable: typeof import('@vueuse/core').watchTriggerable
const watchWithFilter: typeof import('@vueuse/core').watchWithFilter
const whenever: typeof import('@vueuse/core').whenever
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
// @ts-ignore
export type { AdminFeatureRow, AdminProfileRow } from './stores/adminAccess'
import('./stores/adminAccess')
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createRef: UnwrapRef<typeof import('@vueuse/core')['createRef']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly definePage: UnwrapRef<typeof import('vue-router/experimental')['definePage']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getCurrentWatcher: UnwrapRef<typeof import('vue')['getCurrentWatcher']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectHead: UnwrapRef<typeof import('@unhead/vue')['injectHead']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDark: UnwrapRef<typeof import('./composables/dark')['isDark']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly isShallow: UnwrapRef<typeof import('vue')['isShallow']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onElementRemoval: UnwrapRef<typeof import('@vueuse/core')['onElementRemoval']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly preferredDark: UnwrapRef<typeof import('./composables/dark')['preferredDark']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
readonly refManualReset: UnwrapRef<typeof import('@vueuse/core')['refManualReset']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly toggleDark: UnwrapRef<typeof import('./composables/dark')['toggleDark']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAdminAccessStore: UnwrapRef<typeof import('./stores/adminAccess')['useAdminAccessStore']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useAuth: UnwrapRef<typeof import('./composables/useAuth')['useAuth']>
readonly useAuthStore: UnwrapRef<typeof import('./stores/auth')['useAuthStore']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useCountdown: UnwrapRef<typeof import('@vueuse/core')['useCountdown']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssSupports: UnwrapRef<typeof import('@vueuse/core')['useCssSupports']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useHead: UnwrapRef<typeof import('@unhead/vue')['useHead']>
readonly useHeadSafe: UnwrapRef<typeof import('@unhead/vue')['useHeadSafe']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useId: UnwrapRef<typeof import('vue')['useId']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLogout: UnwrapRef<typeof import('./composables/useLogout')['useLogout']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useSSRWidth: UnwrapRef<typeof import('@vueuse/core')['useSSRWidth']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSeoMeta: UnwrapRef<typeof import('@unhead/vue')['useSeoMeta']>
readonly useServerHead: UnwrapRef<typeof import('@unhead/vue')['useServerHead']>
readonly useServerHeadSafe: UnwrapRef<typeof import('@unhead/vue')['useServerHeadSafe']>
readonly useServerSeoMeta: UnwrapRef<typeof import('@unhead/vue')['useServerSeoMeta']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSimulationSocket: UnwrapRef<typeof import('./composables/useSimulationSocket')['useSimulationSocket']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
readonly useTimeAgoIntl: UnwrapRef<typeof import('@vueuse/core')['useTimeAgoIntl']>
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
}
}

View File

@@ -0,0 +1,337 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
Accordion: typeof import('./components/ui/accordion/Accordion.vue')['default']
AccordionContent: typeof import('./components/ui/accordion/AccordionContent.vue')['default']
AccordionItem: typeof import('./components/ui/accordion/AccordionItem.vue')['default']
AccordionTrigger: typeof import('./components/ui/accordion/AccordionTrigger.vue')['default']
Alert: typeof import('./components/ui/alert/Alert.vue')['default']
AlertDescription: typeof import('./components/ui/alert/AlertDescription.vue')['default']
AlertDialog: typeof import('./components/ui/alert-dialog/AlertDialog.vue')['default']
AlertDialogAction: typeof import('./components/ui/alert-dialog/AlertDialogAction.vue')['default']
AlertDialogCancel: typeof import('./components/ui/alert-dialog/AlertDialogCancel.vue')['default']
AlertDialogContent: typeof import('./components/ui/alert-dialog/AlertDialogContent.vue')['default']
AlertDialogDescription: typeof import('./components/ui/alert-dialog/AlertDialogDescription.vue')['default']
AlertDialogFooter: typeof import('./components/ui/alert-dialog/AlertDialogFooter.vue')['default']
AlertDialogHeader: typeof import('./components/ui/alert-dialog/AlertDialogHeader.vue')['default']
AlertDialogTitle: typeof import('./components/ui/alert-dialog/AlertDialogTitle.vue')['default']
AlertDialogTrigger: typeof import('./components/ui/alert-dialog/AlertDialogTrigger.vue')['default']
AlertTitle: typeof import('./components/ui/alert/AlertTitle.vue')['default']
AppSidebar: typeof import('./components/AppSidebar.vue')['default']
AspectRatio: typeof import('./components/ui/aspect-ratio/AspectRatio.vue')['default']
Avatar: typeof import('./components/ui/avatar/Avatar.vue')['default']
AvatarFallback: typeof import('./components/ui/avatar/AvatarFallback.vue')['default']
AvatarImage: typeof import('./components/ui/avatar/AvatarImage.vue')['default']
Badge: typeof import('./components/ui/badge/Badge.vue')['default']
Breadcrumb: typeof import('./components/ui/breadcrumb/Breadcrumb.vue')['default']
BreadcrumbEllipsis: typeof import('./components/ui/breadcrumb/BreadcrumbEllipsis.vue')['default']
BreadcrumbItem: typeof import('./components/ui/breadcrumb/BreadcrumbItem.vue')['default']
BreadcrumbLink: typeof import('./components/ui/breadcrumb/BreadcrumbLink.vue')['default']
BreadcrumbList: typeof import('./components/ui/breadcrumb/BreadcrumbList.vue')['default']
BreadcrumbPage: typeof import('./components/ui/breadcrumb/BreadcrumbPage.vue')['default']
BreadcrumbSeparator: typeof import('./components/ui/breadcrumb/BreadcrumbSeparator.vue')['default']
Button: typeof import('./components/ui/button/Button.vue')['default']
ButtonGroup: typeof import('./components/ui/button-group/ButtonGroup.vue')['default']
ButtonGroupSeparator: typeof import('./components/ui/button-group/ButtonGroupSeparator.vue')['default']
ButtonGroupText: typeof import('./components/ui/button-group/ButtonGroupText.vue')['default']
Calendar: typeof import('./components/ui/calendar/Calendar.vue')['default']
CalendarCell: typeof import('./components/ui/calendar/CalendarCell.vue')['default']
CalendarCellTrigger: typeof import('./components/ui/calendar/CalendarCellTrigger.vue')['default']
CalendarGrid: typeof import('./components/ui/calendar/CalendarGrid.vue')['default']
CalendarGridBody: typeof import('./components/ui/calendar/CalendarGridBody.vue')['default']
CalendarGridHead: typeof import('./components/ui/calendar/CalendarGridHead.vue')['default']
CalendarGridRow: typeof import('./components/ui/calendar/CalendarGridRow.vue')['default']
CalendarHeadCell: typeof import('./components/ui/calendar/CalendarHeadCell.vue')['default']
CalendarHeader: typeof import('./components/ui/calendar/CalendarHeader.vue')['default']
CalendarHeading: typeof import('./components/ui/calendar/CalendarHeading.vue')['default']
CalendarNextButton: typeof import('./components/ui/calendar/CalendarNextButton.vue')['default']
CalendarPrevButton: typeof import('./components/ui/calendar/CalendarPrevButton.vue')['default']
Card: typeof import('./components/ui/card/Card.vue')['default']
CardContent: typeof import('./components/ui/card/CardContent.vue')['default']
CardDescription: typeof import('./components/ui/card/CardDescription.vue')['default']
CardFooter: typeof import('./components/ui/card/CardFooter.vue')['default']
CardHeader: typeof import('./components/ui/card/CardHeader.vue')['default']
CardTitle: typeof import('./components/ui/card/CardTitle.vue')['default']
Carousel: typeof import('./components/ui/carousel/Carousel.vue')['default']
CarouselContent: typeof import('./components/ui/carousel/CarouselContent.vue')['default']
CarouselItem: typeof import('./components/ui/carousel/CarouselItem.vue')['default']
CarouselNext: typeof import('./components/ui/carousel/CarouselNext.vue')['default']
CarouselPrevious: typeof import('./components/ui/carousel/CarouselPrevious.vue')['default']
ChartCrosshair: typeof import('./components/ui/chart/ChartCrosshair.vue')['default']
ChartLegend: typeof import('./components/ui/chart/ChartLegend.vue')['default']
ChartSingleTooltip: typeof import('./components/ui/chart/ChartSingleTooltip.vue')['default']
ChartTooltip: typeof import('./components/ui/chart/ChartTooltip.vue')['default']
Checkbox: typeof import('./components/ui/checkbox/Checkbox.vue')['default']
Collapsible: typeof import('./components/ui/collapsible/Collapsible.vue')['default']
CollapsibleContent: typeof import('./components/ui/collapsible/CollapsibleContent.vue')['default']
CollapsibleTrigger: typeof import('./components/ui/collapsible/CollapsibleTrigger.vue')['default']
Combobox: typeof import('./components/ui/combobox/Combobox.vue')['default']
ComboboxAnchor: typeof import('./components/ui/combobox/ComboboxAnchor.vue')['default']
ComboboxEmpty: typeof import('./components/ui/combobox/ComboboxEmpty.vue')['default']
ComboboxGroup: typeof import('./components/ui/combobox/ComboboxGroup.vue')['default']
ComboboxInput: typeof import('./components/ui/combobox/ComboboxInput.vue')['default']
ComboboxItem: typeof import('./components/ui/combobox/ComboboxItem.vue')['default']
ComboboxList: typeof import('./components/ui/combobox/ComboboxList.vue')['default']
ComboboxSeparator: typeof import('./components/ui/combobox/ComboboxSeparator.vue')['default']
ComboboxTrigger: typeof import('./components/ui/combobox/ComboboxTrigger.vue')['default']
Command: typeof import('./components/ui/command/Command.vue')['default']
CommandDialog: typeof import('./components/ui/command/CommandDialog.vue')['default']
CommandEmpty: typeof import('./components/ui/command/CommandEmpty.vue')['default']
CommandGroup: typeof import('./components/ui/command/CommandGroup.vue')['default']
CommandInput: typeof import('./components/ui/command/CommandInput.vue')['default']
CommandItem: typeof import('./components/ui/command/CommandItem.vue')['default']
CommandList: typeof import('./components/ui/command/CommandList.vue')['default']
CommandSeparator: typeof import('./components/ui/command/CommandSeparator.vue')['default']
CommandShortcut: typeof import('./components/ui/command/CommandShortcut.vue')['default']
ContextMenu: typeof import('./components/ui/context-menu/ContextMenu.vue')['default']
ContextMenuCheckboxItem: typeof import('./components/ui/context-menu/ContextMenuCheckboxItem.vue')['default']
ContextMenuContent: typeof import('./components/ui/context-menu/ContextMenuContent.vue')['default']
ContextMenuGroup: typeof import('./components/ui/context-menu/ContextMenuGroup.vue')['default']
ContextMenuItem: typeof import('./components/ui/context-menu/ContextMenuItem.vue')['default']
ContextMenuLabel: typeof import('./components/ui/context-menu/ContextMenuLabel.vue')['default']
ContextMenuPortal: typeof import('./components/ui/context-menu/ContextMenuPortal.vue')['default']
ContextMenuRadioGroup: typeof import('./components/ui/context-menu/ContextMenuRadioGroup.vue')['default']
ContextMenuRadioItem: typeof import('./components/ui/context-menu/ContextMenuRadioItem.vue')['default']
ContextMenuSeparator: typeof import('./components/ui/context-menu/ContextMenuSeparator.vue')['default']
ContextMenuShortcut: typeof import('./components/ui/context-menu/ContextMenuShortcut.vue')['default']
ContextMenuSub: typeof import('./components/ui/context-menu/ContextMenuSub.vue')['default']
ContextMenuSubContent: typeof import('./components/ui/context-menu/ContextMenuSubContent.vue')['default']
ContextMenuSubTrigger: typeof import('./components/ui/context-menu/ContextMenuSubTrigger.vue')['default']
ContextMenuTrigger: typeof import('./components/ui/context-menu/ContextMenuTrigger.vue')['default']
DarkModeSwitcher: typeof import('./components/DarkModeSwitcher.vue')['default']
Dialog: typeof import('./components/ui/dialog/Dialog.vue')['default']
DialogClose: typeof import('./components/ui/dialog/DialogClose.vue')['default']
DialogContent: typeof import('./components/ui/dialog/DialogContent.vue')['default']
DialogDescription: typeof import('./components/ui/dialog/DialogDescription.vue')['default']
DialogFooter: typeof import('./components/ui/dialog/DialogFooter.vue')['default']
DialogHeader: typeof import('./components/ui/dialog/DialogHeader.vue')['default']
DialogScrollContent: typeof import('./components/ui/dialog/DialogScrollContent.vue')['default']
DialogTitle: typeof import('./components/ui/dialog/DialogTitle.vue')['default']
DialogTrigger: typeof import('./components/ui/dialog/DialogTrigger.vue')['default']
Drawer: typeof import('./components/ui/drawer/Drawer.vue')['default']
DrawerContent: typeof import('./components/ui/drawer/DrawerContent.vue')['default']
DrawerDescription: typeof import('./components/ui/drawer/DrawerDescription.vue')['default']
DrawerFooter: typeof import('./components/ui/drawer/DrawerFooter.vue')['default']
DrawerHeader: typeof import('./components/ui/drawer/DrawerHeader.vue')['default']
DrawerOverlay: typeof import('./components/ui/drawer/DrawerOverlay.vue')['default']
DrawerTitle: typeof import('./components/ui/drawer/DrawerTitle.vue')['default']
DropdownMenu: typeof import('./components/ui/dropdown-menu/DropdownMenu.vue')['default']
DropdownMenuCheckboxItem: typeof import('./components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue')['default']
DropdownMenuContent: typeof import('./components/ui/dropdown-menu/DropdownMenuContent.vue')['default']
DropdownMenuGroup: typeof import('./components/ui/dropdown-menu/DropdownMenuGroup.vue')['default']
DropdownMenuItem: typeof import('./components/ui/dropdown-menu/DropdownMenuItem.vue')['default']
DropdownMenuLabel: typeof import('./components/ui/dropdown-menu/DropdownMenuLabel.vue')['default']
DropdownMenuRadioGroup: typeof import('./components/ui/dropdown-menu/DropdownMenuRadioGroup.vue')['default']
DropdownMenuRadioItem: typeof import('./components/ui/dropdown-menu/DropdownMenuRadioItem.vue')['default']
DropdownMenuSeparator: typeof import('./components/ui/dropdown-menu/DropdownMenuSeparator.vue')['default']
DropdownMenuShortcut: typeof import('./components/ui/dropdown-menu/DropdownMenuShortcut.vue')['default']
DropdownMenuSub: typeof import('./components/ui/dropdown-menu/DropdownMenuSub.vue')['default']
DropdownMenuSubContent: typeof import('./components/ui/dropdown-menu/DropdownMenuSubContent.vue')['default']
DropdownMenuSubTrigger: typeof import('./components/ui/dropdown-menu/DropdownMenuSubTrigger.vue')['default']
DropdownMenuTrigger: typeof import('./components/ui/dropdown-menu/DropdownMenuTrigger.vue')['default']
Empty: typeof import('./components/ui/empty/Empty.vue')['default']
EmptyContent: typeof import('./components/ui/empty/EmptyContent.vue')['default']
EmptyDescription: typeof import('./components/ui/empty/EmptyDescription.vue')['default']
EmptyHeader: typeof import('./components/ui/empty/EmptyHeader.vue')['default']
EmptyMedia: typeof import('./components/ui/empty/EmptyMedia.vue')['default']
EmptyTitle: typeof import('./components/ui/empty/EmptyTitle.vue')['default']
Field: typeof import('./components/ui/field/Field.vue')['default']
FieldContent: typeof import('./components/ui/field/FieldContent.vue')['default']
FieldDescription: typeof import('./components/ui/field/FieldDescription.vue')['default']
FieldError: typeof import('./components/ui/field/FieldError.vue')['default']
FieldGroup: typeof import('./components/ui/field/FieldGroup.vue')['default']
FieldLabel: typeof import('./components/ui/field/FieldLabel.vue')['default']
FieldLegend: typeof import('./components/ui/field/FieldLegend.vue')['default']
FieldSeparator: typeof import('./components/ui/field/FieldSeparator.vue')['default']
FieldSet: typeof import('./components/ui/field/FieldSet.vue')['default']
FieldTitle: typeof import('./components/ui/field/FieldTitle.vue')['default']
FormControl: typeof import('./components/ui/form/FormControl.vue')['default']
FormDescription: typeof import('./components/ui/form/FormDescription.vue')['default']
FormItem: typeof import('./components/ui/form/FormItem.vue')['default']
FormLabel: typeof import('./components/ui/form/FormLabel.vue')['default']
FormMessage: typeof import('./components/ui/form/FormMessage.vue')['default']
HoverCard: typeof import('./components/ui/hover-card/HoverCard.vue')['default']
HoverCardContent: typeof import('./components/ui/hover-card/HoverCardContent.vue')['default']
HoverCardTrigger: typeof import('./components/ui/hover-card/HoverCardTrigger.vue')['default']
Input: typeof import('./components/ui/input/Input.vue')['default']
InputGroup: typeof import('./components/ui/input-group/InputGroup.vue')['default']
InputGroupAddon: typeof import('./components/ui/input-group/InputGroupAddon.vue')['default']
InputGroupButton: typeof import('./components/ui/input-group/InputGroupButton.vue')['default']
InputGroupInput: typeof import('./components/ui/input-group/InputGroupInput.vue')['default']
InputGroupText: typeof import('./components/ui/input-group/InputGroupText.vue')['default']
InputGroupTextarea: typeof import('./components/ui/input-group/InputGroupTextarea.vue')['default']
Item: typeof import('./components/ui/item/Item.vue')['default']
ItemActions: typeof import('./components/ui/item/ItemActions.vue')['default']
ItemContent: typeof import('./components/ui/item/ItemContent.vue')['default']
ItemDescription: typeof import('./components/ui/item/ItemDescription.vue')['default']
ItemFooter: typeof import('./components/ui/item/ItemFooter.vue')['default']
ItemGroup: typeof import('./components/ui/item/ItemGroup.vue')['default']
ItemHeader: typeof import('./components/ui/item/ItemHeader.vue')['default']
ItemMedia: typeof import('./components/ui/item/ItemMedia.vue')['default']
ItemSeparator: typeof import('./components/ui/item/ItemSeparator.vue')['default']
ItemTitle: typeof import('./components/ui/item/ItemTitle.vue')['default']
Kbd: typeof import('./components/ui/kbd/Kbd.vue')['default']
KbdGroup: typeof import('./components/ui/kbd/KbdGroup.vue')['default']
Label: typeof import('./components/ui/label/Label.vue')['default']
LanguageSwitcher: typeof import('./components/LanguageSwitcher.vue')['default']
Menubar: typeof import('./components/ui/menubar/Menubar.vue')['default']
MenubarCheckboxItem: typeof import('./components/ui/menubar/MenubarCheckboxItem.vue')['default']
MenubarContent: typeof import('./components/ui/menubar/MenubarContent.vue')['default']
MenubarGroup: typeof import('./components/ui/menubar/MenubarGroup.vue')['default']
MenubarItem: typeof import('./components/ui/menubar/MenubarItem.vue')['default']
MenubarLabel: typeof import('./components/ui/menubar/MenubarLabel.vue')['default']
MenubarMenu: typeof import('./components/ui/menubar/MenubarMenu.vue')['default']
MenubarRadioGroup: typeof import('./components/ui/menubar/MenubarRadioGroup.vue')['default']
MenubarRadioItem: typeof import('./components/ui/menubar/MenubarRadioItem.vue')['default']
MenubarSeparator: typeof import('./components/ui/menubar/MenubarSeparator.vue')['default']
MenubarShortcut: typeof import('./components/ui/menubar/MenubarShortcut.vue')['default']
MenubarSub: typeof import('./components/ui/menubar/MenubarSub.vue')['default']
MenubarSubContent: typeof import('./components/ui/menubar/MenubarSubContent.vue')['default']
MenubarSubTrigger: typeof import('./components/ui/menubar/MenubarSubTrigger.vue')['default']
MenubarTrigger: typeof import('./components/ui/menubar/MenubarTrigger.vue')['default']
NavigationMenu: typeof import('./components/ui/navigation-menu/NavigationMenu.vue')['default']
NavigationMenuContent: typeof import('./components/ui/navigation-menu/NavigationMenuContent.vue')['default']
NavigationMenuIndicator: typeof import('./components/ui/navigation-menu/NavigationMenuIndicator.vue')['default']
NavigationMenuItem: typeof import('./components/ui/navigation-menu/NavigationMenuItem.vue')['default']
NavigationMenuLink: typeof import('./components/ui/navigation-menu/NavigationMenuLink.vue')['default']
NavigationMenuList: typeof import('./components/ui/navigation-menu/NavigationMenuList.vue')['default']
NavigationMenuTrigger: typeof import('./components/ui/navigation-menu/NavigationMenuTrigger.vue')['default']
NavigationMenuViewport: typeof import('./components/ui/navigation-menu/NavigationMenuViewport.vue')['default']
NumberField: typeof import('./components/ui/number-field/NumberField.vue')['default']
NumberFieldContent: typeof import('./components/ui/number-field/NumberFieldContent.vue')['default']
NumberFieldDecrement: typeof import('./components/ui/number-field/NumberFieldDecrement.vue')['default']
NumberFieldIncrement: typeof import('./components/ui/number-field/NumberFieldIncrement.vue')['default']
NumberFieldInput: typeof import('./components/ui/number-field/NumberFieldInput.vue')['default']
Pagination: typeof import('./components/ui/pagination/Pagination.vue')['default']
PaginationContent: typeof import('./components/ui/pagination/PaginationContent.vue')['default']
PaginationEllipsis: typeof import('./components/ui/pagination/PaginationEllipsis.vue')['default']
PaginationFirst: typeof import('./components/ui/pagination/PaginationFirst.vue')['default']
PaginationItem: typeof import('./components/ui/pagination/PaginationItem.vue')['default']
PaginationLast: typeof import('./components/ui/pagination/PaginationLast.vue')['default']
PaginationNext: typeof import('./components/ui/pagination/PaginationNext.vue')['default']
PaginationPrevious: typeof import('./components/ui/pagination/PaginationPrevious.vue')['default']
PinInput: typeof import('./components/ui/pin-input/PinInput.vue')['default']
PinInputGroup: typeof import('./components/ui/pin-input/PinInputGroup.vue')['default']
PinInputSeparator: typeof import('./components/ui/pin-input/PinInputSeparator.vue')['default']
PinInputSlot: typeof import('./components/ui/pin-input/PinInputSlot.vue')['default']
Popover: typeof import('./components/ui/popover/Popover.vue')['default']
PopoverContent: typeof import('./components/ui/popover/PopoverContent.vue')['default']
PopoverTrigger: typeof import('./components/ui/popover/PopoverTrigger.vue')['default']
Progress: typeof import('./components/ui/progress/Progress.vue')['default']
RadioGroup: typeof import('./components/ui/radio-group/RadioGroup.vue')['default']
RadioGroupItem: typeof import('./components/ui/radio-group/RadioGroupItem.vue')['default']
RangeCalendar: typeof import('./components/ui/range-calendar/RangeCalendar.vue')['default']
RangeCalendarCell: typeof import('./components/ui/range-calendar/RangeCalendarCell.vue')['default']
RangeCalendarCellTrigger: typeof import('./components/ui/range-calendar/RangeCalendarCellTrigger.vue')['default']
RangeCalendarGrid: typeof import('./components/ui/range-calendar/RangeCalendarGrid.vue')['default']
RangeCalendarGridBody: typeof import('./components/ui/range-calendar/RangeCalendarGridBody.vue')['default']
RangeCalendarGridHead: typeof import('./components/ui/range-calendar/RangeCalendarGridHead.vue')['default']
RangeCalendarGridRow: typeof import('./components/ui/range-calendar/RangeCalendarGridRow.vue')['default']
RangeCalendarHeadCell: typeof import('./components/ui/range-calendar/RangeCalendarHeadCell.vue')['default']
RangeCalendarHeader: typeof import('./components/ui/range-calendar/RangeCalendarHeader.vue')['default']
RangeCalendarHeading: typeof import('./components/ui/range-calendar/RangeCalendarHeading.vue')['default']
RangeCalendarNextButton: typeof import('./components/ui/range-calendar/RangeCalendarNextButton.vue')['default']
RangeCalendarPrevButton: typeof import('./components/ui/range-calendar/RangeCalendarPrevButton.vue')['default']
README: typeof import('./components/README.md')['default']
ResizableHandle: typeof import('./components/ui/resizable/ResizableHandle.vue')['default']
ResizablePanelGroup: typeof import('./components/ui/resizable/ResizablePanelGroup.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ScrollArea: typeof import('./components/ui/scroll-area/ScrollArea.vue')['default']
ScrollBar: typeof import('./components/ui/scroll-area/ScrollBar.vue')['default']
Select: typeof import('./components/ui/select/Select.vue')['default']
SelectContent: typeof import('./components/ui/select/SelectContent.vue')['default']
SelectGroup: typeof import('./components/ui/select/SelectGroup.vue')['default']
SelectItem: typeof import('./components/ui/select/SelectItem.vue')['default']
SelectItemText: typeof import('./components/ui/select/SelectItemText.vue')['default']
SelectLabel: typeof import('./components/ui/select/SelectLabel.vue')['default']
SelectScrollDownButton: typeof import('./components/ui/select/SelectScrollDownButton.vue')['default']
SelectScrollUpButton: typeof import('./components/ui/select/SelectScrollUpButton.vue')['default']
SelectSeparator: typeof import('./components/ui/select/SelectSeparator.vue')['default']
SelectTrigger: typeof import('./components/ui/select/SelectTrigger.vue')['default']
SelectValue: typeof import('./components/ui/select/SelectValue.vue')['default']
Separator: typeof import('./components/ui/separator/Separator.vue')['default']
Sheet: typeof import('./components/ui/sheet/Sheet.vue')['default']
SheetClose: typeof import('./components/ui/sheet/SheetClose.vue')['default']
SheetContent: typeof import('./components/ui/sheet/SheetContent.vue')['default']
SheetDescription: typeof import('./components/ui/sheet/SheetDescription.vue')['default']
SheetFooter: typeof import('./components/ui/sheet/SheetFooter.vue')['default']
SheetHeader: typeof import('./components/ui/sheet/SheetHeader.vue')['default']
SheetTitle: typeof import('./components/ui/sheet/SheetTitle.vue')['default']
SheetTrigger: typeof import('./components/ui/sheet/SheetTrigger.vue')['default']
Sidebar: typeof import('./components/ui/sidebar/Sidebar.vue')['default']
SidebarContent: typeof import('./components/ui/sidebar/SidebarContent.vue')['default']
SidebarFooter: typeof import('./components/ui/sidebar/SidebarFooter.vue')['default']
SidebarGroup: typeof import('./components/ui/sidebar/SidebarGroup.vue')['default']
SidebarGroupAction: typeof import('./components/ui/sidebar/SidebarGroupAction.vue')['default']
SidebarGroupContent: typeof import('./components/ui/sidebar/SidebarGroupContent.vue')['default']
SidebarGroupLabel: typeof import('./components/ui/sidebar/SidebarGroupLabel.vue')['default']
SidebarHeader: typeof import('./components/ui/sidebar/SidebarHeader.vue')['default']
SidebarInput: typeof import('./components/ui/sidebar/SidebarInput.vue')['default']
SidebarInset: typeof import('./components/ui/sidebar/SidebarInset.vue')['default']
SidebarMenu: typeof import('./components/ui/sidebar/SidebarMenu.vue')['default']
SidebarMenuAction: typeof import('./components/ui/sidebar/SidebarMenuAction.vue')['default']
SidebarMenuBadge: typeof import('./components/ui/sidebar/SidebarMenuBadge.vue')['default']
SidebarMenuButton: typeof import('./components/ui/sidebar/SidebarMenuButton.vue')['default']
SidebarMenuButtonChild: typeof import('./components/ui/sidebar/SidebarMenuButtonChild.vue')['default']
SidebarMenuItem: typeof import('./components/ui/sidebar/SidebarMenuItem.vue')['default']
SidebarMenuSkeleton: typeof import('./components/ui/sidebar/SidebarMenuSkeleton.vue')['default']
SidebarMenuSub: typeof import('./components/ui/sidebar/SidebarMenuSub.vue')['default']
SidebarMenuSubButton: typeof import('./components/ui/sidebar/SidebarMenuSubButton.vue')['default']
SidebarMenuSubItem: typeof import('./components/ui/sidebar/SidebarMenuSubItem.vue')['default']
SidebarProvider: typeof import('./components/ui/sidebar/SidebarProvider.vue')['default']
SidebarRail: typeof import('./components/ui/sidebar/SidebarRail.vue')['default']
SidebarSeparator: typeof import('./components/ui/sidebar/SidebarSeparator.vue')['default']
SidebarTrigger: typeof import('./components/ui/sidebar/SidebarTrigger.vue')['default']
SimulationScene: typeof import('./components/SimulationScene.vue')['default']
Skeleton: typeof import('./components/ui/skeleton/Skeleton.vue')['default']
Slider: typeof import('./components/ui/slider/Slider.vue')['default']
Sonner: typeof import('./components/ui/sonner/Sonner.vue')['default']
Spinner: typeof import('./components/ui/spinner/Spinner.vue')['default']
Stepper: typeof import('./components/ui/stepper/Stepper.vue')['default']
StepperDescription: typeof import('./components/ui/stepper/StepperDescription.vue')['default']
StepperIndicator: typeof import('./components/ui/stepper/StepperIndicator.vue')['default']
StepperItem: typeof import('./components/ui/stepper/StepperItem.vue')['default']
StepperSeparator: typeof import('./components/ui/stepper/StepperSeparator.vue')['default']
StepperTitle: typeof import('./components/ui/stepper/StepperTitle.vue')['default']
StepperTrigger: typeof import('./components/ui/stepper/StepperTrigger.vue')['default']
Switch: typeof import('./components/ui/switch/Switch.vue')['default']
Table: typeof import('./components/ui/table/Table.vue')['default']
TableBody: typeof import('./components/ui/table/TableBody.vue')['default']
TableCaption: typeof import('./components/ui/table/TableCaption.vue')['default']
TableCell: typeof import('./components/ui/table/TableCell.vue')['default']
TableEmpty: typeof import('./components/ui/table/TableEmpty.vue')['default']
TableFooter: typeof import('./components/ui/table/TableFooter.vue')['default']
TableHead: typeof import('./components/ui/table/TableHead.vue')['default']
TableHeader: typeof import('./components/ui/table/TableHeader.vue')['default']
TableRow: typeof import('./components/ui/table/TableRow.vue')['default']
Tabs: typeof import('./components/ui/tabs/Tabs.vue')['default']
TabsContent: typeof import('./components/ui/tabs/TabsContent.vue')['default']
TabsList: typeof import('./components/ui/tabs/TabsList.vue')['default']
TabsTrigger: typeof import('./components/ui/tabs/TabsTrigger.vue')['default']
TagsInput: typeof import('./components/ui/tags-input/TagsInput.vue')['default']
TagsInputInput: typeof import('./components/ui/tags-input/TagsInputInput.vue')['default']
TagsInputItem: typeof import('./components/ui/tags-input/TagsInputItem.vue')['default']
TagsInputItemDelete: typeof import('./components/ui/tags-input/TagsInputItemDelete.vue')['default']
TagsInputItemText: typeof import('./components/ui/tags-input/TagsInputItemText.vue')['default']
Textarea: typeof import('./components/ui/textarea/Textarea.vue')['default']
TheInput: typeof import('./components/TheInput.vue')['default']
Toggle: typeof import('./components/ui/toggle/Toggle.vue')['default']
ToggleGroup: typeof import('./components/ui/toggle-group/ToggleGroup.vue')['default']
ToggleGroupItem: typeof import('./components/ui/toggle-group/ToggleGroupItem.vue')['default']
Tooltip: typeof import('./components/ui/tooltip/Tooltip.vue')['default']
TooltipContent: typeof import('./components/ui/tooltip/TooltipContent.vue')['default']
TooltipProvider: typeof import('./components/ui/tooltip/TooltipProvider.vue')['default']
TooltipTrigger: typeof import('./components/ui/tooltip/TooltipTrigger.vue')['default']
}
}

View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import { LayoutDashboard, Settings } from 'lucide-vue-next'
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/ui/sidebar'
import DarkModeSwitcher from '~/components/DarkModeSwitcher.vue'
import LanguageSwitcher from '~/components/LanguageSwitcher.vue'
const BRAND_NAME = import.meta.env.VITE_BRAND_NAME || 'Vitesse'
const navItems = [
{ title: 'Dashboard', url: '/dashboard', icon: LayoutDashboard },
{ title: 'Settings', url: '/settings', icon: Settings },
]
</script>
<template>
<Sidebar>
<SidebarHeader>
<div class="px-2 py-1 flex gap-2 items-center">
<span class="text-foreground font-semibold">{{ BRAND_NAME }}</span>
</div>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Application</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem v-for="item in navItems" :key="item.title">
<SidebarMenuButton as-child>
<RouterLink :to="item.url">
<component :is="item.icon" class="h-4 w-4" />
<span>{{ item.title }}</span>
</RouterLink>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<div class="p-2 flex flex-col gap-1">
<LanguageSwitcher />
<DarkModeSwitcher show-label />
</div>
</SidebarFooter>
</Sidebar>
</template>

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import { useColorMode } from '@vueuse/core'
import { computed } from 'vue'
import { useI18n } from '~/modules/i18n'
defineProps<{ showLabel?: boolean }>()
const mode = useColorMode({ disableTransition: false })
const isDark = computed(() => mode.value === 'dark')
const { t } = useI18n()
function toggle() {
mode.value = isDark.value ? 'light' : 'dark'
}
</script>
<template>
<button
class="p-2 text-left rounded-md flex w-full transition-colors items-center hover:bg-accent"
:aria-label="t('button.toggle_dark')"
@click="toggle"
>
<svg
v-if="isDark"
xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
<span v-if="showLabel" class="text-sm ml-2">
{{ isDark ? t('themeToggle.dark', 'Dark Mode') : t('themeToggle.light', 'Light Mode') }}
</span>
</button>
</template>

View File

@@ -0,0 +1,66 @@
<script setup lang="ts">
import { ChevronDown, Globe } from 'lucide-vue-next'
import { computed } from 'vue'
import { availableLocales, loadLanguageAsync, useI18n } from '~/modules/i18n'
const languageNames: Record<string, string> = {
'en': 'English',
'ru': 'Русский',
'ar': 'العربية',
'pt': 'Português',
'pt-BR': 'Português (Brasil)',
'fr': 'Français',
'de': 'Deutsch',
'es': 'Español',
'it': 'Italiano',
'nl': 'Nederlands',
'pl': 'Polski',
'sv': 'Svenska',
'tr': 'Türkçe',
'ja': '日本語',
'ko': '한국어',
'zh-CN': '简体中文',
'uk': 'Українська',
'uz': 'Oʻzbek',
'ka': 'ქართული',
'vi': 'Tiếng Việt',
'id': 'Indonesia',
}
const { locale } = useI18n()
const availableLanguages = computed(() => {
const result: Record<string, string> = {}
for (const loc of availableLocales)
result[loc] = languageNames[loc] || loc
return result
})
const currentLanguage = computed(() => languageNames[locale.value] || locale.value)
async function changeLanguage(newLocale: string) {
await loadLanguageAsync(newLocale)
locale.value = newLocale
}
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<button class="text-sm p-2 text-left rounded-md flex w-full transition-colors items-center hover:bg-accent">
<Globe class="mr-2 shrink-0 h-4 w-4" />
{{ currentLanguage }}
<ChevronDown class="ml-auto h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
v-for="(name, loc) in availableLanguages"
:key="loc"
@click="changeLanguage(loc)"
>
{{ name }}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@@ -0,0 +1,9 @@
## Components
Components in this dir will be auto-registered and on-demand, powered by [`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components).
### Icons
You can use icons from almost any icon sets by the power of [Iconify](https://iconify.design/).
It will only bundle the icons you use. Check out [`unplugin-icons`](https://github.com/antfu/unplugin-icons) for more details.

View File

@@ -0,0 +1,189 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { AmbientLight, BufferGeometry, ConeGeometry, GridHelper, Group, Line, LineBasicMaterial, Matrix4, Mesh, MeshStandardMaterial, PerspectiveCamera, Quaternion, RingGeometry, Scene, Vector3, WebGLRenderer } from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import type { SimulationEntity, SimulationState } from '~/types/simulation'
const props = defineProps<{ state: SimulationState | null }>()
const host = ref<HTMLDivElement>()
let renderer: WebGLRenderer
let camera: PerspectiveCamera
let scene: Scene
let controls: OrbitControls
let observer: ResizeObserver
let frame = 0
const entities = new Map<string, { body: Mesh, trail: Line }>()
const up = new Vector3(0, 1, 0)
const color = { projectile: 0xff6258, drone: 0x5ca8ff }
function groundPoint(ned: number[]) {
return new Vector3(ned[1] || 0, -(ned[2] || 0), -(ned[0] || 0))
}
function makeBody(entity: SimulationEntity) {
const isProjectile = entity.type === 'projectile'
const geometry = isProjectile ? new ConeGeometry(180, 700, 6) : new ConeGeometry(280, 850, 4)
if (!isProjectile)
geometry.rotateZ(-Math.PI / 2)
const material = new MeshStandardMaterial({ color: isProjectile ? color.projectile : color.drone, emissive: isProjectile ? 0x40100c : 0x0b2138, roughness: 0.5 })
const body = new Mesh(geometry, material)
body.userData.kind = entity.type
const trailMaterial = new LineBasicMaterial({ color: isProjectile ? color.projectile : color.drone, transparent: true, opacity: isProjectile ? 0.72 : 0.4 })
const trail = new Line(new BufferGeometry(), trailMaterial)
scene.add(body, trail)
return { body, trail }
}
function updateScene(state: SimulationState | null) {
if (!scene)
return
const environment = (state?.environment ?? {}) as Record<string, unknown>
const cityCenter = Array.isArray(environment.city_center_ned_m) ? environment.city_center_ned_m as number[] : [0, 0, 0]
const center = groundPoint(cityCenter)
const radius = typeof environment.city_radius_m === 'number' ? environment.city_radius_m : 10_000
const defenseRadius = typeof environment.defense_ring_radius_m === 'number' ? environment.defense_ring_radius_m : radius + 8_000
// Static scene guides are replaced when scenario dimensions change.
const oldGuides = scene.getObjectByName('scenario-guides') as Group | undefined
const radarSites = Array.isArray(state?.devices?.radars) ? state.devices.radars as Array<{ id: string, position_ned_m: number[] }> : []
const guideKey = `${radius}:${defenseRadius}:${center.toArray().join(',')}:${radarSites.map(site => site.position_ned_m.join(',')).join(';')}`
if (oldGuides?.userData.key !== guideKey) {
if (oldGuides) {
scene.remove(oldGuides)
oldGuides.traverse((obj) => {
if (obj instanceof Mesh) {
obj.geometry.dispose()
const mat = obj.material
if (Array.isArray(mat)) mat.forEach(m => m.dispose())
else mat.dispose()
}
})
}
const guides = new Group()
guides.name = 'scenario-guides'
guides.userData.key = guideKey
const grid = new GridHelper(Math.max(defenseRadius * 3, 20_000), 40, 0x394738, 0x202a21)
grid.position.set(center.x, -1, center.z)
guides.add(grid)
const city = new Mesh(new RingGeometry(radius - Math.max(radius * 0.006, 50), radius, 96), new MeshStandardMaterial({ color: 0xa9ba75, side: 2, transparent: true, opacity: 0.7 }))
city.rotation.x = -Math.PI / 2
city.position.set(center.x, 2, center.z)
guides.add(city)
const defense = new Mesh(new RingGeometry(defenseRadius - Math.max(radius * 0.004, 40), defenseRadius, 96), new MeshStandardMaterial({ color: 0x547a9a, side: 2, transparent: true, opacity: 0.5 }))
defense.rotation.x = -Math.PI / 2
defense.position.set(center.x, 1, center.z)
guides.add(defense)
for (const site of radarSites) {
const marker = new Mesh(new ConeGeometry(220, 650, 4), new MeshStandardMaterial({ color: 0xf0c76d, emissive: 0x352807 }))
marker.name = site.id
marker.position.copy(groundPoint(site.position_ned_m))
guides.add(marker)
}
scene.add(guides)
const span = Math.max(defenseRadius * 2.9, 30_000)
camera.far = span * 7
camera.position.set(center.x + span * 0.48, span * 0.68, center.z + span * 0.8)
camera.updateProjectionMatrix()
controls.target.copy(center)
controls.update()
}
const currentIds = new Set((state?.entities ?? []).map(entity => entity.id))
for (const [id, item] of entities) {
if (!currentIds.has(id)) {
scene.remove(item.body, item.trail)
item.body.geometry.dispose()
;(item.body.material as MeshStandardMaterial).dispose()
item.trail.geometry.dispose()
;(item.trail.material as LineBasicMaterial).dispose()
entities.delete(id)
}
}
for (const entity of state?.entities ?? []) {
let item = entities.get(entity.id)
if (!item) {
item = makeBody(entity)
entities.set(entity.id, item)
}
item.body.position.copy(groundPoint(entity.position_ned_m))
if (entity.orientation_quaternion_wxyz?.length === 4) {
const [w, x, y, z] = entity.orientation_quaternion_wxyz
const fromNed = new Matrix4().set(0, 1, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 1)
const orientation = new Matrix4().makeRotationFromQuaternion(new Quaternion(x, y, z, w))
item.body.quaternion.setFromRotationMatrix(fromNed.clone().multiply(orientation).multiply(fromNed.clone().invert()))
}
else {
const velocity = groundPoint(entity.velocity_ned_m_s)
if (velocity.lengthSq() > 1e-8)
item.body.quaternion.copy(new Quaternion().setFromUnitVectors(up, velocity.normalize()))
}
item.body.visible = entity.active || entity.type !== 'projectile'
const samples = (entity.trajectory_ned_m ?? []).map(groundPoint)
item.trail.geometry.dispose()
item.trail.geometry = new BufferGeometry().setFromPoints(samples)
}
}
function render() {
frame = requestAnimationFrame(render)
controls.update()
renderer.render(scene, camera)
}
onMounted(() => {
scene = new Scene()
scene.background = null
scene.add(new AmbientLight(0xffffff, 1.6))
const keyLight = new AmbientLight(0xd3e6bc, 1.2)
scene.add(keyLight)
camera = new PerspectiveCamera(42, 1, 1, 250_000)
camera.position.set(28_000, 35_000, 42_000)
renderer = new WebGLRenderer({ antialias: true, alpha: true })
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.75))
renderer.outputColorSpace = 'srgb'
renderer.domElement.className = 'scene-canvas'
host.value!.appendChild(renderer.domElement)
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.075
controls.maxPolarAngle = Math.PI * 0.48
controls.minDistance = 800
controls.maxDistance = 180_000
observer = new ResizeObserver(() => {
const width = host.value!.clientWidth
const height = host.value!.clientHeight
if (!width || !height)
return
camera.aspect = width / height
camera.updateProjectionMatrix()
renderer.setSize(width, height, false)
})
observer.observe(host.value!)
updateScene(props.state)
render()
})
watch(() => props.state, updateScene)
onUnmounted(() => {
cancelAnimationFrame(frame)
observer?.disconnect()
controls?.dispose()
for (const item of entities.values()) {
item.body.geometry.dispose()
;(item.body.material as MeshStandardMaterial).dispose()
item.trail.geometry.dispose()
;(item.trail.material as LineBasicMaterial).dispose()
}
renderer?.dispose()
renderer?.domElement.remove()
})
</script>
<template>
<div ref="host" class="scene-host" />
</template>
<style scoped>
.scene-host { width: 100%; height: 100%; min-height: 420px; }
.scene-host :deep(canvas) { display: block; width: 100%; height: 100%; }
</style>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
const { modelValue } = defineModels<{
modelValue: string
}>()
</script>
<template>
<input
id="input"
v-model="modelValue"
v-bind="$attrs"
type="text"
p="x-4 y-2"
w="250px"
text="center"
bg="transparent"
border="~ rounded gray-200 dark:gray-700"
outline="none active:none"
>
</template>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import type { AccordionRootEmits, AccordionRootProps } from 'reka-ui'
import {
AccordionRoot,
useForwardPropsEmits,
} from 'reka-ui'
const props = defineProps<AccordionRootProps>()
const emits = defineEmits<AccordionRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<AccordionRoot v-bind="forwarded">
<slot />
</AccordionRoot>
</template>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { AccordionContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AccordionContent } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<AccordionContent
v-bind="delegatedProps"
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down text-sm transition-all overflow-hidden"
>
<div :class="cn('pb-4 pt-0', props.class)">
<slot />
</div>
</AccordionContent>
</template>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { AccordionItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AccordionItem, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<AccordionItem
v-bind="forwardedProps"
:class="cn('border-b', props.class)"
>
<slot />
</AccordionItem>
</template>

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import type { AccordionTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ChevronDown } from 'lucide-vue-next'
import {
AccordionHeader,
AccordionTrigger,
} from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<AccordionHeader class="flex">
<AccordionTrigger
v-bind="delegatedProps"
:class="
cn(
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
props.class,
)
"
>
<slot />
<slot name="icon">
<ChevronDown
class="shrink-0 h-4 w-4 transition-transform duration-200"
/>
</slot>
</AccordionTrigger>
</AccordionHeader>
</template>

View File

@@ -0,0 +1,4 @@
export { default as Accordion } from './Accordion.vue'
export { default as AccordionContent } from './AccordionContent.vue'
export { default as AccordionItem } from './AccordionItem.vue'
export { default as AccordionTrigger } from './AccordionTrigger.vue'

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { AlertDialogEmits, AlertDialogProps } from 'reka-ui'
import { AlertDialogRoot, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<AlertDialogProps>()
const emits = defineEmits<AlertDialogEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<AlertDialogRoot v-bind="forwarded">
<slot />
</AlertDialogRoot>
</template>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import type { AlertDialogActionProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AlertDialogAction } from 'reka-ui'
import { buttonVariants } from '@/components/ui/button'
import { cn } from '@/lib/utils'
const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<AlertDialogAction v-bind="delegatedProps" :class="cn(buttonVariants(), props.class)">
<slot />
</AlertDialogAction>
</template>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import type { AlertDialogCancelProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AlertDialogCancel } from 'reka-ui'
import { buttonVariants } from '@/components/ui/button'
import { cn } from '@/lib/utils'
const props = defineProps<AlertDialogCancelProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<AlertDialogCancel
v-bind="delegatedProps"
:class="cn(
buttonVariants({ variant: 'outline' }),
'mt-2 sm:mt-0',
props.class,
)"
>
<slot />
</AlertDialogCancel>
</template>

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
import type { AlertDialogContentEmits, AlertDialogContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import {
AlertDialogContent,
AlertDialogOverlay,
AlertDialogPortal,
useForwardPropsEmits,
} from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<AlertDialogContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<AlertDialogContentEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<AlertDialogPortal>
<AlertDialogOverlay
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-black/80 inset-0 fixed z-50"
/>
<AlertDialogContent
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
props.class,
)
"
>
<slot />
</AlertDialogContent>
</AlertDialogPortal>
</template>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { AlertDialogDescriptionProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import {
AlertDialogDescription,
} from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<AlertDialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<AlertDialogDescription
v-bind="delegatedProps"
:class="cn('text-sm text-muted-foreground', props.class)"
>
<slot />
</AlertDialogDescription>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
:class="
cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
props.class,
)
"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
:class="cn('flex flex-col gap-y-2 text-center sm:text-left', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { AlertDialogTitleProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AlertDialogTitle } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<AlertDialogTitleProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<AlertDialogTitle
v-bind="delegatedProps"
:class="cn('text-lg font-semibold', props.class)"
>
<slot />
</AlertDialogTitle>
</template>

View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
import type { AlertDialogTriggerProps } from 'reka-ui'
import { AlertDialogTrigger } from 'reka-ui'
const props = defineProps<AlertDialogTriggerProps>()
</script>
<template>
<AlertDialogTrigger v-bind="props">
<slot />
</AlertDialogTrigger>
</template>

View File

@@ -0,0 +1,9 @@
export { default as AlertDialog } from './AlertDialog.vue'
export { default as AlertDialogAction } from './AlertDialogAction.vue'
export { default as AlertDialogCancel } from './AlertDialogCancel.vue'
export { default as AlertDialogContent } from './AlertDialogContent.vue'
export { default as AlertDialogDescription } from './AlertDialogDescription.vue'
export { default as AlertDialogFooter } from './AlertDialogFooter.vue'
export { default as AlertDialogHeader } from './AlertDialogHeader.vue'
export { default as AlertDialogTitle } from './AlertDialogTitle.vue'
export { default as AlertDialogTrigger } from './AlertDialogTrigger.vue'

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { AlertVariants } from '.'
import { cn } from '@/lib/utils'
import { alertVariants } from '.'
const props = defineProps<{
class?: HTMLAttributes['class']
variant?: AlertVariants['variant']
}>()
</script>
<template>
<div :class="cn(alertVariants({ variant }), props.class)" role="alert">
<slot />
</div>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('text-sm [&_p]:leading-relaxed', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<h5 :class="cn('mb-1 font-medium leading-none tracking-tight', props.class)">
<slot />
</h5>
</template>

View File

@@ -0,0 +1,24 @@
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
export { default as Alert } from './Alert.vue'
export { default as AlertDescription } from './AlertDescription.vue'
export { default as AlertTitle } from './AlertTitle.vue'
export const alertVariants = cva(
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
{
variants: {
variant: {
default: 'bg-background text-foreground',
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
},
},
defaultVariants: {
variant: 'default',
},
},
)
export type AlertVariants = VariantProps<typeof alertVariants>

View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
import type { AspectRatioProps } from 'reka-ui'
import { AspectRatio } from 'reka-ui'
const props = defineProps<AspectRatioProps>()
</script>
<template>
<AspectRatio v-bind="props">
<slot />
</AspectRatio>
</template>

View File

@@ -0,0 +1 @@
export { default as AspectRatio } from './AspectRatio.vue'

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { AvatarVariants } from '.'
import { AvatarRoot } from 'reka-ui'
import { cn } from '@/lib/utils'
import { avatarVariant } from '.'
const props = withDefaults(defineProps<{
class?: HTMLAttributes['class']
size?: AvatarVariants['size']
shape?: AvatarVariants['shape']
}>(), {
size: 'sm',
shape: 'circle',
})
</script>
<template>
<AvatarRoot :class="cn(avatarVariant({ size, shape }), props.class)">
<slot />
</AvatarRoot>
</template>

View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
import type { AvatarFallbackProps } from 'reka-ui'
import { AvatarFallback } from 'reka-ui'
const props = defineProps<AvatarFallbackProps>()
</script>
<template>
<AvatarFallback v-bind="props">
<slot />
</AvatarFallback>
</template>

View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
import type { AvatarImageProps } from 'reka-ui'
import { AvatarImage } from 'reka-ui'
const props = defineProps<AvatarImageProps>()
</script>
<template>
<AvatarImage v-bind="props" class="h-full w-full object-cover">
<slot />
</AvatarImage>
</template>

View File

@@ -0,0 +1,25 @@
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
export { default as Avatar } from './Avatar.vue'
export { default as AvatarFallback } from './AvatarFallback.vue'
export { default as AvatarImage } from './AvatarImage.vue'
export const avatarVariant = cva(
'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden',
{
variants: {
size: {
sm: 'h-10 w-10 text-xs',
base: 'h-16 w-16 text-2xl',
lg: 'h-32 w-32 text-5xl',
},
shape: {
circle: 'rounded-full',
square: 'rounded-md',
},
},
},
)
export type AvatarVariants = VariantProps<typeof avatarVariant>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { BadgeVariants } from '.'
import { cn } from '@/lib/utils'
import { badgeVariants } from '.'
const props = defineProps<{
variant?: BadgeVariants['variant']
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn(badgeVariants({ variant }), props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,26 @@
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
export { default as Badge } from './Badge.vue'
export const badgeVariants = cva(
'inline-flex gap-1 items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
)
export type BadgeVariants = VariantProps<typeof badgeVariants>

View File

@@ -0,0 +1,13 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<nav aria-label="breadcrumb" :class="props.class">
<slot />
</nav>
</template>

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { MoreHorizontal } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<span
role="presentation"
aria-hidden="true"
:class="cn('flex h-9 w-9 items-center justify-center', props.class)"
>
<slot>
<MoreHorizontal class="h-4 w-4" />
</slot>
<span class="sr-only">More</span>
</span>
</template>

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<li
:class="cn('inline-flex items-center gap-1.5', props.class)"
>
<slot />
</li>
</template>

View File

@@ -0,0 +1,20 @@
<script lang="ts" setup>
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>(), {
as: 'a',
})
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn('transition-colors hover:text-foreground', props.class)"
>
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<ol
:class="cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', props.class)"
>
<slot />
</ol>
</template>

View File

@@ -0,0 +1,19 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<span
role="link"
aria-disabled="true"
aria-current="page"
:class="cn('font-normal text-foreground', props.class)"
>
<slot />
</span>
</template>

View File

@@ -0,0 +1,21 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { ChevronRight } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<li
role="presentation"
aria-hidden="true"
:class="cn('[&>svg]:w-3.5 [&>svg]:h-3.5', props.class)"
>
<slot>
<ChevronRight />
</slot>
</li>
</template>

View File

@@ -0,0 +1,7 @@
export { default as Breadcrumb } from './Breadcrumb.vue'
export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue'
export { default as BreadcrumbItem } from './BreadcrumbItem.vue'
export { default as BreadcrumbLink } from './BreadcrumbLink.vue'
export { default as BreadcrumbList } from './BreadcrumbList.vue'
export { default as BreadcrumbPage } from './BreadcrumbPage.vue'
export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue'

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { ButtonGroupVariants } from '.'
import { cn } from '@/lib/utils'
import { buttonGroupVariants } from '.'
const props = defineProps<{
class?: HTMLAttributes['class']
orientation?: ButtonGroupVariants['orientation']
}>()
</script>
<template>
<div
role="group"
data-slot="button-group"
:data-orientation="props.orientation"
:class="cn(buttonGroupVariants({ orientation: props.orientation }), props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import type { SeparatorProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<SeparatorProps & { class?: HTMLAttributes['class'] }>(), {
orientation: 'vertical',
})
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<Separator
data-slot="button-group-separator"
v-bind="delegatedProps"
:orientation="props.orientation"
:class="cn(
'bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto',
props.class,
)"
/>
</template>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { ButtonGroupVariants } from '.'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
interface Props extends PrimitiveProps {
class?: HTMLAttributes['class']
orientation?: ButtonGroupVariants['orientation']
}
const props = withDefaults(defineProps<Props>(), {
as: 'div',
})
</script>
<template>
<Primitive
role="group"
data-slot="button-group"
:data-orientation="props.orientation"
:as="as"
:as-child="asChild"
:class="cn('bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4', props.class)"
>
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,25 @@
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
export { default as ButtonGroup } from './ButtonGroup.vue'
export { default as ButtonGroupSeparator } from './ButtonGroupSeparator.vue'
export { default as ButtonGroupText } from './ButtonGroupText.vue'
export const buttonGroupVariants = cva(
'flex w-fit items-stretch [&>*:focus-visible]:z-10 [&>*:focus-visible]:relative [&>[data-slot=select-trigger]:not([class*=\'w-\'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2',
{
variants: {
orientation: {
horizontal:
'[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none',
vertical:
'flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none',
},
},
defaultVariants: {
orientation: 'horizontal',
},
},
)
export type ButtonGroupVariants = VariantProps<typeof buttonGroupVariants>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { ButtonVariants } from '.'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '.'
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
})
</script>
<template>
<Primitive
:as="props.as"
:as-child="props.asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,37 @@
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
export { default as Button } from './Button.vue'
export const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
'default': 'h-10 px-4 py-2',
'sm': 'h-9 rounded-md px-3',
'lg': 'h-11 rounded-md px-8',
'icon': 'h-10 w-10',
'icon-sm': 'size-9',
'icon-lg': 'size-11',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
export type ButtonVariants = VariantProps<typeof buttonVariants>

View File

@@ -0,0 +1,58 @@
<script lang="ts" setup>
import type { CalendarRootEmits, CalendarRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarRoot, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNextButton, CalendarPrevButton } from '.'
const props = defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<CalendarRootEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<CalendarRoot
v-slot="{ grid, weekDays }"
:class="cn('p-3', props.class)"
v-bind="forwarded"
>
<CalendarHeader>
<CalendarPrevButton />
<CalendarHeading />
<CalendarNextButton />
</CalendarHeader>
<div class="mt-4 flex flex-col gap-y-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
<CalendarGrid v-for="month in grid" :key="month.value.toString()">
<CalendarGridHead>
<CalendarGridRow>
<CalendarHeadCell
v-for="day in weekDays" :key="day"
>
{{ day }}
</CalendarHeadCell>
</CalendarGridRow>
</CalendarGridHead>
<CalendarGridBody>
<CalendarGridRow v-for="(weekDates, index) in month.rows" :key="`weekDate-${index}`" class="mt-2 w-full">
<CalendarCell
v-for="weekDate in weekDates"
:key="weekDate.toString()"
:date="weekDate"
>
<CalendarCellTrigger
:day="weekDate"
:month="month.value"
/>
</CalendarCell>
</CalendarGridRow>
</CalendarGridBody>
</CalendarGrid>
</div>
</CalendarRoot>
</template>

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
import type { CalendarCellProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarCell, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarCell
:class="cn('relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-view])]:bg-accent/50', props.class)"
v-bind="forwardedProps"
>
<slot />
</CalendarCell>
</template>

View File

@@ -0,0 +1,36 @@
<script lang="ts" setup>
import type { CalendarCellTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarCellTrigger, useForwardProps } from 'reka-ui'
import { buttonVariants } from '@/components/ui/button'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarCellTrigger
:class="cn(
buttonVariants({ variant: 'ghost' }),
'h-9 w-9 p-0 font-normal',
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
// Selected
'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:opacity-100 data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
// Disabled
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
// Unavailable
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
// Outside months
'data-[outside-view]:text-muted-foreground data-[outside-view]:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
props.class,
)"
v-bind="forwardedProps"
>
<slot />
</CalendarCellTrigger>
</template>

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
import type { CalendarGridProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarGrid, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarGridProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarGrid
:class="cn('w-full border-collapse space-y-1', props.class)"
v-bind="forwardedProps"
>
<slot />
</CalendarGrid>
</template>

View File

@@ -0,0 +1,12 @@
<script lang="ts" setup>
import type { CalendarGridBodyProps } from 'reka-ui'
import { CalendarGridBody } from 'reka-ui'
const props = defineProps<CalendarGridBodyProps>()
</script>
<template>
<CalendarGridBody v-bind="props">
<slot />
</CalendarGridBody>
</template>

View File

@@ -0,0 +1,12 @@
<script lang="ts" setup>
import type { CalendarGridHeadProps } from 'reka-ui'
import { CalendarGridHead } from 'reka-ui'
const props = defineProps<CalendarGridHeadProps>()
</script>
<template>
<CalendarGridHead v-bind="props">
<slot />
</CalendarGridHead>
</template>

View File

@@ -0,0 +1,19 @@
<script lang="ts" setup>
import type { CalendarGridRowProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarGridRow, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarGridRowProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarGridRow :class="cn('flex', props.class)" v-bind="forwardedProps">
<slot />
</CalendarGridRow>
</template>

View File

@@ -0,0 +1,19 @@
<script lang="ts" setup>
import type { CalendarHeadCellProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarHeadCell, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeadCellProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeadCell :class="cn('w-9 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)" v-bind="forwardedProps">
<slot />
</CalendarHeadCell>
</template>

View File

@@ -0,0 +1,19 @@
<script lang="ts" setup>
import type { CalendarHeaderProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarHeader, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeaderProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeader :class="cn('relative flex w-full items-center justify-between pt-1', props.class)" v-bind="forwardedProps">
<slot />
</CalendarHeader>
</template>

Some files were not shown because too many files have changed in this diff Show More