diff --git a/.prettierignore b/.prettierignore index 0b720b01..41612a2c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,6 +7,7 @@ /apps/fxc-front/public /apps/fxc-tiles/src/assets/airspaces +/libs/windy-sounding/types /.nx/cache /.nx/workspace-data diff --git a/libs/windy-sounding/.prettierignore b/libs/windy-sounding/.prettierignore new file mode 100644 index 00000000..6bc14429 --- /dev/null +++ b/libs/windy-sounding/.prettierignore @@ -0,0 +1,2 @@ +dist/ +types/ diff --git a/libs/windy-sounding/package.json b/libs/windy-sounding/package.json index 093d0a6e..9ec05d58 100644 --- a/libs/windy-sounding/package.json +++ b/libs/windy-sounding/package.json @@ -26,8 +26,8 @@ "clean": "rm -rf dist", "lint": "eslint . --max-warnings=0", "lint:fix": "eslint . --fix", - "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,css,scss,md}\"", - "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,css,scss,md}\"", + "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,css,scss,less,md}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,css,scss,less,md}\"", "manifest": "node generate-manifest.js dist", "preview": "pnpm build:dev --watch & pnpm build:prod --watch & vite preview", "upload:prepare": "rm -rf dist && pnpm build:prod && pnpm build:dev && pnpm build:config && pnpm manifest", diff --git a/libs/windy-sounding/src/style/desktop.less b/libs/windy-sounding/src/style/desktop.less new file mode 100644 index 00000000..a329a6d4 --- /dev/null +++ b/libs/windy-sounding/src/style/desktop.less @@ -0,0 +1,101 @@ +// Desktop-Specific Styles + +// Windy container overrides for desktop. +#device-desktop #plugin-windy-plugin-fxc-soundings .plugin__content { + padding: 25px !important; + display: grid; + grid-template-rows: auto 1fr; +} + +// Plugin title bar in desktop header. +#wsp-title { + #wsp-icon { + position: relative; + top: -3px; + margin-right: 0.3em; + vertical-align: middle; + } + + #wsp-dev { + position: absolute; + bottom: -3px; + left: 35px; + } + + #wsp-version { + margin-left: 0.5em; + } +} + +#wsp-sounding { + // Desktop grid layout for diagram, favorite shortcuts, and sponsor banner. + #device-desktop & { + display: grid; + justify-items: stretch; + grid-template-rows: auto 1fr auto; + } + + // Weather model selector & metadata (desktop only). + #wsp-model { + padding: 0 0 1em; + + dl { + display: grid; + grid-gap: 4px 16px; + grid-template-columns: max-content; + align-items: center; + } + + dt { + font-style: italic; + } + + dd { + font-weight: 500; + grid-column-start: 2; + } + + select { + background: inherit; + color: inherit; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 2em; + padding: 2px 4px; + font-size: inherit; + font-family: inherit; + font-weight: 500; + cursor: pointer; + + option { + background: #333; + color: #fff; + } + } + } + + // Favorite location shortcut buttons (desktop only). + #wsp-flyto { + padding-top: 0.5em; + + .button { + line-height: 1em; + padding: 0.3em 0.6em; + margin: 0.1em 0.2em; + + &:hover { + background-color: var(--color-gray); + } + + &.selected { + background-color: var(--color-orange); + } + } + } + + // Sponsor banner (desktop only). + #wsp-sponsor { + padding: 0.5em; + text-align: center; + background-color: var(--color-gray-dark); + } +} diff --git a/libs/windy-sounding/src/style/mobile.less b/libs/windy-sounding/src/style/mobile.less new file mode 100644 index 00000000..88497f2f --- /dev/null +++ b/libs/windy-sounding/src/style/mobile.less @@ -0,0 +1,103 @@ +// Mobile-Specific Styles + +// Windy container overrides for mobile. +#device-mobile #plugin-windy-plugin-fxc-soundings .plugin__content { + pointer-events: initial; +} + +#plugin-windy-plugin-fxc-soundings { + // Mobile bottom sheet container. + &.plugin-mobile-bottom-small { + padding: 0; + max-height: fit-content !important; + + .desktop-only { + display: none; + } + + .plugin__content { + position: initial; + } + } +} + +#wsp-sounding { + // Drag handle for resizing the mobile sounding diagram. + #wsp-resize { + cursor: move; + + rect { + fill: lightgray; + stroke: black; + stroke-width: 0.3px; + } + + line { + stroke: darkgrey; + stroke-width: 2px; + } + } + + // Mobile bottom bar selector (model & favorite location). + #wsp-favorite { + display: grid; + grid-template-columns: 1fr 1fr auto; + padding: 10px 0; + margin: 0; + background-color: var(--color-red); + + & > div { + cursor: pointer; + } + + .select { + width: 100%; + text-align: center; + white-space: nowrap; + + &:before, + &:after { + display: inline-block; + position: relative; + top: -2px; + } + + &:after { + transform-origin: center center; + transition: transform 0.3s; + transform: rotate(-90deg); + } + + &.active::after { + transform: rotate(0); + } + + small { + padding: 0 5px; + display: inline-block; + max-width: 120px; + text-overflow: ellipsis; + overflow: hidden; + } + } + } + + // Dropdown list options for model / favorites (mobile only). + .options { + padding: 5px; + width: 100%; + background-color: var(--color-red); + text-align: center; + + & > span { + display: inline-block; + border-radius: 1em; + padding: 2px 5px; + margin: 0 5px; + + &.selected { + background-color: var(--color-red-light); + } + } + } +} diff --git a/libs/windy-sounding/src/style/skewt.less b/libs/windy-sounding/src/style/skewt.less new file mode 100644 index 00000000..60876fb9 --- /dev/null +++ b/libs/windy-sounding/src/style/skewt.less @@ -0,0 +1,349 @@ +// Sounding Graph Styles + +#wsp-sounding { + // Base SVG rendering & colors + svg { + touch-action: none; + + text { + fill: var(--color-text-primary); + + a { + fill: var(--color-text-link); + } + } + } + + // Error / Status message box + .wsp-message { + display: grid; + align-items: center; + text-align: center; + border: 1px solid lightgray; + } + + // --------------------------------------------------------------------------- + // Interactive Controls & Overlays + // --------------------------------------------------------------------------- + + // Zoom in / out toggle button (desktop & mobile). + #wsp-zoom { + cursor: pointer; + transform: scale(1.2); + + rect { + fill: #fff; + } + + path { + fill: var(--color-orange); + } + } + + // Map overlay layer switcher icons (Clouds, Wind, Rain, CCL). + .layer-icon { + cursor: pointer; + width: 32px; + height: 32px; + opacity: 0.3; + + rect { + fill: transparent; + } + + &:hover { + opacity: 0.8; + } + + &.active { + opacity: 1; + + path { + fill: var(--color-orange); + } + } + } + + // --------------------------------------------------------------------------- + // Base Graph Container + // --------------------------------------------------------------------------- + + .graph { + shape-rendering: geometricprecision; + + // Background canvas & borders + .background { + fill: white; + stroke: none; + shape-rendering: crispedges; + } + + .border { + fill: none; + stroke: gray; + stroke-width: 1; + shape-rendering: crispedges; + } + + // Default graph text labels with outline filter for legibility + text { + fill: black; + stroke: none; + filter: url(#outline); + font-size: 13px; + } + + // Ground elevation block + .surface { + fill: #8f6d4d; + shape-rendering: crispedges; + } + + // Default profile lines and boundary dividers + .line { + stroke-width: 3; + } + + path { + fill: none; + stroke-linejoin: round; + } + + .boundary { + stroke: darkgray; + stroke-width: 1px; + shape-rendering: crispedges; + } + + // Interactive hover / touch crosshair cursor + .cursor { + line { + stroke: darkgray; + stroke-dasharray: 2; + shape-rendering: crispedges; + } + + text { + dominant-baseline: hanging; + + &.altitude, + &.speed { + text-anchor: end; + } + } + + &.bottom text { + dominant-baseline: auto; + } + } + + // ------------------------------------------------------------------------- + // Cloud Visualizations & Animations + // ------------------------------------------------------------------------- + + // Convective cumulus cloud icon and pattern rendering + path.cumulus { + stroke: #030104; + fill: lightyellow; + stroke-width: 2; + vector-effect: non-scaling-stroke; + + &.bg { + stroke-width: 1; + opacity: 0.7; + vector-effect: non-scaling-stroke; + } + } + + @keyframes cumulus-drift { + from { + transform: translate3d(0, 0, 0); + } + to { + transform: translate3d(340px, 0, 0); + } + } + + // Drifting animated background clouds in convective layer + .cumulus-drift { + animation: cumulus-drift linear infinite; + opacity: 0.5; + + &.slow { + animation-duration: 80s; + } + + &.fast { + animation-duration: 60s; + } + } + + // ------------------------------------------------------------------------- + // Skew-T Log-P Thermodynamic Diagram + // ------------------------------------------------------------------------- + + &.skewt { + // Center watermark (model update timestamp) + #wsp-watermark { + text { + text-anchor: middle; + } + + .title { + fill: #ddd; + font-size: 30px; + dominant-baseline: auto; + } + + .subtitle { + fill: #999; + font-size: 13px; + dominant-baseline: hanging; + } + + #device-desktop & { + text { + fill: #eee; + } + + .title { + dominant-baseline: middle; + } + + .subtitle { + fill: none; + } + } + } + + // Cursor readout values + text.temperature { + fill: red; + } + + text.dewpoint { + fill: steelblue; + } + + // Atmospheric sounding profile curves (Temperature & Dewpoint) + .line { + .temperature { + stroke: red; + } + + .dewpoint { + stroke: steelblue; + } + } + + // Reference Grid & Meteorological Curves + .axis { + path { + stroke-width: 0.3; + } + + // Dry adiabats (constant potential temperature) + .dry-adiabatic { + stroke: green; + } + + // Saturated / Pseudo-adiabats (equivalent potential temperature) + .wet-adiabatic { + stroke: green; + stroke-dasharray: 4 6; + } + + // Isotherms (constant temperature lines) + .isotherm { + stroke: darkred; + + .zero { + stroke-width: 1; + font-weight: bold; + } + } + + // Isohumes (lines of constant saturation mixing ratio) + .isohume { + stroke: blue; + stroke-dasharray: 4 6; + } + + // Geopotential height / altitude horizontal grid lines + .altitude { + stroke: lightgray; + + line { + shape-rendering: crispedges; + } + } + } + + // Isotherm temperature labels + .isotherm text { + fill: darkred; + } + + // Upper cloud coverage indicator and column gradient + .cloud { + rect { + shape-rendering: crispedges; + } + + .cirrus { + filter: url(#outline); + } + } + + // Surface convective thermal parcel ascent + .parcel { + // Parcel dry & wet adiabatic trajectory + .line { + stroke: green; + } + + // Mixing ratio line from surface dew point + .isohume { + stroke: steelblue; + stroke-width: 1px; + stroke-dasharray: 3; + } + + // Top of thermal elevation label + text { + text-anchor: end; + dominant-baseline: hanging; + } + } + } + + // ------------------------------------------------------------------------- + // Vertical Wind Profile Diagram + // ------------------------------------------------------------------------- + + &.wind { + // Cursor wind speed readout + text.speed { + fill: purple; + } + + // Wind speed curve + .line { + &.speed { + stroke: purple; + } + } + + // Wind speed grid axis + .axis { + .speed { + stroke: lightgray; + } + + line { + shape-rendering: crispedges; + } + } + } + } +} diff --git a/libs/windy-sounding/src/styles.less b/libs/windy-sounding/src/styles.less index 68545b0b..0bd0d800 100644 --- a/libs/windy-sounding/src/styles.less +++ b/libs/windy-sounding/src/styles.less @@ -1,122 +1,11 @@ -// Windy override -#device-desktop #plugin-windy-plugin-fxc-soundings .plugin__content { - padding: 25px !important; - display: grid; - grid-template-rows: auto 1fr; -} - -#device-mobile #plugin-windy-plugin-fxc-soundings .plugin__content { - pointer-events: initial; -} - -// Update available -#wsp-update { - background-color: var(--color-warning-moderate); - color: var(--color-black); - text-align: center; -} - -// flyXC icon on desktop. -#wsp-icon { - position: relative; - top: -3px; - margin-right: 0.3em; - vertical-align: middle; -} - -// dev badge for the icon. -#wsp-dev { - position: absolute; - bottom: -3px; - left: 35px; -} +// Skew-T and wind profile diagrams +@import './style/skewt.less'; -#wsp-version { - margin-left: 0.5em; -} - -// Model details, desktop only. -#wsp-model { - padding: 0 0 1em; - - dl { - display: grid; - grid-gap: 4px 16px; - grid-template-columns: max-content; - align-items: center; - } - - dt { - font-style: italic; - } - - dd { - font-weight: 500; - grid-column-start: 2; - } - - select { - background: inherit; - color: inherit; - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 3px; - padding: 2px 4px; - font-size: inherit; - font-family: inherit; - font-weight: 500; - cursor: pointer; - - option { - background: #333; - color: #fff; - } - } -} - -// Favorite shortcuts, desktop only. -#wsp-flyto { - .button { - line-height: 1em; - padding: 0.3em 0.6em; - margin: 0.1em 0.2em; - } - - .selected { - background-color: var(--color-orange); - } - - padding-top: 0.5em; -} - -// Zomm in and out. -#wsp-zoom { - cursor: pointer; - transform: scale(1.2); - - rect { - fill: #fff; - } - - path { - fill: var(--color-orange); - } -} +// Desktop-specific styles +@import './style/desktop.less'; -// Resize the mobile layout. -#wsp-resize { - cursor: move; - - rect { - fill: lightgray; - stroke: black; - stroke-width: 0.3px; - } - - line { - stroke: darkgrey; - stroke-width: 2px; - } -} +// Mobile-specific styles +@import './style/mobile.less'; #wsp-sounding { text-rendering: optimizeLegibility; @@ -131,424 +20,4 @@ a { font-weight: bold; } - - #device-desktop & { - display: grid; - justify-items: stretch; - // graph, favorites, sponsors - grid-template-rows: auto 1fr auto; - - #wsp-sponsor { - padding: 0.5em; - text-align: center; - background-color: var(--color-gray-dark); - } - } - - .wsp-message { - display: grid; - align-items: center; - text-align: center; - border: 1px solid lightgray; - } - - svg { - touch-action: none; - - text { - fill: var(--color-text-primary); - - a { - fill: var(--color-text-link); - } - } - } - - p.header { - padding-bottom: 1em; - } - - .graph { - shape-rendering: geometricprecision; - - .background { - fill: white; - stroke: none; - shape-rendering: crispEdges; - } - - .border { - fill: none; - stroke: gray; - stroke-width: 1; - shape-rendering: crispEdges; - } - - text { - fill: black; - stroke: none; - filter: url(#outline); - font-size: 13px; - } - - .cursor { - line { - stroke: darkgray; - stroke-dasharray: 2; - shape-rendering: crispEdges; - } - - text { - dominant-baseline: hanging; - - &.altitude, - &.speed { - text-anchor: end; - } - } - - &.bottom text { - dominant-baseline: auto; - } - } - - .surface { - fill: #8f6d4d; - shape-rendering: crispEdges; - } - - .line { - stroke-width: 3; - } - - path { - fill: none; - stroke-linejoin: round; - } - - .boundary { - stroke: darkgray; - stroke-width: 1px; - shape-rendering: crispEdges; - } - - path.cumulus { - stroke: #030104; - fill: lightyellow; - stroke-width: 2; - vector-effect: non-scaling-stroke; - - &.bg { - stroke-width: 1; - opacity: 0.7; - vector-effect: non-scaling-stroke; - } - } - - @keyframes cumulus-drift { - from { - transform: translate3d(0, 0, 0); - } - to { - transform: translate3d(340px, 0, 0); - } - } - - .cumulus-drift { - animation: cumulus-drift linear infinite; - opacity: 0.5; - - &.slow { - animation-duration: 80s; - } - - &.fast { - animation-duration: 60s; - } - } - - &.skewt { - #wsp-watermark { - text { - text-anchor: middle; - } - - .title { - fill: #ddd; - font-size: 30px; - dominant-baseline: auto; - } - - .subtitle { - fill: #999; - font-size: 13px; - dominant-baseline: hanging; - } - - #device-desktop & { - text { - fill: #eee; - } - - .title { - dominant-baseline: middle; - } - - .subtitle { - fill: none; - } - } - } - - .temperature text, - text.temperature { - fill: red; - } - - .dewpoint text, - text.dewpoint { - fill: steelblue; - } - - .isotherm text { - fill: darkred; - } - - .line { - .temperature { - stroke: red; - } - - .dewpoint { - stroke: steelblue; - } - } - - .axis { - path { - stroke-width: 0.3; - } - - .dry-adiabatic { - stroke: green; - } - - .wet-adiabatic { - stroke: green; - stroke-dasharray: 4 6; - } - - .isotherm { - stroke: darkred; - - .zero { - stroke-width: 1; - font-weight: bold; - } - } - - .altitude { - stroke: lightgray; - - line { - shape-rendering: crispEdges; - } - } - - .isohume { - stroke: blue; - stroke-dasharray: 4 6; - } - - text { - &.temperature { - text-anchor: middle; - dominant-baseline: middle; - } - } - } - - .cloud { - rect { - shape-rendering: crispEdges; - } - - .cirrus { - filter: url(#outline); - } - } - - .parcel { - .line { - stroke: green; - } - - .isohume { - stroke: steelblue; - stroke-width: 1px; - stroke-dasharray: 3; - } - - text { - text-anchor: end; - dominant-baseline: hanging; - } - - path.cumulus { - stroke: #030104; - fill: lightyellow; - stroke-width: 2; - - &.bg { - stroke-width: 1; - opacity: 0.7; - } - } - } - } - - &.wind { - text.speed { - fill: purple; - } - - .line { - &.speed { - stroke: purple; - } - } - - .axis { - .speed { - stroke: lightgray; - } - - line { - shape-rendering: crispEdges; - } - } - } - } - - .parcel { - stroke-width: 3; - fill: none; - stroke-linejoin: round; - - &.trajectory { - stroke: rgb(89, 156, 0); - } - - &.isohume { - stroke-width: 1; - stroke: gray; - stroke-dasharray: 4; - } - } -} - -.layer-icon { - cursor: pointer; - width: 32px; - height: 32px; - opacity: 0.3; - - rect { - fill: transparent; - } - - &:hover { - opacity: 0.8; - } - - &.active { - opacity: 1; - - path { - fill: var(--color-orange); - } - } -} - -// Mobile bottom bar. -#plugin-windy-plugin-fxc-soundings { - &.plugin-mobile-bottom-small { - padding: 0; - max-height: fit-content !important; - - .desktop-only { - display: none; - } - - .plugin__content { - position: initial; - } - - #wsp-favorite { - display: grid; - grid-template-columns: 1fr 1fr auto; - padding: 10px 0; - margin: 0; - background-color: var(--color-red); - - & > div { - cursor: pointer; - } - - .select { - width: 100%; - text-align: center; - white-space: nowrap; - - &:before, - &:after { - display: inline-block; - position: relative; - top: -2px; - } - - &:after { - transform-origin: center center; - transition: transform 0.3s; - transform: rotate(-90deg); - } - - &.active::after { - transform: rotate(0); - } - - small { - padding: 0 5px; - display: inline-block; - max-width: 120px; - text-overflow: ellipsis; - overflow: hidden; - } - } - } - - .options { - padding: 5px; - width: 100%; - background-color: var(--color-red); - text-align: center; - } - - .options > span { - display: inline-block; - border-radius: 1em; - padding: 2px 5px; - margin: 0 5px; - } - - .options > span.selected { - background-color: var(--color-red-light); - } - } - - .plugin__content { - select { - font-size: 1rem; - } - } }