()
+ const reachable: LatLon[] = []
+ const queue: string[] = [startKey]
visited.add(startKey)
// BFS flood fill through grid, capped at maxKm from origin
while (queue.length > 0) {
- const key = queue.shift()
+ const key = queue.shift()!
const [latStr, lonStr] = key.split(",")
const lat = parseFloat(latStr)
const lon = parseFloat(lonStr)
@@ -157,7 +312,7 @@ function propagationReach(gridLookup, startLat, startLon, minScore) {
reachable.push({ lat, lon })
// Check 4 neighbors
- const neighbors = [
+ const neighbors: [number, number][] = [
[lat + step, lon],
[lat - step, lon],
[lat, lon + step],
@@ -180,19 +335,19 @@ function propagationReach(gridLookup, startLat, startLon, minScore) {
return convexHull(reachable)
}
-function convexHull(points) {
+function convexHull(points: LatLon[]): LatLon[] {
// Graham scan
points.sort((a, b) => a.lon - b.lon || a.lat - b.lat)
- const cross = (o, a, b) =>
+ const cross = (o: LatLon, a: LatLon, b: LatLon): number =>
(a.lon - o.lon) * (b.lat - o.lat) - (a.lat - o.lat) * (b.lon - o.lon)
- const lower = []
+ const lower: LatLon[] = []
for (const p of points) {
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0)
lower.pop()
lower.push(p)
}
- const upper = []
+ const upper: LatLon[] = []
for (let i = points.length - 1; i >= 0; i--) {
const p = points[i]
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0)
@@ -204,16 +359,16 @@ function convexHull(points) {
return lower.concat(upper)
}
-function isMobile() {
+function isMobile(): boolean {
return window.innerWidth < 768
}
-function mobileHandleHTML() {
+function mobileHandleHTML(): string {
if (!isMobile()) return ""
return ``
}
-function buildLoadingHTML(detail) {
+function buildLoadingHTML(detail: PointDetail): string {
const tier = scoreTier(detail.score)
return `
${mobileHandleHTML()}
@@ -230,7 +385,7 @@ function buildLoadingHTML(detail) {
`
}
-function buildForecastSvg(forecast) {
+function buildForecastSvg(forecast: ForecastPoint[]): string {
if (!forecast || forecast.length < 2) return ""
const marginL = 28, marginR = 6, marginT = 4, marginB = 16
@@ -240,7 +395,7 @@ function buildForecastSvg(forecast) {
const n = forecast.length
const now = new Date()
- const points = forecast.map((f, i) => {
+ const points: SvgPoint[] = forecast.map((f, i) => {
const x = marginL + (i / (n - 1)) * plotW
const y = marginT + plotH * (1 - f.score / 100)
return { x, y, score: f.score, time: new Date(f.time) }
@@ -250,7 +405,7 @@ function buildForecastSvg(forecast) {
// "Now" dot
const nowIdx = points.reduce((best, p, i) =>
- Math.abs(p.time - now) < Math.abs(points[best].time - now) ? i : best, 0)
+ Math.abs(p.time.getTime() - now.getTime()) < Math.abs(points[best].time.getTime() - now.getTime()) ? i : best, 0)
const nowPt = points[nowIdx]
// Trend (from now onward)
@@ -302,7 +457,7 @@ function buildForecastSvg(forecast) {
`
}
-function buildPopupHTML(detail, viewshedLoading) {
+function buildPopupHTML(detail: PointDetail, viewshedLoading: boolean): string {
const tier = scoreTier(detail.score)
const range = rangeEstimate(detail.score, detail)
const time = new Date(detail.valid_time).toISOString().replace("T", " ").slice(0, 16) + " UTC"
@@ -311,7 +466,7 @@ function buildPopupHTML(detail, viewshedLoading) {
let explanations = ""
for (const key of FACTOR_ORDER) {
const meta = FACTOR_META[key]
- const value = detail.factors[key]
+ const value = detail.factors[key] as number | undefined
if (value === undefined) continue
const pct = Math.round(meta.weight * 100)
rows += `
@@ -364,10 +519,10 @@ function buildPopupHTML(detail, viewshedLoading) {
`
}
-function buildScatterHTML(scatter) {
+function buildScatterHTML(scatter: RainScatter): string {
const cls = scatter.classification
const cells = scatter.cells
- const clsColors = { excellent: "#059669", good: "#0d9488", marginal: "#ca8a04", none: "#666" }
+ const clsColors: Record = { excellent: "#059669", good: "#0d9488", marginal: "#ca8a04", none: "#666" }
const clsColor = clsColors[cls] || "#666"
const clsLabel = cls.charAt(0).toUpperCase() + cls.slice(1)
@@ -390,13 +545,14 @@ function buildScatterHTML(scatter) {
`
}
-function bearingLabel(deg) {
+function bearingLabel(deg: number): string {
const dirs = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
return dirs[Math.round(deg / 22.5) % 16]
}
-function buildDuctInfoHTML(info) {
+function buildDuctInfoHTML(info: DuctInfo): string {
const layers = info.ducts || []
+
let layerRows = ""
if (layers.length > 0) {
layerRows = layers.map((d, i) => {
@@ -417,13 +573,24 @@ function buildDuctInfoHTML(info) {
// --- Hook ---
-export const PropagationMap = {
- mounted() {
- const centerLat = parseFloat(this.el.dataset.centerLat)
- const centerLon = parseFloat(this.el.dataset.centerLon)
- const zoom = parseInt(this.el.dataset.zoom, 10)
+export const PropagationMap: Record & {
+ mounted(this: PropagationMapHook): void
+ showDetailPanel(this: PropagationMapHook): void
+ hideDetailPanel(this: PropagationMapHook): void
+ renderScores(this: PropagationMapHook, scores: ScorePoint[]): void
+ interpolateScore(this: PropagationMapHook, lat: number, lon: number, step: number): number | null
+ scoreColorRGB(this: PropagationMapHook, score: number): RGB
+ lookupPoint(this: PropagationMapHook, lat: number, lon: number): (ScorePoint & BandInfo) | null
+ renderTimeline(this: PropagationMapHook): void
+ sendBounds(this: PropagationMapHook): void
+ destroyed(this: PropagationMapHook): void
+} = {
+ mounted(this: PropagationMapHook) {
+ const centerLat = parseFloat(this.el.dataset.centerLat!)
+ const centerLon = parseFloat(this.el.dataset.centerLon!)
+ const zoom = parseInt(this.el.dataset.zoom!, 10)
- const center =
+ const center: [number, number] =
Number.isFinite(centerLat) && Number.isFinite(centerLon)
? [centerLat, centerLon]
: [32.897, -97.038]
@@ -452,12 +619,12 @@ export const PropagationMap = {
]
// Render pre-loaded scores immediately (no round-trip needed)
- const initialScores = JSON.parse(this.el.dataset.scores || "[]")
+ const initialScores: ScorePoint[] = JSON.parse(this.el.dataset.scores || "[]")
if (initialScores.length > 0) {
this.renderScores(initialScores)
}
- this.handleEvent("update_scores", ({ scores }) => {
+ this.handleEvent("update_scores", ({ scores }: { scores: ScorePoint[] }) => {
topbar.hide()
this.renderScores(scores)
@@ -470,7 +637,7 @@ export const PropagationMap = {
this.bandInfo = JSON.parse(this.el.dataset.bandInfo || "{}")
this.rangeCircles = L.layerGroup().addTo(this.map)
- this.handleEvent("update_band_info", ({ band_info }) => {
+ this.handleEvent("update_band_info", ({ band_info }: { band_info: BandInfo }) => {
this.bandInfo = band_info
})
@@ -496,7 +663,7 @@ export const PropagationMap = {
this.gridVisible = false
// Listen for grid toggle from LiveView
- this.handleEvent("toggle_grid", ({ visible }) => {
+ this.handleEvent("toggle_grid", ({ visible }: { visible: boolean }) => {
this.gridVisible = visible
if (visible) {
this.gridLayer.addTo(this.map)
@@ -507,7 +674,7 @@ export const PropagationMap = {
})
// Click on map to request viewshed + factor detail from server
- this.map.on("click", (e) => {
+ this.map.on("click", (e: L.LeafletMouseEvent) => {
this.rangeCircles.clearLayers()
this.viewshedLoading = true
this.lastDetail = null
@@ -531,7 +698,7 @@ export const PropagationMap = {
// Show panel immediately with whatever we have client-side
const basic = this.lookupPoint(e.latlng.lat, e.latlng.lng)
if (basic && this.detailPanel) {
- const merged = { ...basic, ...this.bandInfo, factors: null }
+ const merged = { ...basic, ...this.bandInfo, factors: null } as unknown as PointDetail
this.detailPanel.innerHTML = buildLoadingHTML(merged)
this.showDetailPanel()
this.pushEvent("point_detail", { lat: e.latlng.lat, lon: e.latlng.lng })
@@ -547,17 +714,17 @@ export const PropagationMap = {
L.DomEvent.disableScrollPropagation(this.detailPanel)
// Close button event delegation
- this.detailPanel.addEventListener("click", (e) => {
- if (e.target.closest(".detail-close-btn")) {
+ this.detailPanel.addEventListener("click", (e: MouseEvent) => {
+ if ((e.target as HTMLElement).closest(".detail-close-btn")) {
this.hideDetailPanel()
}
})
}
this.lastDetail = null
- this.handleEvent("point_detail", (detail) => {
+ this.handleEvent("point_detail", (detail: PointDetail) => {
if (detail && this.detailPanel) {
- const merged = { ...detail, ...this.bandInfo }
+ const merged: PointDetail = { ...detail, ...this.bandInfo }
this.lastDetail = merged
this.detailPanel.innerHTML = buildPopupHTML(merged, this.viewshedLoading)
this.showDetailPanel()
@@ -581,7 +748,7 @@ export const PropagationMap = {
opacity: opacity,
interactive: false
})
- this.scatterMarkers.addLayer(marker)
+ this.scatterMarkers!.addLayer(marker)
}
}
@@ -599,7 +766,7 @@ export const PropagationMap = {
}
const tier = scoreTier(detail.score)
this.reachPolygon = L.polygon(
- hull.map(p => [p.lat, p.lon]),
+ hull.map(p => [p.lat, p.lon] as [number, number]),
{
color: tier.color,
weight: 2,
@@ -616,7 +783,7 @@ export const PropagationMap = {
}
})
- this.handleEvent("viewshed_result", ({ origin, points }) => {
+ this.handleEvent("viewshed_result", ({ origin, points }: ViewshedResult) => {
this.rangeCircles.clearLayers()
this.viewshedLoading = false
@@ -626,7 +793,7 @@ export const PropagationMap = {
}
if (points && points.length > 0) {
- const latlngs = points.map(p => [p.lat, p.lon])
+ const latlngs = points.map(p => [p.lat, p.lon] as [number, number])
L.polygon(latlngs, {
color: "#222",
@@ -640,7 +807,7 @@ export const PropagationMap = {
}
// Always redraw center marker at clicked location
- const loc = this.clickedLatLng || [origin.lat, origin.lon]
+ const loc: [number, number] = this.clickedLatLng || [origin.lat, origin.lon]
const basic = this.lookupPoint(loc[0], loc[1])
const markerColor = basic ? scoreTier(basic.score).color : "#888"
@@ -663,7 +830,7 @@ export const PropagationMap = {
this.renderTimeline()
}
- this.handleEvent("update_timeline", ({ times, selected }) => {
+ this.handleEvent("update_timeline", ({ times, selected }: { times: TimelineEntry[]; selected: string }) => {
this.timelineData = times
this.selectedTime = selected
this.renderTimeline()
@@ -676,7 +843,7 @@ export const PropagationMap = {
}
// Escape key closes detail panel and range circles
- document.addEventListener("keydown", (e) => {
+ document.addEventListener("keydown", (e: KeyboardEvent) => {
if (e.key === "Escape") {
this.hideDetailPanel()
}
@@ -697,7 +864,7 @@ export const PropagationMap = {
legend.onAdd = () => {
const div = L.DomUtil.create("div")
const mobile = isMobile()
- const rows = [
+ const rows: [string, string, string][] = [
["#00ffa3", "Excellent", "80+"],
["#7dffd4", "Good", "65"],
["#ffe566", "Marginal", "50"],
@@ -721,12 +888,12 @@ export const PropagationMap = {
legend.addTo(this.map)
},
- showDetailPanel() {
+ showDetailPanel(this: PropagationMapHook) {
if (!this.detailPanel) return
this.detailPanel.style.display = "block"
},
- hideDetailPanel() {
+ hideDetailPanel(this: PropagationMapHook) {
if (this.detailPanel) {
this.detailPanel.style.display = "none"
this.detailPanel.innerHTML = ""
@@ -737,7 +904,7 @@ export const PropagationMap = {
this.lastDetail = null
},
- renderScores(scores) {
+ renderScores(this: PropagationMapHook, scores: ScorePoint[]) {
this.scores = scores
if (this.scoreOverlay) {
this.map.removeLayer(this.scoreOverlay)
@@ -753,25 +920,26 @@ export const PropagationMap = {
})
// Pre-calculate RGBA strings for scores 0-100
- const colorCache = new Array(101)
+ const colorCache = new Array(101)
for (let i = 0; i <= 100; i++) {
const c = this.scoreColorRGB(i)
colorCache[i] = `rgba(${c.r},${c.g},${c.b},0.55)`
}
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this
- this.scoreOverlay = L.GridLayer.extend({
- createTile(coords) {
+ this.scoreOverlay = new (L.GridLayer.extend({
+ createTile(this: L.GridLayer, coords: L.Coords) {
const tile = document.createElement("canvas")
const size = this.getTileSize()
tile.width = size.x
tile.height = size.y
- const ctx = tile.getContext("2d")
+ const ctx = tile.getContext("2d")!
const step = 0.125
const nwPoint = coords.scaleBy(size)
- const nw = this._map.unproject(nwPoint, coords.z)
- const se = this._map.unproject(nwPoint.add(size), coords.z)
+ const nw = (this as any)._map.unproject(nwPoint, coords.z) as L.LatLng
+ const se = (this as any)._map.unproject(nwPoint.add(size), coords.z) as L.LatLng
const latPerPx = (nw.lat - se.lat) / size.y
const lonPerPx = (se.lng - nw.lng) / size.x
@@ -779,7 +947,7 @@ export const PropagationMap = {
const cellPxY = Math.max(1, Math.ceil(step / Math.abs(latPerPx)))
const absLatPerPx = Math.abs(latPerPx)
- let lastColor = null
+ let lastColor: string | null = null
for (let py = 0; py < size.y; py += cellPxY) {
const lat = nw.lat - py * absLatPerPx
for (let px = 0; px < size.x; px += cellPxX) {
@@ -797,16 +965,14 @@ export const PropagationMap = {
}
return tile
}
- })
-
- this.scoreOverlay = new (this.scoreOverlay)({ opacity: 1.0 })
+ }))({ opacity: 1.0 }) as L.GridLayer
this.scoreOverlay.addTo(this.map)
},
- interpolateScore(lat, lon, step) {
+ interpolateScore(this: PropagationMapHook, lat: number, lon: number, step: number): number | null {
const rlat = (Math.round(lat / step) * step).toFixed(3)
const rlon = (Math.round(lon / step) * step).toFixed(3)
- if (this.gridLookup.has(`${rlat},${rlon}`)) return this.gridLookup.get(`${rlat},${rlon}`)
+ if (this.gridLookup.has(`${rlat},${rlon}`)) return this.gridLookup.get(`${rlat},${rlon}`)!
const lat0 = Math.floor(lat / step) * step
const lat1 = lat0 + step
@@ -818,21 +984,21 @@ export const PropagationMap = {
const s01 = this.gridLookup.get(`${lat0.toFixed(3)},${lon1.toFixed(3)}`)
const s11 = this.gridLookup.get(`${lat1.toFixed(3)},${lon1.toFixed(3)}`)
- const corners = [s00, s10, s01, s11].filter(s => s !== undefined)
+ const corners = [s00, s10, s01, s11].filter((s): s is number => s !== undefined)
if (corners.length < 2) return null
if (corners.length === 4) {
const tLat = (lat - lat0) / step
const tLon = (lon - lon0) / step
return Math.round(
- s00 * (1 - tLat) * (1 - tLon) + s10 * tLat * (1 - tLon) +
- s01 * (1 - tLat) * tLon + s11 * tLat * tLon
+ s00! * (1 - tLat) * (1 - tLon) + s10! * tLat * (1 - tLon) +
+ s01! * (1 - tLat) * tLon + s11! * tLat * tLon
)
}
return Math.round(corners.reduce((a, b) => a + b, 0) / corners.length)
},
- scoreColorRGB(score) {
+ scoreColorRGB(this: PropagationMapHook, score: number): RGB {
for (let i = 0; i < this.colorScale.length - 1; i++) {
const upper = this.colorScale[i]
const lower = this.colorScale[i + 1]
@@ -850,7 +1016,7 @@ export const PropagationMap = {
return { r: last.r, g: last.g, b: last.b }
},
- lookupPoint(lat, lon) {
+ lookupPoint(this: PropagationMapHook, lat: number, lon: number): (ScorePoint & BandInfo) | null {
if (!this.gridData) return null
const step = 0.125
const rlat = (Math.round(lat / step) * step).toFixed(3)
@@ -860,7 +1026,7 @@ export const PropagationMap = {
return { ...data, ...this.bandInfo }
},
- renderTimeline() {
+ renderTimeline(this: PropagationMapHook) {
if (!this.timelineEl || this.timelineData.length === 0) {
if (this.timelineEl) this.timelineEl.style.display = "none"
return
@@ -882,7 +1048,7 @@ export const PropagationMap = {
// Hide timeline if all times are within 1 hour (no real forecast spread)
const firstDt = new Date(this.timelineData[0].time)
const lastDt = new Date(this.timelineData[this.timelineData.length - 1].time)
- if (lastDt - firstDt < 3600000) {
+ if (lastDt.getTime() - firstDt.getTime() < 3600000) {
const dt = firstDt
const utcLabel = `${dt.getUTCHours().toString().padStart(2, "0")}:00 UTC`
const dateLabel = `${dt.getUTCFullYear()}-${(dt.getUTCMonth() + 1).toString().padStart(2, "0")}-${dt.getUTCDate().toString().padStart(2, "0")}`
@@ -895,17 +1061,17 @@ export const PropagationMap = {
}
const now = new Date()
- const items = this.timelineData.map((t, idx) => {
+ const items: TimelineItem[] = this.timelineData.map((t, idx) => {
const dt = new Date(t.time)
const isSelected = t.time === this.selectedTime
- const isPast = dt < new Date(now - 1800000) // 30min grace
- const offsetH = Math.round((dt - now) / 3600000)
- return { ...t, dt, isSelected, isPast, offsetH, idx }
+ const isPast = dt.getTime() < now.getTime() - 1800000 // 30min grace
+ const offsetH = Math.round((dt.getTime() - now.getTime()) / 3600000)
+ return { ...t, dt, isSelected, isPast, offsetH, idx, label: "" }
})
// "Now" = the item closest to current time
const closestIdx = items.reduce((best, item, i) =>
- Math.abs(item.dt - now) < Math.abs(items[best].dt - now) ? i : best, 0)
+ Math.abs(item.dt.getTime() - now.getTime()) < Math.abs(items[best].dt.getTime() - now.getTime()) ? i : best, 0)
items.forEach((t, i) => {
if (i === closestIdx) {
@@ -946,17 +1112,17 @@ export const PropagationMap = {
`
// Attach click handlers
- this.timelineEl.querySelectorAll("button[data-time]").forEach(btn => {
+ this.timelineEl.querySelectorAll("button[data-time]").forEach(btn => {
btn.addEventListener("click", () => {
- this.selectedTime = btn.dataset.time
+ this.selectedTime = btn.dataset.time!
this.renderTimeline()
topbar.show()
- this.pushEvent("select_time", { time: btn.dataset.time })
+ this.pushEvent("select_time", { time: btn.dataset.time! })
})
})
},
- sendBounds() {
+ sendBounds(this: PropagationMapHook) {
topbar.show()
const b = this.map.getBounds()
this.pushEvent("map_bounds", {
@@ -967,7 +1133,7 @@ export const PropagationMap = {
})
},
- destroyed() {
+ destroyed(this: PropagationMapHook) {
if (this.map) this.map.remove()
}
}
diff --git a/assets/js/rover_map_hook.js b/assets/js/rover_map_hook.ts
similarity index 73%
rename from assets/js/rover_map_hook.js
rename to assets/js/rover_map_hook.ts
index 372334da..ed877375 100644
--- a/assets/js/rover_map_hook.js
+++ b/assets/js/rover_map_hook.ts
@@ -1,5 +1,44 @@
-export const RoverMap = {
- mounted() {
+interface Score {
+ lat: number
+ lon: number
+ score: number
+}
+
+interface Station {
+ lat: number
+ lon: number
+ label: string
+}
+
+interface ColorScaleEntry {
+ min: number
+ r: number
+ g: number
+ b: number
+}
+
+interface RGB {
+ r: number
+ g: number
+ b: number
+}
+
+interface RoverMapHook extends ViewHook {
+ stations: Station[]
+ stationMarkers: L.LayerGroup
+ scoreOverlay: L.GridLayer | null
+ gridLookup: Map
+ colorScale: ColorScaleEntry[]
+ map: L.Map
+ renderScores(this: RoverMapHook, scores: Score[]): void
+ interpolateScore(this: RoverMapHook, lat: number, lon: number, step: number): number | null
+ scoreColorRGB(this: RoverMapHook, score: number): RGB
+ renderStations(this: RoverMapHook): void
+ fitBounds(this: RoverMapHook): void
+}
+
+export const RoverMap: RoverMapHook = {
+ mounted(this: RoverMapHook) {
this.stations = []
this.stationMarkers = L.layerGroup()
this.scoreOverlay = null
@@ -28,7 +67,7 @@ export const RoverMap = {
this.stationMarkers.addTo(map)
// Render pre-loaded propagation scores
- const initialScores = JSON.parse(this.el.dataset.scores || "[]")
+ const initialScores: Score[] = JSON.parse(this.el.dataset.scores || "[]")
if (initialScores.length > 0) {
this.renderScores(initialScores)
}
@@ -46,11 +85,11 @@ export const RoverMap = {
pushBounds()
})
- this.handleEvent("update_scores", ({ scores }) => {
+ this.handleEvent("update_scores", ({ scores }: { scores: Score[] }) => {
this.renderScores(scores)
})
- this.handleEvent("stations_updated", ({ stations }) => {
+ this.handleEvent("stations_updated", ({ stations }: { stations: Station[] }) => {
this.stations = stations
this.renderStations()
})
@@ -59,7 +98,7 @@ export const RoverMap = {
// --- Propagation score overlay (same as main map) ---
- renderScores(scores) {
+ renderScores(this: RoverMapHook, scores: Score[]) {
if (this.scoreOverlay) {
this.map.removeLayer(this.scoreOverlay)
this.scoreOverlay = null
@@ -72,7 +111,7 @@ export const RoverMap = {
this.gridLookup.set(key, s.score)
})
- const colorCache = new Array(101)
+ const colorCache = new Array(101)
for (let i = 0; i <= 100; i++) {
const c = this.scoreColorRGB(i)
colorCache[i] = `rgba(${c.r},${c.g},${c.b},0.55)`
@@ -80,12 +119,12 @@ export const RoverMap = {
const self = this
const OverlayClass = L.GridLayer.extend({
- createTile(coords) {
+ createTile(coords: L.Coords) {
const tile = document.createElement("canvas")
const size = this.getTileSize()
tile.width = size.x
tile.height = size.y
- const ctx = tile.getContext("2d")
+ const ctx = tile.getContext("2d")!
const step = 0.125
const nwPoint = coords.scaleBy(size)
@@ -98,7 +137,7 @@ export const RoverMap = {
const cellPxY = Math.max(1, Math.ceil(step / Math.abs(latPerPx)))
const absLatPerPx = Math.abs(latPerPx)
- let lastColor = null
+ let lastColor: string | null = null
for (let py = 0; py < size.y; py += cellPxY) {
const lat = nw.lat - py * absLatPerPx
for (let px = 0; px < size.x; px += cellPxX) {
@@ -118,14 +157,14 @@ export const RoverMap = {
}
})
- this.scoreOverlay = new OverlayClass({ opacity: 1.0 })
- this.scoreOverlay.addTo(this.map)
+ this.scoreOverlay = new OverlayClass({ opacity: 1.0 }) as L.GridLayer
+ this.scoreOverlay!.addTo(this.map)
},
- interpolateScore(lat, lon, step) {
+ interpolateScore(this: RoverMapHook, lat: number, lon: number, step: number): number | null {
const rlat = (Math.round(lat / step) * step).toFixed(3)
const rlon = (Math.round(lon / step) * step).toFixed(3)
- if (this.gridLookup.has(`${rlat},${rlon}`)) return this.gridLookup.get(`${rlat},${rlon}`)
+ if (this.gridLookup.has(`${rlat},${rlon}`)) return this.gridLookup.get(`${rlat},${rlon}`)!
const lat0 = Math.floor(lat / step) * step
const lat1 = lat0 + step
@@ -137,21 +176,21 @@ export const RoverMap = {
const s01 = this.gridLookup.get(`${lat0.toFixed(3)},${lon1.toFixed(3)}`)
const s11 = this.gridLookup.get(`${lat1.toFixed(3)},${lon1.toFixed(3)}`)
- const corners = [s00, s10, s01, s11].filter(s => s !== undefined)
+ const corners = [s00, s10, s01, s11].filter((s): s is number => s !== undefined)
if (corners.length < 2) return null
if (corners.length === 4) {
const tLat = (lat - lat0) / step
const tLon = (lon - lon0) / step
return Math.round(
- s00 * (1 - tLat) * (1 - tLon) + s10 * tLat * (1 - tLon) +
- s01 * (1 - tLat) * tLon + s11 * tLat * tLon
+ s00! * (1 - tLat) * (1 - tLon) + s10! * tLat * (1 - tLon) +
+ s01! * (1 - tLat) * tLon + s11! * tLat * tLon
)
}
return Math.round(corners.reduce((a, b) => a + b, 0) / corners.length)
},
- scoreColorRGB(score) {
+ scoreColorRGB(this: RoverMapHook, score: number): RGB {
for (let i = 0; i < this.colorScale.length - 1; i++) {
const upper = this.colorScale[i]
const lower = this.colorScale[i + 1]
@@ -171,7 +210,7 @@ export const RoverMap = {
// --- Station rendering ---
- renderStations() {
+ renderStations(this: RoverMapHook) {
this.stationMarkers.clearLayers()
for (const s of this.stations) {
@@ -198,12 +237,12 @@ export const RoverMap = {
this.fitBounds()
},
- fitBounds() {
- const points = this.stations.map(s => [s.lat, s.lon])
+ fitBounds(this: RoverMapHook) {
+ const points: [number, number][] = this.stations.map(s => [s.lat, s.lon])
if (points.length >= 2) {
this.map.fitBounds(points, { padding: [60, 60], maxZoom: 9 })
} else if (points.length === 1) {
this.map.setView(points[0], 8)
}
}
-}
+} as unknown as RoverMapHook
diff --git a/assets/js/types/global.d.ts b/assets/js/types/global.d.ts
new file mode 100644
index 00000000..08a946a9
--- /dev/null
+++ b/assets/js/types/global.d.ts
@@ -0,0 +1,92 @@
+// Global type declarations for vendor libraries and Phoenix LiveView
+
+// Leaflet is assigned to window.L in app.ts
+declare const L: typeof import("leaflet")
+
+declare module "leaflet" {
+ interface Map {
+ createPane(name: string): HTMLElement
+ }
+
+ interface GridLayerOptions {
+ opacity?: number
+ }
+
+ function gridLayer(options?: GridLayerOptions): GridLayer
+}
+
+// Phoenix LiveView hook interface
+interface ViewHook {
+ el: HTMLElement
+ liveSocket: any
+ pushEvent(event: string, payload: Record): void
+ pushEventTo(
+ selectorOrTarget: string | HTMLElement,
+ event: string,
+ payload: Record
+ ): void
+ handleEvent(event: string, callback: (payload: any) => void): void
+ upload(name: string, files: FileList): void
+}
+
+// Vendor module declarations
+declare module "../vendor/topbar" {
+ const topbar: {
+ config(opts: { barColors: Record; shadowColor: string }): void
+ show(delay?: number): void
+ hide(): void
+ }
+ export default topbar
+}
+
+declare module "../vendor/chart.js" {
+ export default class Chart {
+ constructor(ctx: CanvasRenderingContext2D, config: any)
+ destroy(): void
+ options: any
+ data: any
+ }
+}
+
+declare module "../vendor/leaflet/leaflet.js" {
+ export = L
+}
+
+declare module "phoenix" {
+ export class Socket {
+ constructor(url: string, opts?: Record)
+ }
+}
+
+declare module "phoenix_html" {
+ const _default: void
+ export default _default
+}
+
+declare module "phoenix_live_view" {
+ import { Socket } from "phoenix"
+
+ export class LiveSocket {
+ constructor(url: string, socketClass: typeof Socket, opts?: Record)
+ connect(): void
+ enableDebug(): void
+ enableLatencySim(ms: number): void
+ disableLatencySim(): void
+ }
+}
+
+declare module "phoenix-colocated/microwaveprop" {
+ export const hooks: Record
+}
+
+declare module "../../deps/live_stash/assets/js/live-stash.js" {
+ export default function initLiveStash(
+ params: Record
+ ): (params: Record) => Record
+}
+
+interface Window {
+ L: typeof import("leaflet")
+ liveSocket: any
+ liveReloader: any
+}
diff --git a/assets/js/weather_map_hook.js b/assets/js/weather_map_hook.ts
similarity index 76%
rename from assets/js/weather_map_hook.js
rename to assets/js/weather_map_hook.ts
index 2602bfd4..1bb364ed 100644
--- a/assets/js/weather_map_hook.js
+++ b/assets/js/weather_map_hook.ts
@@ -1,6 +1,99 @@
-// assets/js/weather_map_hook.js
+// assets/js/weather_map_hook.ts
-function isMobile() {
+// --- Type definitions ---
+
+interface RGB {
+ r: number
+ g: number
+ b: number
+}
+
+interface Breakpoint extends RGB {
+ value: number
+}
+
+interface ContinuousColorScale {
+ breakpoints: Breakpoint[]
+ trueColor?: never
+ format: (v: number | null) => string
+ label: string
+ unit: string
+}
+
+interface BooleanColorScale {
+ breakpoints: null
+ trueColor: RGB
+ format: (v: boolean | null) => string
+ label: string
+ unit: string
+}
+
+type ColorScale = ContinuousColorScale | BooleanColorScale
+
+type LayerId = keyof typeof COLOR_SCALES
+
+interface WeatherPoint {
+ lat: number
+ lon: number
+ valid_time: string | null
+ temperature: number | null
+ dewpoint_depression: number | null
+ surface_rh: number | null
+ surface_pressure_mb: number | null
+ surface_refractivity: number | null
+ refractivity_gradient: number | null
+ bl_height: number | null
+ pwat: number | null
+ temp_850mb: number | null
+ dewpoint_850mb: number | null
+ lapse_rate: number | null
+ inversion_strength: number | null
+ inversion_base_m: number | null
+ ducting: boolean | null
+ duct_base_m: number | null
+ duct_strength: number | null
+ [key: string]: unknown
+}
+
+interface DetailRow {
+ label?: string
+ value?: string
+ color?: string
+ separator?: boolean
+}
+
+interface MapBounds {
+ south: number
+ north: number
+ west: number
+ east: number
+}
+
+interface WeatherMapHook extends ViewHook {
+ map: L.Map
+ weatherOverlay: L.GridLayer | null
+ weatherData: WeatherPoint[]
+ gridLookup: Map
+ selectedLayer: LayerId
+ detailPanel: HTMLElement | null
+ escHint: HTMLDivElement
+ clickMarker: L.LayerGroup
+ legend: L.Control
+ _escHandler: (e: KeyboardEvent) => void
+ _layerObserver: MutationObserver
+
+ mounted(this: WeatherMapHook): void
+ destroyed(this: WeatherMapHook): void
+ sendBounds(this: WeatherMapHook): void
+ positionDetailPanel(this: WeatherMapHook): void
+ buildLookup(this: WeatherMapHook): void
+ renderLayer(this: WeatherMapHook): void
+ buildLegend(this: WeatherMapHook): HTMLElement
+}
+
+// --- Helpers ---
+
+function isMobile(): boolean {
return window.innerWidth < 768
}
@@ -13,7 +106,7 @@ const COLOR_SCALES = {
{ value: -100, r: 255, g: 144, b: 68 },
{ value: 0, r: 255, g: 79, b: 79 }
],
- format: v => v != null ? `${v.toFixed(0)} N/km` : "N/A",
+ format: (v: number | null): string => v != null ? `${v.toFixed(0)} N/km` : "N/A",
label: "Refractivity Gradient",
unit: "N/km"
},
@@ -25,7 +118,7 @@ const COLOR_SCALES = {
{ value: 2000, r: 255, g: 144, b: 68 },
{ value: 3000, r: 255, g: 79, b: 79 }
],
- format: v => v != null ? `${v.toFixed(0)} m` : "N/A",
+ format: (v: number | null): string => v != null ? `${v.toFixed(0)} m` : "N/A",
label: "Boundary Layer Height",
unit: "m"
},
@@ -37,7 +130,7 @@ const COLOR_SCALES = {
{ value: 20, r: 255, g: 144, b: 68 },
{ value: 30, r: 255, g: 79, b: 79 }
],
- format: v => v != null ? `${v.toFixed(1)} \u00b0C` : "N/A",
+ format: (v: number | null): string => v != null ? `${v.toFixed(1)} \u00b0C` : "N/A",
label: "Dewpoint Depression (T - Td)",
unit: "\u00b0C"
},
@@ -49,7 +142,7 @@ const COLOR_SCALES = {
{ value: 45, r: 65, g: 105, b: 225 },
{ value: 60, r: 0, g: 0, b: 180 }
],
- format: v => v != null ? `${v.toFixed(1)} mm` : "N/A",
+ format: (v: number | null): string => v != null ? `${v.toFixed(1)} mm` : "N/A",
label: "Precipitable Water",
unit: "mm"
},
@@ -61,14 +154,14 @@ const COLOR_SCALES = {
{ value: 30, r: 255, g: 140, b: 0 },
{ value: 45, r: 200, g: 0, b: 0 }
],
- format: v => v != null ? `${v.toFixed(1)} \u00b0C (${(v * 9/5 + 32).toFixed(0)} \u00b0F)` : "N/A",
+ format: (v: number | null): string => v != null ? `${v.toFixed(1)} \u00b0C (${(v * 9/5 + 32).toFixed(0)} \u00b0F)` : "N/A",
label: "Surface Temperature",
unit: "\u00b0C"
},
ducting: {
breakpoints: null,
trueColor: { r: 0, g: 255, b: 163 },
- format: v => v ? "Yes" : "No",
+ format: (v: boolean | null): string => v ? "Yes" : "No",
label: "Ducting Detected",
unit: ""
},
@@ -80,7 +173,7 @@ const COLOR_SCALES = {
{ value: 80, r: 0, g: 255, b: 163 },
{ value: 100, r: 0, g: 200, b: 130 }
],
- format: v => v != null ? `${v.toFixed(0)}%` : "N/A",
+ format: (v: number | null): string => v != null ? `${v.toFixed(0)}%` : "N/A",
label: "Relative Humidity",
unit: "%"
},
@@ -92,7 +185,7 @@ const COLOR_SCALES = {
{ value: 360, r: 125, g: 255, b: 212 },
{ value: 400, r: 0, g: 255, b: 163 }
],
- format: v => v != null ? `${v.toFixed(1)} N` : "N/A",
+ format: (v: number | null): string => v != null ? `${v.toFixed(1)} N` : "N/A",
label: "Surface Refractivity",
unit: "N"
},
@@ -104,7 +197,7 @@ const COLOR_SCALES = {
{ value: 15, r: 255, g: 140, b: 0 },
{ value: 30, r: 200, g: 0, b: 0 }
],
- format: v => v != null ? `${v.toFixed(1)} °C` : "N/A",
+ format: (v: number | null): string => v != null ? `${v.toFixed(1)} °C` : "N/A",
label: "Temperature at 850mb",
unit: "°C"
},
@@ -116,7 +209,7 @@ const COLOR_SCALES = {
{ value: 15, r: 65, g: 105, b: 225 },
{ value: 25, r: 0, g: 0, b: 180 }
],
- format: v => v != null ? `${v.toFixed(1)} °C` : "N/A",
+ format: (v: number | null): string => v != null ? `${v.toFixed(1)} °C` : "N/A",
label: "Dewpoint at 850mb",
unit: "°C"
},
@@ -128,7 +221,7 @@ const COLOR_SCALES = {
{ value: 8, r: 255, g: 144, b: 68 },
{ value: 10, r: 255, g: 79, b: 79 }
],
- format: v => v != null ? `${v.toFixed(1)} °C/km` : "N/A",
+ format: (v: number | null): string => v != null ? `${v.toFixed(1)} °C/km` : "N/A",
label: "Lapse Rate",
unit: "°C/km"
},
@@ -140,7 +233,7 @@ const COLOR_SCALES = {
{ value: 5, r: 0, g: 255, b: 163 },
{ value: 10, r: 0, g: 200, b: 130 }
],
- format: v => v != null ? (v > 0 ? `+${v.toFixed(1)} °C` : "None") : "N/A",
+ format: (v: number | null): string => v != null ? (v > 0 ? `+${v.toFixed(1)} °C` : "None") : "N/A",
label: "Inversion Strength",
unit: "°C"
},
@@ -152,7 +245,7 @@ const COLOR_SCALES = {
{ value: 2000, r: 255, g: 144, b: 68 },
{ value: 3000, r: 255, g: 79, b: 79 }
],
- format: v => v != null ? `${v.toFixed(0)} m AGL` : "None",
+ format: (v: number | null): string => v != null ? `${v.toFixed(0)} m AGL` : "None",
label: "Inversion Base Height",
unit: "m"
},
@@ -164,7 +257,7 @@ const COLOR_SCALES = {
{ value: 1000, r: 255, g: 144, b: 68 },
{ value: 2000, r: 255, g: 79, b: 79 }
],
- format: v => v != null ? `${v.toFixed(0)} m AGL` : "None",
+ format: (v: number | null): string => v != null ? `${v.toFixed(0)} m AGL` : "None",
label: "Duct Base Height",
unit: "m"
},
@@ -176,13 +269,13 @@ const COLOR_SCALES = {
{ value: 20, r: 0, g: 255, b: 163 },
{ value: 40, r: 0, g: 200, b: 130 }
],
- format: v => v != null ? (v > 0 ? `${v.toFixed(1)} M-units` : "None") : "N/A",
+ format: (v: number | null): string => v != null ? (v > 0 ? `${v.toFixed(1)} M-units` : "None") : "N/A",
label: "Duct Strength",
unit: "M"
}
-}
+} as const satisfies Record
-function interpolateColor(value, breakpoints) {
+function interpolateColor(value: number | null, breakpoints: Breakpoint[]): RGB | null {
if (value == null) return null
if (value <= breakpoints[0].value) return breakpoints[0]
if (value >= breakpoints[breakpoints.length - 1].value) return breakpoints[breakpoints.length - 1]
@@ -202,10 +295,10 @@ function interpolateColor(value, breakpoints) {
return breakpoints[breakpoints.length - 1]
}
-function buildDetailHTML(point) {
+function buildDetailHTML(point: WeatherPoint): string {
if (!point) return ""
- const rows = [
+ const rows: DetailRow[] = [
{ label: "Temperature", value: COLOR_SCALES.temperature.format(point.temperature), color: "#ff9044" },
{ label: "Td Depression", value: COLOR_SCALES.dewpoint_depression.format(point.dewpoint_depression), color: "#7dffd4" },
{ label: "Rel. Humidity", value: COLOR_SCALES.surface_rh.format(point.surface_rh), color: "#00ffa3" },
@@ -218,7 +311,7 @@ function buildDetailHTML(point) {
{ label: "T @ 850mb", value: COLOR_SCALES.temp_850mb.format(point.temp_850mb), color: "#ff9044" },
{ label: "Td @ 850mb", value: COLOR_SCALES.dewpoint_850mb.format(point.dewpoint_850mb), color: "#4169e1" },
{ label: "Lapse Rate", value: COLOR_SCALES.lapse_rate.format(point.lapse_rate), color: "#ffe566" },
- { label: "Inversion", value: COLOR_SCALES.inversion_strength.format(point.inversion_strength), color: point.inversion_strength > 0 ? "#00ffa3" : "#666" },
+ { label: "Inversion", value: COLOR_SCALES.inversion_strength.format(point.inversion_strength), color: point.inversion_strength != null && point.inversion_strength > 0 ? "#00ffa3" : "#666" },
{ label: "Inv. Base", value: COLOR_SCALES.inversion_base_m.format(point.inversion_base_m), color: point.inversion_base_m != null ? "#7dffd4" : "#666" },
{ separator: true },
{ label: "Ducting", value: COLOR_SCALES.ducting.format(point.ducting), color: point.ducting ? "#00ffa3" : "#ff4f4f" },
@@ -262,8 +355,10 @@ function buildDetailHTML(point) {
`
}
-export const WeatherMap = {
- mounted() {
+// --- Hook ---
+
+export const WeatherMap: WeatherMapHook = {
+ mounted(this: WeatherMapHook) {
this.map = L.map(this.el, {
center: [32.897, -97.038],
zoom: 7,
@@ -279,16 +374,16 @@ export const WeatherMap = {
this.weatherOverlay = null
this.weatherData = []
this.gridLookup = new Map()
- this.selectedLayer = this.el.dataset.selectedLayer || "refractivity_gradient"
+ this.selectedLayer = (this.el.dataset.selectedLayer || "refractivity_gradient") as LayerId
- const initialData = JSON.parse(this.el.dataset.weather || "[]")
+ const initialData: WeatherPoint[] = JSON.parse(this.el.dataset.weather || "[]")
if (initialData.length > 0) {
this.weatherData = initialData
this.buildLookup()
this.renderLayer()
}
- this.handleEvent("update_weather", ({ data }) => {
+ this.handleEvent("update_weather", ({ data }: { data: WeatherPoint[] }) => {
this.weatherData = data
this.buildLookup()
this.renderLayer()
@@ -307,7 +402,7 @@ export const WeatherMap = {
this.escHint.textContent = "Press ESC to close"
this.el.appendChild(this.escHint)
- this.handleEvent("point_detail", (detail) => {
+ this.handleEvent("point_detail", (detail: WeatherPoint) => {
if (detail && detail.lat && this.detailPanel) {
this.detailPanel.innerHTML = buildDetailHTML(detail)
this.detailPanel.style.display = "block"
@@ -318,7 +413,7 @@ export const WeatherMap = {
this.clickMarker = L.layerGroup().addTo(this.map)
- this.map.on("click", (e) => {
+ this.map.on("click", (e: L.LeafletMouseEvent) => {
this.clickMarker.clearLayers()
L.circleMarker([e.latlng.lat, e.latlng.lng], {
radius: 6, color: "#fff", weight: 2,
@@ -327,7 +422,7 @@ export const WeatherMap = {
this.pushEvent("point_detail", { lat: e.latlng.lat, lon: e.latlng.lng })
})
- this._escHandler = (e) => {
+ this._escHandler = (e: KeyboardEvent) => {
if (e.key === "Escape" && this.detailPanel) {
this.detailPanel.style.display = "none"
this.detailPanel.innerHTML = ""
@@ -347,7 +442,7 @@ export const WeatherMap = {
this.map.on("moveend", () => this.sendBounds())
this._layerObserver = new MutationObserver(() => {
- const newLayer = this.el.dataset.selectedLayer
+ const newLayer = this.el.dataset.selectedLayer as LayerId | undefined
if (newLayer && newLayer !== this.selectedLayer) {
this.selectedLayer = newLayer
this.renderLayer()
@@ -360,12 +455,12 @@ export const WeatherMap = {
this.legend.addTo(this.map)
},
- destroyed() {
+ destroyed(this: WeatherMapHook) {
if (this._layerObserver) this._layerObserver.disconnect()
if (this._escHandler) document.removeEventListener("keydown", this._escHandler)
},
- sendBounds() {
+ sendBounds(this: WeatherMapHook) {
const b = this.map.getBounds()
this.pushEvent("map_bounds", {
south: b.getSouth(), north: b.getNorth(),
@@ -373,7 +468,7 @@ export const WeatherMap = {
})
},
- positionDetailPanel() {
+ positionDetailPanel(this: WeatherMapHook) {
if (!this.detailPanel || isMobile()) return
const controls = document.getElementById("weather-controls")
if (controls) {
@@ -383,7 +478,7 @@ export const WeatherMap = {
}
},
- buildLookup() {
+ buildLookup(this: WeatherMapHook) {
this.gridLookup = new Map()
this.weatherData.forEach(d => {
const key = `${d.lat.toFixed(3)},${d.lon.toFixed(3)}`
@@ -391,7 +486,7 @@ export const WeatherMap = {
})
},
- renderLayer() {
+ renderLayer(this: WeatherMapHook) {
if (this.weatherOverlay) {
this.map.removeLayer(this.weatherOverlay)
this.weatherOverlay = null
@@ -406,17 +501,17 @@ export const WeatherMap = {
const isDucting = layerId === "ducting"
const Overlay = L.GridLayer.extend({
- createTile(coords) {
+ createTile(this: L.GridLayer, coords: L.Coords) {
const tile = document.createElement("canvas")
const size = this.getTileSize()
tile.width = size.x
tile.height = size.y
- const ctx = tile.getContext("2d")
+ const ctx = tile.getContext("2d")!
const step = 0.125
const nwPoint = coords.scaleBy(size)
- const nw = this._map.unproject(nwPoint, coords.z)
- const se = this._map.unproject(nwPoint.add(size), coords.z)
+ const nw = (this as any)._map.unproject(nwPoint, coords.z) as L.LatLng
+ const se = (this as any)._map.unproject(nwPoint.add(size), coords.z) as L.LatLng
const latPerPx = (nw.lat - se.lat) / size.y
const lonPerPx = (se.lng - nw.lng) / size.x
@@ -437,11 +532,12 @@ export const WeatherMap = {
if (isDucting) {
if (value) {
- ctx.fillStyle = `rgba(${scale.trueColor.r},${scale.trueColor.g},${scale.trueColor.b},0.55)`
+ const tc = (scale as BooleanColorScale).trueColor
+ ctx.fillStyle = `rgba(${tc.r},${tc.g},${tc.b},0.55)`
ctx.fillRect(px, py, cellPxX, cellPxY)
}
} else {
- const c = interpolateColor(value, scale.breakpoints)
+ const c = interpolateColor(value as number | null, (scale as ContinuousColorScale).breakpoints)
if (c) {
ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},0.55)`
ctx.fillRect(px, py, cellPxX, cellPxY)
@@ -453,7 +549,7 @@ export const WeatherMap = {
}
})
- this.weatherOverlay = new Overlay({ opacity: 1.0 })
+ this.weatherOverlay = new Overlay({ opacity: 1.0 }) as L.GridLayer
this.weatherOverlay.addTo(this.map)
if (this.legend) {
@@ -464,7 +560,7 @@ export const WeatherMap = {
}
},
- buildLegend() {
+ buildLegend(this: WeatherMapHook): HTMLElement {
const div = L.DomUtil.create("div")
const layerId = this.selectedLayer
const scale = COLOR_SCALES[layerId]
@@ -473,7 +569,7 @@ export const WeatherMap = {
const mobile = isMobile()
if (layerId === "ducting") {
- const c = scale.trueColor
+ const c = (scale as BooleanColorScale).trueColor
div.style.cssText = `background:#fff;color:#333;padding:${mobile ? '4px 6px' : '10px 14px'};border-radius:${mobile ? '6px' : '8px'};box-shadow:0 2px 8px rgba(0,0,0,0.3);font-size:${mobile ? '10px' : '13px'};line-height:2;`
div.innerHTML = `${scale.label}
` +
`Ducting
` +
@@ -481,7 +577,7 @@ export const WeatherMap = {
return div
}
- const bp = scale.breakpoints
+ const bp = (scale as ContinuousColorScale).breakpoints
if (mobile) {
div.style.cssText = "background:#fff;color:#333;padding:4px 6px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.3);font-size:10px;line-height:1.6;"
@@ -497,4 +593,4 @@ export const WeatherMap = {
}
return div
}
-}
+} as unknown as WeatherMapHook
diff --git a/assets/tsconfig.json b/assets/tsconfig.json
index a9401b62..5410b251 100644
--- a/assets/tsconfig.json
+++ b/assets/tsconfig.json
@@ -1,32 +1,17 @@
-// This file is needed on most editors to enable the intelligent autocompletion
-// of LiveView's JavaScript API methods. You can safely delete it if you don't need it.
-//
-// Note: This file assumes a basic esbuild setup without node_modules.
-// We include a generic paths alias to deps to mimic how esbuild resolves
-// the Phoenix and LiveView JavaScript assets.
-// If you have a package.json in your project, you should remove the
-// paths configuration and instead add the phoenix dependencies to the
-// dependencies section of your package.json:
-//
-// {
-// ...
-// "dependencies": {
-// ...,
-// "phoenix": "../deps/phoenix",
-// "phoenix_html": "../deps/phoenix_html",
-// "phoenix_live_view": "../deps/phoenix_live_view"
-// }
-// }
-//
-// Feel free to adjust this configuration however you need.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": ["../deps/*"]
},
- "allowJs": true,
- "noEmit": true
+ "target": "ES2022",
+ "module": "ES2022",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "noEmit": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "typeRoots": ["./js/types"]
},
"include": ["js/**/*"]
}
diff --git a/config/config.exs b/config/config.exs
index f00dbf44..cda7db98 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -12,7 +12,7 @@ config :esbuild,
version: "0.27.4",
microwaveprop: [
args:
- ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),
+ ~w(js/app.ts --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
]