[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: ocula\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: npm\n  directory: \"/\"\n  schedule:\n    interval: monthly\n    day: wednesday\n    time: \"12:00\"\n    timezone: Australia/Brisbane\n  open-pull-requests-limit: 10\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\njspm_packages/\ntypings/\npublic/\n\n.cache\n.env\n.env.build\n\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.now\n.DS_Store"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"files.exclude\": {\n        \"**/.git\": true,\n        \"**/.svn\": true,\n        \"**/.hg\": true,\n        \"**/CVS\": true,\n        \"**/.DS_Store\": true,\n        \"**/node_modules\": true,\n        \"**/*.log\": true\n    },\n    \"npm.packageManager\": \"yarn\",\n    \"vetur.experimental.templateInterpolationService\": true\n}"
  },
  {
    "path": "BACKERS.md",
    "content": "# Backers\n\nKerry Tarrant\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Andrew Courtice\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <a href=\"https://app.ocula.io\">\n        <img src=\"https://github.com/andrewcourtice/ocula/raw/master/client/src/assets/images/logo/logo-192.svg\" alt=\"Ocula\"/>\n    </a>\n</p>\n\n# Ocula\nThe free and open-source progressive weather app\n\n<!-- TOC depthfrom:2 -->\n\n- [About](#about)\n- [Features](#features)\n- [Philosophy](#philosophy)\n- [Donating](#donating)\n- [Credits](#credits)\n\n<!-- /TOC -->\n\n## About\nOcula is a weather app built entirely using modern web standards in an attempt to create a great looking weather app that anyone can use on any device while also providing a simple PWA template for developers to build upon.\n\nI set out to create Ocula as a replacement for my favourite weather app - Pocket Weather, which was unfortunately shut down at the end of 2019 due to high maintenance costs.\n\n<p align=\"center\">\n    <img src=\"https://user-images.githubusercontent.com/11718453/93705532-95b09a80-fb61-11ea-89d9-e72e6146aea2.png\" width=\"192\" />\n    <img src=\"https://user-images.githubusercontent.com/11718453/93705531-93e6d700-fb61-11ea-8201-80efecfc95d3.png\" width=\"192\" />\n    <img src=\"https://user-images.githubusercontent.com/11718453/93705526-8e898c80-fb61-11ea-82aa-cf381b5e13a3.png\" width=\"192\" />\n</p>\n<p align=\"center\">\n    <img src=\"https://user-images.githubusercontent.com/11718453/94127849-c57edb80-fe9c-11ea-9590-34e43c0b2ae0.png\" width=\"192\" />\n    <img src=\"https://user-images.githubusercontent.com/11718453/94127875-cb74bc80-fe9c-11ea-8f7c-47550a6a3607.png\" width=\"192\" />\n    <img src=\"https://user-images.githubusercontent.com/11718453/93705522-87fb1500-fb61-11ea-8b2d-cefa59c9c712.png\" width=\"192\" />\n</p>\n\n## Features\n- No location restrictions - available worldwide\n- Daily forecast for up to 8 days\n- Hourly forecast data for up to 24 hours\n- Trend charts for hourly temp, rainfall and wind\n- Ocean tide information with tide height trend chart\n- Interactive weather maps with 6 different map types (radar, precipitation, temp, cloud, wind, pressure)\n- Frame-by-frame playback for radar images to visualise incoming rain\n- Dark/Light Themes. Default theme changes based on current time of day\n- Options to reorder or hide forecast sections, set your prefferred map type, units and more\n- Open-source, privacy friendly, and best of all - free\n\n## Philosophy\nThe goal of this project is to satisfy the following:\n\n- Must be open-source and freely available to all.\n- Must be ad-free, subscription-free and any revenue generated to be used for ongoing maintenance costs.\n- Must be built entirely using free (or freemium) services/assets (including hosting, api's, graphics etc.).\n- Must be fast, lightweight, accessible and beautiful.\n\nIt is my hope that by satisfying the above conditions Ocula can be a weather app for all to enjoy without being bombarded with ads and signups. \n\nHowever, as a result of satisfying the above conditions it is therefore not sustainable without some form of monetisation. For the most part I use free tiers of various services to ensure the app remains free but with increased usage I will personally incur the cost and may be forced to shutdown the service should costs become burdensome. For this reason I ask that you consider one of the following:\n\n- If you like Ocula and use it as your everyday weather app I ask that you please consider contributing a regular small donation to the project (see [donating](#donating)) to help ease the cost of maintenance.\n- If you are a developer you are free to fork this repository and host your own copy in accordance with the MIT licence.\n\n## Donating\nPlease consider donating to the ongoing development of this project by visiting my [Patreon page](https://www.patreon.com/ocula).\n\n## Credits\n- Weather forecast provided by [OpenWeatherMap](https://openweathermap.org).\n- Tidal information provided by [WorldTides](https://www.worldtides.info).\n- Precipitation map tiles provided by [RainViewer](https://www.rainviewer.com).\n- Maps and geocoding services provided by [MapBox](https://www.mapbox.com).\n- Logo designed by [Ethan Roxburgh](https://github.com/ethanroxburgh).\n- Icons provided by [Remix Icons](https://remixicon.com).\n"
  },
  {
    "path": "_config.yml",
    "content": "theme: jekyll-theme-cayman"
  },
  {
    "path": "api/_helpers/camel-case-keys.ts",
    "content": "import toCamelCase from './to-camel-case';\n\nexport default function camelCaseKeys(data: Record<string, any>): Record<string, any> {\n    const output = {};\n\n    for (const key in data) {\n        let value = data[key];\n        const camelKey = toCamelCase(key);\n\n        switch (true) {\n            case Array.isArray(value):\n                value = value.map(camelCaseKeys);\n                break;\n            case typeof value === 'object':\n                value = camelCaseKeys(value);\n                break;\n        }\n\n        output[camelKey] = value;\n    }\n\n    return output;\n}"
  },
  {
    "path": "api/_helpers/to-camel-case.ts",
    "content": "export default function(value: string): string {\n    return value.toLowerCase().replace(/([-_]\\w)/g, group => group[1].toUpperCase());\n}"
  },
  {
    "path": "api/location/_helpers/map-location.ts",
    "content": "export default function(feature) {\n    const {\n        id,\n        text,\n        place_name,\n        center\n    } = feature;\n\n    return {\n        id,\n        shortName: text,\n        longName: place_name,\n        latitude: center[1],\n        longitude: center[0]\n    };\n}"
  },
  {
    "path": "api/location/coordinates.ts",
    "content": "import fetch from 'node-fetch';\nimport mapLocation from './_helpers/map-location';\n\nimport {\n    NowRequest,\n    NowResponse\n} from '@vercel/node';\n\nexport default async function (request: NowRequest, response: NowResponse) {\n    const {\n        latitude,\n        longitude\n    } = request.query;\n\n    const apiKey = process.env.MAPBOX_API_KEY;\n    const apiResponse = await fetch(`https://api.mapbox.com/geocoding/v5/mapbox.places/${longitude},${latitude}.json?access_token=${apiKey}&types=locality,place&limit=1`);\n    \n    const {\n        features\n    } = await apiResponse.json();\n\n    if (!features || features.length < 1) {\n        response.statusCode = 500;\n    }\n\n    const output = mapLocation(features[0]);\n\n    return response.json(output);\n}"
  },
  {
    "path": "api/location/search.ts",
    "content": "import fetch from 'node-fetch';\nimport mapLocation from './_helpers/map-location';\n\nimport {\n    NowRequest,\n    NowResponse\n} from '@vercel/node';\n\nexport default async function (request: NowRequest, response: NowResponse) {\n    const {\n        query\n    } = request.query;\n\n    const apiKey = process.env.MAPBOX_API_KEY;\n    const apiResponse = await fetch(`https://api.mapbox.com/geocoding/v5/mapbox.places/${query}.json?access_token=${apiKey}&types=locality,place&limit=5&autocomplete=true`);\n    \n    const {\n        features\n    } = await apiResponse.json();\n\n    if (!features || features.length < 1) {\n        response.statusCode = 500;\n    }\n\n    const output = features.map(mapLocation);\n\n    return response.json(output);\n}"
  },
  {
    "path": "api/package.json",
    "content": "{\n    \"name\": \"@ocula/api\",\n    \"version\": \"1.0.0\",\n    \"repository\": \"https://github.com/andrewcourtice/ocula.git\",\n    \"author\": \"Andrew Courtice\",\n    \"license\": \"MIT\",\n    \"dependencies\": {\n        \"@vercel/node\": \"^1.8.5\",\n        \"lodash\": \"^4.17.20\",\n        \"node-fetch\": \"^2.6.1\"\n    }\n}\n"
  },
  {
    "path": "api/tsconfig.json",
    "content": "{\n    \"extends\": \"../tsconfig.json\",\n    \"compilerOptions\": {\n        \"module\": \"commonjs\"\n    }\n}"
  },
  {
    "path": "api/weather/forecast.ts",
    "content": "import fetch from 'node-fetch';\n\nimport camelCaseKeys from '../_helpers/camel-case-keys';\n\nimport {\n    NowRequest,\n    NowResponse\n} from '@vercel/node';\n\nexport default async function (request: NowRequest, response: NowResponse) {\n    let {\n        latitude,\n        longitude,\n        units\n    } = request.query;\n\n    units = units || 'metric';\n\n    const owmApiKey = process.env.OWM_API_KEY;\n    const worldtidesApiKey = process.env.WORLDTIDES_API_KEY;\n\n    const responses = await Promise.all([\n        fetch(`https://api.openweathermap.org/data/2.5/onecall?appid=${owmApiKey}&lat=${latitude}&lon=${longitude}&units=${units}&exclude=minutely`),\n        fetch(`https://www.worldtides.info/api/v2?heights&extremes&date=today&days=1&step=3600&lat=${latitude}&lon=${longitude}&key=${worldtidesApiKey}`),\n        fetch('https://tilecache.rainviewer.com/api/maps.json')\n    ]);\n\n    let [\n        forecast,\n        tides,\n        timestamps\n    ] = await Promise.all(responses.map(response => response.json()));\n\n    forecast = camelCaseKeys(forecast);\n\n    return response.json({\n        ...forecast,\n        tides,\n        radar: {\n            timestamps\n        }\n    });\n}"
  },
  {
    "path": "client/.browserslistrc",
    "content": "chrome >= 58\nfirefox >= 54\nedge >= 18\nsafari >= 11\nios_saf >= 11\nopera >= 55"
  },
  {
    "path": "client/babel.config.js",
    "content": "module.exports = {\n    presets: [\n        ['@babel/env', {\n            useBuiltIns: 'entry',\n            corejs: 3\n        }],\n        '@babel/typescript'\n    ],\n    plugins: [\n        ['const-enum', {\n            transform: 'constObject'\n        }],\n        '@babel/transform-typescript'\n    ]\n};"
  },
  {
    "path": "client/build/_base/config.js",
    "content": "import path from 'path';\n\nimport HtmlWebpackPlugin from 'html-webpack-plugin';\nimport CopyWebpackPlugin from 'copy-webpack-plugin';\nimport FaviconsWebpackPlugin from 'favicons-webpack-plugin';\n\nimport {\n    CleanWebpackPlugin\n} from 'clean-webpack-plugin';\n\nimport { \n    VueLoaderPlugin \n} from 'vue-loader';\n\n\nimport webpack from 'webpack';\n\nexport default {\n\n    stats: 'minimal',\n\n    entry: {\n        app: './src/index.ts'\n    },\n\n    output: {\n        path: path.resolve(__dirname, '../../../public'),\n        publicPath: '/',\n    },\n\n    performance: {\n        hints: false\n    },\n\n    resolve: {\n        extensions: ['.ts', '.js'],\n\n        alias: {           \n            'components': path.resolve(__dirname, '../src/components'),\n            'constants': path.resolve(__dirname, '../src/constants'),\n            'controllers': path.resolve(__dirname, '../src/controllers'),\n            'store': path.resolve(__dirname, '../src/store'),\n        },\n\n        symlinks: false\n    },\n\n    module: {\n        rules: [\n            {\n                test: /\\.vue$/,\n                use: 'vue-loader'\n            },\n            {\n                test: /\\.(ts|js)x?$/,\n                loader: 'babel-loader',\n                exclude: {\n                    test: /node_modules/,\n                    not: [\n                        /@ocula/,\n                        /.*\\.vue\\.(js|ts)$/\n                    ]\n                }\n            },\n            {\n                test: /\\.(png|jpg|gif|svg|woff|woff2|eot|ttf)$/,\n                loader: 'url-loader',\n                options: {\n                    limit: 8192\n                }\n            },\n            {\n                test: /\\.html$/,\n                loader: 'html-loader'\n            }\n        ]\n    },\n\n    plugins: [\n        \n        new webpack.EnvironmentPlugin({\n            'MAPBOX_API_KEY': '',\n            'WORLDTIDES_API_KEY': '',\n            'OWM_API_KEY': '',\n            'GA_TRACKING_ID': '',\n            'SENTRY_DSN': '',\n        }),\n        \n        new webpack.DefinePlugin({\n            '__VUE_OPTIONS_API__': false, \n            '__VUE_PROD_DEVTOOLS__': false \n        }),\n        \n        new CleanWebpackPlugin(),\n    \n        new VueLoaderPlugin(),\n    \n        new HtmlWebpackPlugin({\n            title: 'Ocula',\n            template: './src/index.ejs'\n        }),\n    \n        new FaviconsWebpackPlugin({\n            logo: './src/assets/images/logo/logo-512.svg',\n            favicons: {\n                appName: 'Ocula',\n                appShortName: 'Ocula',\n                appDescription: 'The open-source, progressive weather app',\n                developerName: 'Andrew Courtice',\n                display: 'standalone',\n                background: '#FFFFFF',\n                theme_color: '#FFFFFF',\n                appleStatusBarStyle: 'default',\n                start_url: '/?source=pwa',\n                scope: '/',\n                icons: {\n                    android: true,\n                    appleIcon: true,\n                    appleStartup: true,\n                    favicons: true,\n                    firefox: true,\n                    windows: true\n                }\n            }\n        }),\n\n        new CopyWebpackPlugin([\n            './src/static'\n        ])\n\n    ]\n};\n"
  },
  {
    "path": "client/build/_base/workbox.js",
    "content": "export default {\n    swDest: 'service-worker.js',\n    clientsClaim: true,\n    //skipWaiting: true,\n    navigateFallback: '/index.html',\n    runtimeCaching: [\n        {\n            urlPattern: /^https:\\/\\/fonts\\.googleapis\\.com/,\n            handler: 'StaleWhileRevalidate',\n            options: {\n                cacheName: 'ocula-fonts'\n            }\n        },\n        {\n            urlPattern: /.*fontawesome\\.com/,\n            handler: 'StaleWhileRevalidate',\n            options: {\n                cacheName: 'ocula-fonts'\n            }\n        }\n    ]\n};"
  },
  {
    "path": "client/build/development.js",
    "content": "import MiniCssExtractPlugin from 'mini-css-extract-plugin';\n\nimport merge from 'webpack-merge';\nimport base from './_base/config';\n\nconst CSS_LOADERS = [\n    'vue-style-loader', \n    'css-loader',\n    {\n        loader: 'postcss-loader',\n        options: {\n            config: {\n                path: 'client/'\n            }\n        }\n    }\n];\n\nexport default merge(base, {\n\n    mode: 'development',\n\n    devServer: {\n        port: 3000,\n        hot: true,\n        noInfo: true,\n        historyApiFallback: true,\n        clientLogLevel: 'warning'\n    },\n\n    output: {\n        filename: '[name]-[hash].js',\n        chunkFilename: '[name]-[hash].js'\n    },\n\n    devtool: 'cheap-module-eval-source-map',\n\n    module: {\n        rules: [\n            {\n                test: /\\.css$/,\n                use: CSS_LOADERS\n            },\n            {\n                test: /\\.scss$/,\n                use: [].concat(CSS_LOADERS, 'sass-loader'),\n                exclude: {\n                    test: /node_modules/,\n                    not: [\n                        /@ocula/\n                    ]\n                }\n            },\n            {\n                test: /\\.sass$/,\n                use: [].concat(CSS_LOADERS, 'sass-loader?indentedSyntax'),\n                exclude: {\n                    test: /node_modules/,\n                    not: [\n                        /@ocula/\n                    ]\n                }\n            }\n        ]\n    },\n\n    plugins: [\n   \n        new MiniCssExtractPlugin({\n            filename: '[name]-[hash].css',\n            chunkFilename: '[name]-[hash].css'\n        })\n\n    ]\n});"
  },
  {
    "path": "client/build/insights.js",
    "content": "import merge from 'webpack-merge';\nimport production from './production';\n\nimport {\n    BundleAnalyzerPlugin\n} from 'webpack-bundle-analyzer';\n\nexport default merge(production, {\n\n    plugins: [\n    \n        new BundleAnalyzerPlugin({\n            analyzerMode: 'static',\n            reportFilename: 'ocula-bundle-report.html'\n        })\n\n    ]\n});\n"
  },
  {
    "path": "client/build/production.js",
    "content": "import TerserPlugin from 'terser-webpack-plugin';\nimport OptimiseCSSPlugin from 'optimize-css-assets-webpack-plugin';\nimport MiniCssExtractPlugin from 'mini-css-extract-plugin';\nimport WorkboxPlugin from 'workbox-webpack-plugin';\n\nimport merge from 'webpack-merge';\nimport base from './_base/config';\n\nimport workboxConfig from './_base/workbox';\n\nconst CSS_LOADERS = [\n    MiniCssExtractPlugin.loader, \n    'css-loader',\n    {\n        loader: 'postcss-loader',\n        options: {\n            config: {\n                path: 'client/'\n            }\n        }\n    }\n];\n\nexport default merge(base, {\n    mode: 'production',\n\n    output: {\n        filename: '[name]-[contenthash].js',\n        chunkFilename: '[name]-[contenthash].js'\n    },\n\n    devtool: 'source-map',\n\n    optimization: {\n        runtimeChunk: 'single',\n        splitChunks: {\n            automaticNameDelimiter: '-',\n            cacheGroups: {\n                vendor: {\n                    test: (module) => module.context && module.context.includes('node_modules') && !module.context.includes('@ocula'),\n                    name: 'vendor',\n                    chunks: 'initial',\n                    enforce: true\n                }\n            }\n        },\n        minimizer: [\n            new TerserPlugin(),\n            new OptimiseCSSPlugin()\n        ]\n    },\n\n    module: {\n        rules: [\n            {\n                test: /\\.css$/,\n                use: CSS_LOADERS\n            },\n            {\n                test: /\\.scss$/,\n                use: [].concat(CSS_LOADERS, 'sass-loader'),\n                exclude: {\n                    test: /node_modules/,\n                    not: [\n                        /@ocula/\n                    ]\n                }\n            },\n            {\n                test: /\\.sass$/,\n                use: [].concat(CSS_LOADERS, 'sass-loader?indentedSyntax'),\n                exclude: {\n                    test: /node_modules/,\n                    not: [\n                        /@ocula/\n                    ]\n                }\n            }\n        ]\n    },\n\n    plugins: [\n    \n        new MiniCssExtractPlugin({\n            filename: '[name]-[contenthash].css',\n            chunkFilename: '[name]-[contenthash].css'\n        }),\n\n        new WorkboxPlugin.GenerateSW(workboxConfig)\n\n    ]\n});"
  },
  {
    "path": "client/package.json",
    "content": "{\n    \"name\": \"@ocula/client\",\n    \"title\": \"Ocula\",\n    \"version\": \"1.0.3\",\n    \"main\": \"./src/index.ts\",\n    \"repository\": \"https://github.com/andrewcourtice/ocula.git\",\n    \"author\": \"Andrew Courtice\",\n    \"description\": \"The free and open-source progressive weather app\",\n    \"license\": \"MIT\",\n    \"scripts\": {\n        \"dev\": \"webpack --env=development\",\n        \"build\": \"webpack --env=production\",\n        \"start\": \"webpack-dev-server --env=development\",\n        \"insights\": \"webpack --env=insights\"\n    },\n    \"dependencies\": {\n        \"@ocula/charts\": \"1.0.0\",\n        \"@ocula/components\": \"1.0.0\",\n        \"@ocula/event-emitter\": \"1.0.0\",\n        \"@ocula/router\": \"1.0.0\",\n        \"@ocula/state\": \"1.0.0\",\n        \"@ocula/utilities\": \"1.0.0\",\n        \"@sentry/browser\": \"^5.29.0\",\n        \"@sentry/integrations\": \"^5.29.0\",\n        \"core-js\": \"3.8.1\",\n        \"regenerator-runtime\": \"^0.13.7\",\n        \"vue\": \"3.0.5\",\n        \"workbox-window\": \"^6.0.2\"\n    },\n    \"devDependencies\": {\n        \"@babel/core\": \"^7.12.9\",\n        \"@babel/plugin-syntax-dynamic-import\": \"^7.8.3\",\n        \"@babel/plugin-transform-typescript\": \"^7.12.1\",\n        \"@babel/preset-env\": \"^7.12.7\",\n        \"@babel/preset-typescript\": \"^7.12.7\",\n        \"@babel/register\": \"^7.12.1\",\n        \"@types/d3\": \"^6.2.0\",\n        \"@vue/compiler-sfc\": \"3.0.5\",\n        \"autoprefixer\": \"^9.8.6\",\n        \"babel-loader\": \"^8.1.0\",\n        \"babel-plugin-const-enum\": \"^1.0.1\",\n        \"clean-webpack-plugin\": \"^3.0.0\",\n        \"copy-webpack-plugin\": \"^5.1.1\",\n        \"css-loader\": \"^3.6.0\",\n        \"favicons-webpack-plugin\": \"^3.0.1\",\n        \"file-loader\": \"^6.1.0\",\n        \"html-loader\": \"^1.3.0\",\n        \"html-webpack-plugin\": \"^4.4.1\",\n        \"mini-css-extract-plugin\": \"^0.9.0\",\n        \"node-sass\": \"^4.14.1\",\n        \"optimize-css-assets-webpack-plugin\": \"^5.0.4\",\n        \"postcss-loader\": \"^3.0.0\",\n        \"sass-loader\": \"^8.0.2\",\n        \"style-loader\": \"^1.2.1\",\n        \"terser-webpack-plugin\": \"^2.3.5\",\n        \"url-loader\": \"^4.1.0\",\n        \"vue-loader\": \"16.1.1\",\n        \"vue-style-loader\": \"^4.1.2\",\n        \"webpack\": \"^4.44.1\",\n        \"webpack-bundle-analyzer\": \"^3.8.0\",\n        \"webpack-cli\": \"^3.3.12\",\n        \"webpack-dev-server\": \"^3.11.0\",\n        \"webpack-merge\": \"^4.2.2\",\n        \"workbox-webpack-plugin\": \"^5.1.4\"\n    }\n}\n"
  },
  {
    "path": "client/postcss.config.js",
    "content": "module.exports = {\n    plugins: [\n        require('autoprefixer')\n    ]\n};"
  },
  {
    "path": "client/src/app.vue",
    "content": "<template>\n    <layout class=\"app transition-theme-change\" :class=\"appClass\" footer>\n        <router-view />\n        <template #footer>\n            <nav class=\"app__nav\">\n                <container class=\"app__nav-container\" layout=\"row center-stretch\" :grid=\"routes.length\">\n                    <router-link class=\"app__route\" v-for=\"route in routes\" :key=\"route.label\" :to=\"route.route\">\n                        <icon-button class=\"app__route-button\" layout=\"vertical\" :icon=\"route.icon\">\n                            <small>{{ route.label }}</small>\n                        </icon-button>\n                    </router-link>\n                </container>\n            </nav>\n        </template>\n        <location-modal />\n        <core-components />\n    </layout>\n</template>\n\n<script lang=\"ts\">\nimport ROUTES from './constants/core/routes';\n\nimport LocationModal from './components/modals/location.vue';\n\nimport setThemeMeta from './helpers/set-theme-meta';\n\nimport {\n    defineComponent,\n    watch,\n    computed\n} from 'vue';\n\nimport {\n    phase,\n    theme\n} from './store';\nimport PHASE from './enums/forecast/phase';\n\nconst routes = [\n    {\n        label: 'Forecast',\n        icon: 'sun-line',\n        route: {\n            name: ROUTES.forecast.index\n        }\n    },\n    {\n        label: 'Maps',\n        icon: 'road-map-line',\n        route: {\n            name: ROUTES.maps.index\n        }\n    },\n    {\n        label: 'Settings',\n        icon: 'equalizer-line',\n        route: {\n            name: ROUTES.settings.index\n        }\n    }\n];\n\nconst PHASE_CLASS = {\n    [PHASE.day]: 'phase--day',\n    [PHASE.night]: 'phase--night'\n};\n\nexport default defineComponent({\n\n    components: {\n        LocationModal\n    },\n\n    setup() {\n        watch(() => theme.value.core, ({ colour }) => setThemeMeta(colour));\n\n        const appClass = computed(() => [\n            PHASE_CLASS[phase.value],\n            theme.value.core.class\n        ]);\n        \n        return {\n            appClass,\n            routes\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .app {\n        width: 100%;\n        height: 100%;\n        margin: 0;\n        padding: 0;\n        user-select: none;\n        color: var(--font__colour);\n        background-color: var(--background__colour);\n    }\n\n    .app__nav {\n        border-top: 1px solid var(--border__colour);\n    }\n\n    .app__nav-container {\n        padding: var(--spacing__x-small) var(--spacing__medium);\n    }\n\n    .app__route {\n        display: block;\n        color: inherit;\n\n        &:hover {\n            color: inherit;\n        }\n\n        &.router-link-active {\n            color: var(--colour__primary);\n        }\n    }\n\n    .app__route-button {\n        display: flex;\n    }\n\n    .route {\n        width: 100%;\n        height: 100%;\n        overflow: hidden;\n        overflow-y: auto;\n    }\n\n    .transition-theme-change {\n        transition: color var(--transition__timing--fade) var(--transition__easing--default),\n                    background var(--transition__timing--fade) var(--transition__easing--default);\n    }\n\n</style>"
  },
  {
    "path": "client/src/assets/images/figures/index.ts",
    "content": "import fog from './fog.svg';\nimport fullMoon from './full-moon.svg';\nimport lightRain from './light-rain.svg';\nimport lightSnow from './light-snow.svg';\nimport moderateRain from './moderate-rain.svg';\nimport nightWind from './night-wind.svg';\nimport partlyCloudyDay from './partly-cloudy-day.svg';\nimport partlyCloudyNight from './partly-cloudy-night.svg';\nimport rain from './rain.svg';\nimport rainCloud from './rain-cloud.svg';\nimport rainyNight from './rainy-night.svg';\nimport snow from './snow.svg';\nimport snowySunnyDay from './snowy-sunny-day.svg';\nimport storm from './storm.svg';\nimport stormyNight from './stormy-night.svg';\nimport stormyWeather from './stormy-weather.svg';\nimport sun from './sun.svg';\nimport weather from './weather.svg';\nimport wind from './wind.svg';\nimport windyWeather from './windy-weather.svg';\n\nexport default {\n    fog,\n    fullMoon,\n    lightRain,\n    lightSnow,\n    moderateRain,\n    nightWind,\n    partlyCloudyDay,\n    partlyCloudyNight,\n    rain,\n    rainCloud,\n    rainyNight,\n    snow,\n    snowySunnyDay,\n    storm,\n    stormyNight,\n    stormyWeather,\n    sun,\n    weather,\n    wind,\n    windyWeather\n};"
  },
  {
    "path": "client/src/components/charts/_base/chart.ts",
    "content": "import EVENTS from '../../../constants/core/events';\n\nimport {\n    defineComponent, \n    ref,\n    watch,\n    onMounted,\n    onBeforeUnmount,\n    WatchStopHandle,\n} from 'vue';\n\nimport {\n    useSubscriber\n} from '@ocula/components';\n\nexport default function chart(Chart) {\n    return defineComponent({\n\n        props: {\n\n            data: {\n                type: Array,\n                default: () => []\n            },\n\n            options: {\n                type: Object\n            },\n\n            autoRender: {\n                type: Boolean,\n                default: true\n            },\n\n            autoUpdate: {\n                type: Boolean,\n                default: true\n            }\n\n        },\n\n        setup(props) {\n            let chart;\n            let watchHandle: WatchStopHandle;\n\n            const element = ref<Element>(null);\n\n            async function render() {\n                if (!chart) {\n                    return;\n                }\n    \n                return chart.render(props.data, props.options);\n            }\n\n            useSubscriber(EVENTS.application.resized, render);\n\n            onMounted(() => {\n                chart = new Chart(element.value);\n            \n                if (props.autoRender) {\n                    render();\n                }\n    \n                if (props.autoUpdate) {\n                    watchHandle = watch([\n                        () => props.data,\n                        () => props.options\n                    ], render);\n                }\n            });\n\n            onBeforeUnmount(() => watchHandle && watchHandle());\n\n            return {\n                element\n            };\n        }\n\n    });\n}"
  },
  {
    "path": "client/src/components/charts/line.vue",
    "content": "<template>\n    <div class=\"line-chart\" ref=\"element\"></div>\n</template>\n\n<script lang=\"ts\">\nimport {\n    LineChart\n} from '@ocula/charts';\n\nimport chart from './_base/chart';\n\nexport default chart(LineChart);\n</script>\n\n<style lang=\"scss\">\n\n    .line-chart {\n        width: 100%;\n        height: 100%;\n    }\n\n</style>"
  },
  {
    "path": "client/src/components/charts/trends.vue",
    "content": "<template>\n    <div class=\"trends-chart\">\n        <div class=\"trends-chart__body\" :style=\"bodyStyle\">\n            <line-chart class=\"trends-chart__chart\" :data=\"data\" :options=\"options\" />\n            <div class=\"trends-chart__now\" layout=\"column center-left\">\n                <span class=\"trends-chart__now-label\">\n                    <slot name=\"start-label\" :value=\"data[0]\">Now</slot>\n                </span>\n            </div>\n            <template v-for=\"value in data.slice(1, -1)\" :key=\"keyBy(value)\">\n                <div class=\"trends-chart__column\">\n                    <slot name=\"primary-column\" :value=\"value\"></slot>\n                </div>\n                <div class=\"trends-chart__column\" v-for=\"row in secondaryRows\" :key=\"row\">\n                    <slot name=\"secondary-column\" :value=\"value\" :row=\"row\"></slot>\n                </div>\n            </template>\n            <div class=\"trends-chart__later\" layout=\"column center-right\">\n                <span class=\"trends-chart__later-label\">\n                    <slot name=\"end-label\" :value=\"data[data.length - 1]\">Later</slot>\n                </span>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport LineChart from './line.vue';\n\nimport {\n    defineComponent,\n    computed,\n    PropType\n} from 'vue';\n\nimport type {\n    ILineOptions\n} from '@ocula/charts';\n\nexport default defineComponent({\n\n    components: {\n        LineChart\n    },\n    \n    props: {\n\n        data: {\n            type: Array,\n            default: () => []\n        },\n\n        options: {\n            type: Object as PropType<ILineOptions>\n        },\n\n        keyBy: {\n            type: Function\n        },\n\n        secondaryRows: {\n            type: Number,\n            default: 0\n        }\n\n    },\n\n    setup(props) {\n\n        const bodyStyle = computed(() => ({\n            gridTemplateRows: `repeat(${props.secondaryRows + 2}, auto)`,\n            gridTemplateColumns: `2rem repeat(${props.data.length - 2}, 4rem) 2rem`\n        }));\n\n        return {\n            bodyStyle\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n    @import \"~@ocula/style/src/_mixins.scss\";\n\n    .trends-chart {\n        overflow: hidden;\n        overflow-x: auto;\n    }\n\n    .trends-chart__body {\n        display: inline-grid;\n        padding-bottom: var(--spacing__small);\n        grid-auto-flow: column;\n        row-gap: var(--spacing__x-small);\n        width: auto;\n    }\n\n    .trends-chart__chart {\n        grid-column: 1 / -1;\n        height: 196px;\n    }\n\n    .trends-chart__column {\n        @include text-truncate;\n        padding: 0 var(--spacing__xx-small);\n        text-align: center;\n    }\n\n    .trends-chart__now,\n    .trends-chart__later {\n        grid-row: 2 / -1;      \n    }\n\n    .trends-chart__now-label,\n    .trends-chart__later-label {\n        color: var(--font__colour--meta);\n        font-size: var(--font__size--x-small);\n        text-transform: uppercase;\n        transform-origin: center;\n    }\n\n    .trends-chart__now-label {\n        transform: rotate(90deg);\n    }\n    \n    .trends-chart__later-label {\n        transform: rotate(-90deg);\n    }\n\n</style>"
  },
  {
    "path": "client/src/components/drawers/maps.vue",
    "content": "<template>\n    <drawer :id=\"id\" class=\"maps-drawer\" position=\"top\" ref=\"drawer\">\n        <template #default=\"{ close }\">\n            <container class=\"maps-drawer__container\" grid=\"3\">\n                <router-link class=\"link--inherit\" :to=\"getRoute(key)\" v-for=\"(value, key) in maps\" :key=\"key\">\n                    <icon-button class=\"maps-drawer__option menu-item\" layout=\"vertical\" :icon=\"value.icon\" @click.native=\"close\">\n                        <small>{{ value.label }}</small>\n                    </icon-button>\n                </router-link>\n            </container>\n        </template>\n    </drawer>\n</template>\n\n<script lang=\"ts\">\nimport MAP from '../../enums/maps/map';\n\nimport DRAWERS from '../../constants/core/drawers';\nimport MAPS from '../../constants/maps/maps';\nimport ROUTES from '../../constants/core/routes';\n\nimport {\n    defineComponent,\n    ref\n} from 'vue';\n\nexport default defineComponent({\n    \n    setup() {\n        const id = DRAWERS.maps;\n        const drawer = ref(null);\n\n        function getRoute(type: MAP) {\n            return {\n                name: ROUTES.maps.index,\n                params: {\n                    type\n                }\n            };\n        }\n\n        return {\n            id,\n            drawer,\n            maps: MAPS,\n            getRoute\n        };\n    }\n\n})\n</script>\n\n<style lang=\"scss\">\n\n    .maps-drawer__container {\n        padding: var(--spacing__small);\n    }\n\n    .maps-drawer__option {\n        display: flex;\n    }\n\n</style>\n"
  },
  {
    "path": "client/src/components/forecast/daily-forecast.vue",
    "content": "<template>\n    <accordion class=\"forecast-daily\">\n        <template #default=\"accordion\">\n            <table class=\"forecast-daily__days\">\n                <tbody class=\"forecast-daily__day\" v-for=\"day in days\" :key=\"day.dt.raw\">\n                    <tr class=\"forecast-daily__day-header menu-item\" @click=\"accordion.toggle(day.dt.raw)\">\n                        <td class=\"forecast-daily__day-column forecast-daily__day-column--icon\">\n                            <icon :name=\"getIcon(day.weather.id.raw)\"/>\n                        </td>\n                        <td class=\"forecast-daily__day-column forecast-daily__day-column--label\">\n                            <div>{{ getDate(day) }}</div>\n                            <div class=\"text--meta text--tight\">\n                                <small>{{ day.weather.description.formatted }}</small>\n                            </div>\n                        </td>\n                        <td class=\"forecast-daily__day-column forecast-daily__day-column--precip\">\n                            <div layout=\"row center-right\" v-if=\"day.pop.raw > 0\">\n                                <div class=\"text--meta\">{{ day.pop.formatted }}</div>\n                                <icon name=\"drop-fill\" class=\"forecast-daily__precip-icon\" :style=\"getPrecipIconStyle(day)\"/>\n                            </div>\n                        </td>\n                        <td class=\"forecast-daily__day-column forecast-daily__day-column--min\">{{ getMinMax(day.temp.min) }}</td>\n                        <td class=\"forecast-daily__day-column forecast-daily__day-column--max\">{{ getMinMax(day.temp.max) }}</td>\n                    </tr>\n                    <tr class=\"forecast-daily__day-body\">\n                        <td colspan=\"5\">\n                            <accordion-pane :id=\"day.dt.raw\">\n                                <div class=\"forecast-daily__day-details\" grid=\"2 md-3\">\n                                    <observation class=\"forecast-daily__day-observation\" label=\"Temp Min\" icon=\"temp-cold-line\">{{ day.temp.min.formatted }}</observation>\n                                    <observation class=\"forecast-daily__day-observation\" label=\"Temp Max\" icon=\"temp-hot-line\">{{ day.temp.max.formatted }}</observation>\n                                    <observation class=\"forecast-daily__day-observation\" label=\"Wind Speed\" icon=\"windy-line\">{{ day.windSpeed.formatted }}</observation>\n                                    <observation class=\"forecast-daily__day-observation\" label=\"Wind Direction\" icon=\"compass-3-line\">{{ day.windDeg.formatted }}</observation>\n                                    <observation class=\"forecast-daily__day-observation\" label=\"Humidity\" icon=\"contrast-drop-2-line\">{{ day.humidity.formatted }}</observation>\n                                    <observation class=\"forecast-daily__day-observation\" label=\"Pressure\" icon=\"swap-line\">{{ day.pressure.formatted }}</observation>\n                                    <observation class=\"forecast-daily__day-observation\" label=\"Cloud Coverage\" icon=\"cloudy-line\">{{ day.clouds.formatted }}</observation>\n                                    <observation class=\"forecast-daily__day-observation\" label=\"UV Index\" icon=\"sun-line\">\n                                        <div layout=\"row center-left\">\n                                            <div class=\"margin__right--x-small\">{{ day.uvi.formatted }}</div>\n                                            <div class=\"dot\" :style=\"getUvIndexDotStyle(day.uvi.raw)\"></div>\n                                        </div>\n                                    </observation>\n                                </div>\n                            </accordion-pane>\n                        </td>\n                    </tr>\n                </tbody>\n            </table>\n        </template>\n    </accordion>\n</template>\n\n<script lang=\"ts\">\nimport UV_INDEX from '../../constants/forecast/uv-index';\n\nimport Observation from '../weather/observation.vue';\n\nimport getIcon from '../../helpers/get-icon';\n\nimport {\n    defineComponent,\n    computed\n} from 'vue';\n\nimport {\n    forecast,\n    format\n} from '../../store';\n\nimport {\n    scaleContinuous\n} from '@ocula/utilities';\n\nimport type {\n    Formatted,\n    IMappedForecastDay\n} from '../../types/state';\n\nexport default defineComponent({\n\n    components: {\n        Observation\n    },\n    \n    setup() {\n        const precipScale = scaleContinuous([0, 1], [0.5, 1]);\n        const days = computed(() => forecast.value.daily);\n\n        function getDate(day: Formatted<IMappedForecastDay>) {\n            return format.value.date(day.dt.formatted as any);\n        }\n\n        function getPrecipIconStyle(day: Formatted<IMappedForecastDay>) {\n            return {\n                opacity: precipScale(day.pop.raw, true)\n            };\n        }\n\n        function getMinMax(value) {\n            return Math.round(value.raw);\n        }\n\n        function getUvIndexDotStyle(uvIndex: number) {\n            const {\n                colour\n            } = UV_INDEX.slice()\n                .reverse()\n                .find(({ start }) => uvIndex >= start);\n\n            return {\n                backgroundColor: colour\n            };\n        }\n\n        return {\n            days,\n            getIcon,\n            getDate,\n            getPrecipIconStyle,\n            getMinMax,\n            getUvIndexDotStyle\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .forecast-daily {\n\n    }\n\n    .forecast-daily__days {\n        margin: calc(var(--spacing__x-small) * -1);\n\n        & tr {\n\n            & td:first-of-type {\n                border-top-left-radius: var(--border__radius);\n                border-bottom-left-radius: var(--border__radius);\n            }\n\n            & td:last-of-type {\n                border-top-right-radius: var(--border__radius);\n                border-bottom-right-radius: var(--border__radius);\n            }\n        }\n    }\n\n    .forecast-daily__precip-icon {\n        display: block;\n        width: 1em;\n        height: 1em;\n        margin-left: var(--spacing__xx-small);\n        fill: var(--colour__primary);\n    }\n\n    .forecast-daily__day-column {\n        padding: var(--spacing__x-small);\n        text-align: center;\n        vertical-align: middle;\n    }\n\n    .forecast-daily__day-column--label {\n        width: 100%;\n        text-align: left;\n    }\n\n    .forecast-daily__day-column--precip {\n        text-align: right;\n    }\n\n    .forecast-daily__day-column--min {\n        color: var(--font__colour--meta);\n    }\n\n    .forecast-daily__day-body {\n\n        & td {\n            padding: 0;\n        }\n\n        & .accordion-pane {\n            width: 100%;\n            overflow: hidden;\n        }       \n    }\n\n    .forecast-daily__day-details {\n        padding: var(--spacing__small) var(--spacing__x-small) var(--spacing__large);\n    }\n\n    .forecast-daily__day-observation {\n        font-size: var(--font__size--small);\n    }\n\n</style>"
  },
  {
    "path": "client/src/components/forecast/hourly-forecast.vue",
    "content": "<template>\n    <div class=\"forecast-hourly\">\n        <div class=\"forecast-hourly__header\" layout=\"row center-justify\">\n            <div self=\"size-x1\">{{ trend.label }} ({{ trend.unitOfMeasure }})</div>\n            <div class=\"forecast-hourly__options\" layout=\"row center-center\">\n                <icon-button class=\"menu-item text--small\"\n                    v-for=\"trend in trends\"\n                    v-tooltip=\"trend.label\"\n                    layout=\"vertical\"\n                    :key=\"trend.key\"\n                    :icon=\"trend.icon\"\n                    :class=\"getOptionClass(trend.key)\"\n                    @click=\"setTrend(trend.key)\">\n                </icon-button>\n            </div>\n        </div>\n        <trends :data=\"hours\" :options=\"trend.chartOptions\" :key-by=\"hour => hour.dt.raw\" :secondary-rows=\"2\">\n            <template #primary-column=\"column\">\n                <small>{{ getTime(column.value) }}</small>\n            </template>\n            <template #secondary-column=\"column\">\n                <template v-if=\"type === 'wind'\">\n                    <icon name=\"arrow-up-line\" :style=\"getWindIconStyle(column.value)\" v-if=\"column.row === 1\"/>\n                    <small class=\"text--x-small\" v-else>{{ column.value.windDeg.formatted }}</small>\n                </template>\n                <template v-else>\n                    <icon :name=\"getIcon(column.value.weather.id.raw, column.value.dt.raw)\" v-if=\"column.row === 1\"/>\n                    <small class=\"text--x-small\" v-else>{{ column.value.weather.description.formatted }}</small>\n                </template>\n            </template>\n        </trends>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport TREND from '../../enums/forecast/trend';\nimport TRENDS from '../../constants/forecast/trends';\n\nimport Trends from '../charts/trends.vue';\n\nimport getIcon from '../../helpers/get-icon';\n\nimport {\n    defineComponent,\n    ref,\n    computed\n} from 'vue';\n\nimport {\n    forecast,\n    format,\n    unitOfMeasure\n} from '../../store';\n\nimport type {\n    Formatted,\n    IMappedForecastHour\n} from '../../types/state';\n\nexport default defineComponent({\n    \n    components: {\n        Trends\n    },\n\n    setup(props) {\n        const type = ref(TREND.temperature);\n\n        const trends = Object.keys(TRENDS).map(key => {\n            const {\n                icon,\n                label\n            } = TRENDS[key];\n\n            return {\n                key,\n                icon,\n                label\n            };\n        })\n\n        const hours = computed(() => forecast.value.hourly);\n\n        const trend = computed(() => {\n            const trend = TRENDS[type.value];\n            const uom = unitOfMeasure.value[trend.observation];\n\n            return {\n                ...trend,\n                unitOfMeasure: uom\n            };\n        });\n\n        function getTime(hour: Formatted<IMappedForecastHour>): string {\n            return format.value.time(hour.dt.formatted as any, 'h a');\n        }\n\n        function getWindIconStyle(hour: Formatted<IMappedForecastHour>) {\n            return {\n                transformOrigin: 'center',\n                transform: `rotate(${hour.windDeg.raw}deg)`\n            };\n        }\n\n        function getOptionClass(key: TREND): string {\n            return key === type.value && 'menu-item--active';\n        }\n\n        function setTrend(value: TREND): void {\n            type.value = value;\n        }\n\n        return {\n            type,\n            hours,\n            trends,\n            trend,\n            getTime,\n            getIcon,\n            getWindIconStyle,\n            getOptionClass,\n            setTrend\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n    @import \"~@ocula/style/src/_mixins.scss\";\n\n    .forecast-hourly__header {\n        padding: 0 var(--spacing__medium) 0 var(--spacing__large) ;\n    }\n\n    .forecast-hourly__options {\n        width: auto;\n    }\n\n</style>"
  },
  {
    "path": "client/src/components/forecast/summary.vue",
    "content": "<template>\n    <div class=\"forecast-summary\">\n        <div layout=\"row center-justify\">\n            <div>\n                <div class=\"forecast-summary__temp\">{{ forecast.current.temp.formatted }}</div>\n                <div class=\"forecast-summary__feels-like\">\n                    <small>Feels like {{ forecast.current.feelsLike.formatted }}</small>\n                </div>\n                <div class=\"forecast-summary__description\">\n                    <small>{{ forecast.current.weather.description.formatted }}</small>\n                </div>\n            </div>\n            <div>\n                <img class=\"forecast-summary__figure\" :src=\"getFigure(forecast.current.weather.id.raw)\" :alt=\"forecast.current.weather.description.raw\">\n            </div>\n        </div>\n        <div class=\"forecast-summary__last-updated\" v-if=\"lastUpdated\">\n            <small>Updated {{ lastUpdated }} ago</small>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport getFigure from '../../helpers/get-figure';\n\nimport {\n    defineComponent,\n    ref\n} from \"vue\";\n\nimport {\n    state,\n    forecast\n} from '../../store';\n\nimport {\n    useTimer\n} from '@ocula/components';\n\nimport {\n    dateFormatDistanceToNow\n} from '@ocula/utilities';\n\nexport default defineComponent({\n    \n    setup() {\n        const lastUpdated = ref('');\n\n        useTimer(() => {\n            if (state.lastUpdated) {\n                lastUpdated.value = dateFormatDistanceToNow(state.lastUpdated);\n            }\n        }, 10000);\n\n        return {\n            lastUpdated,\n            forecast,\n            getFigure\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n    @import \"~@ocula/style/src/_mixins.scss\";\n\n    .forecast-summary__temp {\n        margin-bottom: var(--spacing__small);\n        font-size: 4rem;\n        font-weight: var(--font__weight--medium);\n        line-height: 1;\n    }\n\n    .forecast-summary__figure {\n        width: 96px;\n        height: 96px;\n    }\n\n    .forecast-summary__last-updated {\n        margin-top: var(--spacing__large);\n        opacity: 0.5;\n    }\n\n    @include breakpoint(\"lg\") {\n        \n        .forecast-summary__figure {\n            width: 192px;\n            height: 192px;\n        }\n\n    }\n\n</style>"
  },
  {
    "path": "client/src/components/forecast/tides.vue",
    "content": "<template>\n    <div class=\"forecast-tides\">\n        <div class=\"forecast-tides__header\" :grid=\"tides.extremes.length\">\n            <div class=\"forecast-tides__observation\" v-for=\"entry in tides.extremes\" :key=\"entry.dt.raw\">\n                <div class=\"text--meta\">\n                    <small>{{ entry.type.formatted }}</small>\n                </div>\n                <div>{{ getTime(entry) }}</div>\n                <div>\n                    <small>{{ entry.height.formatted }}</small>\n                </div>\n            </div>\n        </div>\n        <trends class=\"forecast-tides__trends\" :data=\"tides.heights\" :options=\"chartOptions\" :key-by=\"entry => entry.dt.raw\">\n            <template #start-label=\"data\">{{ getTime(data.value) }}</template>\n            <template #primary-column=\"column\">\n                <small>{{ getTime(column.value) }}</small>\n            </template>\n            <template #end-label=\"data\">{{ getTime(data.value) }}</template>\n        </trends>\n        <div class=\"forecast-tides__disclaimer\">\n            <small class=\"text--meta\">All tide measurements are displayed in metres (m)</small>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport TIDES_CHART_OPTIONS from '../../constants/forecast/tides-chart-options';\n\nimport Trends from '../charts/trends.vue';\n\nimport {\n    computed,\n    defineComponent\n} from 'vue';\n\nimport {\n    forecast,\n    format\n} from '../../store';\n\nexport default defineComponent({\n\n    components: {\n        Trends\n    },\n   \n    setup(props) {\n        const tides = computed(() => forecast.value.tides);\n\n        function getTime(entry): string {\n            return format.value.time(entry.dt.formatted as any, 'h a');\n        }\n\n        return {\n            tides,\n            getTime,\n            chartOptions: TIDES_CHART_OPTIONS\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .forecast-tides__header {\n        padding: 0 var(--spacing__large);\n    }\n    \n    .forecast-tides__observation {\n        text-align: center;\n    }\n\n    .forecast-tides__trends {\n        margin: var(--spacing__small) 0;\n    }\n\n    .forecast-tides__disclaimer {\n        text-align: center;\n    }\n\n</style>"
  },
  {
    "path": "client/src/components/forecast/today.vue",
    "content": "<template>\n    <transition-box-resize class=\"forecast-today\" grid=\"3\">\n        <div class=\"forecast-today__observation\" v-for=\"observation in observations\" :key=\"observation.label\">\n            <div class=\"text--meta\">\n                <small>{{ observation.label }}</small>\n            </div>\n            <div>{{ observation.value }}</div>\n        </div>\n    </transition-box-resize>\n    <div class=\"margin__top--small text--centre\">\n        <icon-button :icon=\"detailedViewButton.icon\" v-tooltip=\"detailedViewButton.tooltip\" @click=\"toggleDetailedView\"></icon-button>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport Observation from '../weather/observation.vue';\n\nimport {\n    defineComponent,\n    ref,\n    computed\n} from 'vue';\n\nimport {\n    forecast,\n    format\n} from '../../store';\n\nimport {\n    numberRound\n} from '@ocula/utilities';\n\nexport default defineComponent({\n\n    components: {\n        Observation\n    },\n    \n    setup() {\n        const showDetailedView = ref(false);\n\n        const detailedViewButton = computed(() => ({\n            icon: showDetailedView.value ? 'arrow-up-s-line' : 'arrow-down-s-line',\n            tooltip: `Show ${showDetailedView.value ? 'less' : 'more'} detail`\n        }));\n\n        function toggleDetailedView() {\n            showDetailedView.value = !showDetailedView.value;\n        }\n\n        const observations = computed(() => {\n            const {\n                today,\n                current\n            } = forecast.value;\n\n            const output = [\n                {\n                    icon: 'temp-cold-line',\n                    label: 'Temp',\n                    value: `${numberRound(today.temp.min.raw)} / ${numberRound(today.temp.max.raw)}`\n                },\n                {\n                    icon: 'contrast-drop-2-line',\n                    label: 'Humidity',\n                    value: current.humidity.formatted\n                },\n                {\n                    icon: 'rainy-line',\n                    label: 'Precipitation',\n                    value: `${today.pop.formatted} chance`\n                },\n                {\n                    icon: 'sun-line',\n                    label: 'Sunrise',\n                    value: format.value.time(today.sunrise.formatted as any)\n                },\n                {\n                    icon: 'moon-line',\n                    label: 'Sunset',\n                    value: format.value.time(today.sunset.formatted as any)\n                },\n                {\n                    icon: 'windy-line',\n                    label: 'Wind',\n                    value: `${current.windSpeed.formatted} ${current.windDeg.formatted}`\n                },\n                {\n                    icon: 'swap-line',\n                    label: 'Pressure',\n                    value: current.pressure.formatted\n                },\n                {\n                    icon: 'cloudy-line',\n                    label: 'Cloud Cov.',\n                    value: current.clouds.formatted\n                },\n                {\n                    icon: 'eye-line',\n                    label: 'Visibility',\n                    value: current.visibility.formatted\n                }\n            ];\n\n            if (showDetailedView.value) {\n                return output;\n            }\n\n            return output.slice(0, 6);\n        });\n\n        return {\n            observations,\n            detailedViewButton,\n            toggleDetailedView\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .forecast-today__observation {\n        text-align: center;\n    }\n\n</style>"
  },
  {
    "path": "client/src/components/forecast/uv-index.vue",
    "content": "<template>\n    <div class=\"forecast-uv-index\">\n        <div class=\"forecast-uv-index__bar-wrapper\">\n            <div class=\"forecast-uv-index__bar\" :style=\"barStyle\">\n                <div class=\"forecast-uv-index__marker\" :data-value=\"markerValue\" :style=\"markerStyle\"></div>\n            </div>\n        </div>\n        <div class=\"forecast-uv-index__legend\" layout=\"rows center-center\">\n            <div class=\"forecast-uv-index__legend-key\" layout=\"row center-left\" v-for=\"key in legend\" :key=\"key.id\">\n                <div class=\"dot margin__right--x-small\" :style=\"{ background: key.colour }\"></div>\n                <small class=\"text--meta\">{{ key.label }}</small>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport UVINDEX from '../../constants/forecast/uv-index';\n\nimport {\n    defineComponent,\n    computed\n} from 'vue';\n\nimport {\n    forecast\n} from '../../store';\n\nimport {\n    arrayJoinBy,\n    numberMaxBy,\n    numberRound,\n    scaleContinuous\n} from '@ocula/utilities';\n\nconst MIN = 0;\nconst MAX = numberMaxBy(UVINDEX, ({ start }) => start).start + 2;\n\nconst offsetScale = scaleContinuous([MIN, MAX], [0, 100]);\n\nexport default defineComponent({\n\n    setup(props) {\n        const gradient = arrayJoinBy(UVINDEX, ({ colour, start }) => `${colour} ${offsetScale(start)}%`);\n\n        const barStyle = {\n            background: `linear-gradient(to right, ${gradient})`\n        };\n\n        const markerValue = computed(() => forecast.value.current.uvi.formatted);\n\n        const markerStyle = computed(() => ({\n            left: `${numberRound(offsetScale(forecast.value.current.uvi.raw, true), 2)}%`\n        }));\n\n        return {\n            barStyle,\n            markerValue,\n            markerStyle,\n            legend: UVINDEX\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .forecast-uv-index__bar-wrapper {\n        padding: 2rem 0 var(--spacing__x-small) 0;\n    }\n\n    .forecast-uv-index__bar {\n        position: relative;\n        display: block;\n        width: 100%;\n        height: 0.5rem;\n        border-radius: 0.25rem;\n    }\n\n    .forecast-uv-index__marker {\n        position: absolute;\n        top: 0;\n        left: 0;\n        width: 0.5em;\n        height: 100%;\n        border-radius: 50%;\n        background-color: var(--background__colour--hover);\n        transform: translateX(-50%);\n        overflow: visible;\n        transition: left var(--transition__timing--long) var(--transition__easing--default);\n\n        &::before,\n        &::after {\n            position: absolute;\n            display: block;\n            content: '';\n            bottom: 100%;\n            left: 50%;\n            transform: translateX(-50%);\n        }\n\n        &::before {\n            margin-bottom: 0.1rem;\n            border-top: 0.5rem solid var(--background__colour--hover);\n            border-left: 0.5rem solid transparent;\n            border-right: 0.5rem solid transparent;\n        }\n\n        &::after {\n            content: attr(data-value);\n            margin-bottom: 0.6rem;\n            padding: var(--spacing__xx-small) var(--spacing__x-small);\n            font-size: var(--font__size--small);\n            font-weight: var(--font__weight--heavy);\n            background: var(--background__colour--hover);\n            border-radius: var(--border__radius);\n        }\n    }\n\n    .forecast-uv-index__legend-key {\n        width: auto;\n        margin: var(--spacing__xx-small) var(--spacing__x-small);\n    }\n\n</style>"
  },
  {
    "path": "client/src/components/layouts/settings.vue",
    "content": "<template>\n    <div class=\"settings-layout\">\n        <div class=\"settings-layout__header\" layout=\"row center-left\">\n            <router-link class=\"link--inherit\" :to=\"backRoute\" replace v-if=\"backRoute\">\n                <icon-button icon=\"arrow-left-line\" class=\"margin__right--small\"/>\n            </router-link>\n            <strong>{{ title }}</strong>\n        </div>\n        <div class=\"settings-layout__body\">\n            <slot></slot>\n        </div>\n    </div>\n</template>\n\n<script>\nimport {\n    defineComponent\n} from 'vue';\n\nexport default defineComponent({\n   \n   props: {\n\n       title: {\n           type: String,\n           default: 'Settings'\n       },\n\n       backRoute: {\n           type: Object\n       }\n\n   }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .settings-layout__header {\n        padding: var(--spacing__large);\n    }\n\n    .settings-layout__body {\n        padding-bottom: var(--spacing__large);\n    }\n\n</style>"
  },
  {
    "path": "client/src/components/layouts/weather.vue",
    "content": "<template>\n    <div class=\"weather-layout\">\n        <slot v-if=\"hasLocationSet\"></slot>\n        <container v-else>\n            <div class=\"weather-layout__empty-state\">\n                <img :src=\"logo\" alt=\"Ocula\">\n                <h3>Welcome to Ocula</h3>\n                <p>\n                    Get started by setting a location.\n                </p>\n                <p>\n                    You can set a location by either searching for a place you know or using your current GPS position.\n                </p>\n                <div layout=\"rows center-center\">\n                    <button class=\"button button--primary margin__all--x-small\" @click=\"setLocation\">Set location</button>\n                </div>\n            </div>\n        </container>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport EVENTS from '../../constants/core/events';\n\nimport applicationController from '../../controllers/application';\n\nimport logo from '../../assets/images/logo/logo-192.svg';\n\nimport {\n    defineComponent,\n    computed,\n    onMounted,\n    onActivated,\n    watch\n} from 'vue';\n\nimport {\n    state,\n    update,\n    setCurrentLocation\n} from '../../store';\n\nimport {\n    useSubscriber\n} from '@ocula/components';\n\nimport {\n    typeIsNil\n} from '@ocula/utilities';\n\nexport default defineComponent({\n\n    setup() {\n        const hasLocationSet = computed(() => !typeIsNil(state.settings.location));\n\n        const {\n            setLocation\n        } = applicationController;\n\n        function updateForecast() {\n            if (hasLocationSet.value) {\n                update();\n            }\n        }\n\n        onMounted(updateForecast);\n        onActivated(updateForecast);\n        \n        watch(() => state.settings.location, updateForecast);\n\n        useSubscriber(EVENTS.application.visible, updateForecast);\n        \n        return {\n            logo,\n            hasLocationSet,\n            setLocation,\n            setCurrentLocation\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .weather-layout {\n    }\n\n    .weather-layout__empty-state {\n        padding: var(--spacing__large);\n        text-align: center;\n    }\n\n</style>"
  },
  {
    "path": "client/src/components/modals/location.vue",
    "content": "<template>\n    <modal :id=\"id\" class=\"location-modal\" ref=\"modal\" @open=\"reset\">\n        <search-box class=\"location-modal__search\" placeholder=\"Search for a location...\" :loading=\"loading\" v-model=\"search\" v-focus />\n        <div class=\"menu margin__top--small\">\n            <div class=\"menu-item\" layout=\"row center-left\" @click=\"setCurrentLocation\">\n                <icon name=\"gps-line\" class=\"margin__right--small\"/>\n                <div class=\"text--truncate\" self=\"size-x1\">Current Location</div>\n            </div>\n            <template v-if=\"query\">\n                <div class=\"menu-item\" layout=\"row center-left\" v-for=\"location in searchResults\" :key=\"location.id\" @click=\"addLocation(location, true)\">\n                    <icon name=\"map-pin-line\" class=\"margin__right--small\"/>\n                    <div class=\"text--truncate\" self=\"size-x1\">{{ location.longName }}</div>\n                </div>\n            </template>\n            <template v-else>\n                <div class=\"menu-item\" layout=\"row center-justify\" v-for=\"location in locations\" :key=\"location.id\" @click=\"setLocation(location)\">\n                    <icon name=\"star-line\" class=\"margin__right--small\"/>\n                    <div class=\"text--truncate\" self=\"size-x1\">{{ location.longName }}</div>\n                    <div v-tooltip:left=\"'Remove Location'\" @click.stop=\"removeLocation(location)\">\n                        <icon name=\"delete-bin-line\" class=\"margin__left--small\"/>\n                    </div>\n                </div>\n            </template>\n        </div>\n    </modal>\n</template>\n\n<script lang=\"ts\">\nimport MODALS from '../../constants/core/modals';\n\nimport {\n    defineComponent,\n    ref,\n    computed\n} from 'vue';\n\nimport {\n    state,\n    searchLocations,\n    setLocation,\n    setCurrentLocation,\n    addLocation,\n    removeLocation\n} from '../../store';\n\nimport {\n    functionDebounce\n} from '@ocula/utilities';\n\nimport type {\n    ILocation\n} from '../../types/location';\n\nexport default defineComponent({\n\n    setup() {\n        const id = MODALS.locations;\n\n        const modal = ref(null);\n\n        const query = ref('');\n        const searchResults = ref<ILocation[]>([]);\n        const loading = ref(false);\n\n        const locations = computed(() => state.settings.locations);\n\n        const executeSearch = functionDebounce(async function(query) {\n            loading.value = true;\n\n            try {\n                searchResults.value = await searchLocations(query);\n            } finally {\n                loading.value = false;\n            }\n        }, 500);\n\n        const search = computed({\n            get: () => query.value,\n            set: value => {\n                query.value = value;\n\n                if (value && value.length > 0) {\n                    executeSearch(value);\n                }\n            }\n        });\n\n        function closeInvoke(callback) {\n            return (...args) => {\n                try {\n                    callback(...args);\n                } finally {\n                    modal.value.close();\n                }\n            };\n        }\n\n        function reset() {\n            query.value = '';\n            searchResults.value = [];\n        }\n\n        return {\n            id,\n            modal,\n            query,\n            loading,\n            search,\n            reset,\n            locations,\n            searchResults,\n            removeLocation,\n            addLocation: closeInvoke(addLocation),\n            setLocation: closeInvoke(setLocation),\n            setCurrentLocation: closeInvoke(setCurrentLocation)\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .location-modal {\n\n        & .modal__body {\n            padding: var(--spacing__small);\n        }\n    }\n\n    .location-modal__search {\n        width: 100%;\n        border: none;\n    }\n\n</style>"
  },
  {
    "path": "client/src/components/settings/settings-item.vue",
    "content": "<template>\n    <div class=\"settings-item\" layout=\"row center-justify\">\n        <div class=\"settings-item__label\">\n            <slot name=\"label\">{{ label }}</slot>\n        </div>\n        <div class=\"settings-item__value text--meta\">\n            <slot name=\"value\">{{ value }}</slot>\n        </div>\n        <slot></slot>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport {\n    defineComponent\n} from 'vue';\n\nexport default defineComponent({\n    \n    props: {\n\n        label: {\n            type: String\n        },\n\n        value: {\n            type: null\n        }\n\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .settings-item {\n        position: relative;\n\n        & select {\n            position: absolute;\n            display: block;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            opacity: 0;\n        }\n    }\n\n</style>"
  },
  {
    "path": "client/src/components/weather/actions.vue",
    "content": "<template>\n    <div class=\"weather-actions\" :class=\"actionsClass\" layout=\"row center-justify\">\n        <icon-button class=\"weather-actions__action weather-actions__action--location\" icon=\"map-pin-line\" v-tooltip:right=\"'Set location'\" @click=\"setLocation\">\n            <div v-if=\"location\">{{ location.shortName }}</div>\n            <div v-else>Unknown</div>\n        </icon-button>\n        <div self=\"size-x1\">\n            <slot></slot>\n        </div>\n        <icon-button class=\"weather-actions__action weather-actions__action--update\" icon=\"refresh-line\" @click=\"update(true)\" v-tooltip:left=\"'Update'\"></icon-button>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport STATUS from '../../enums/core/status';\n\nimport applicationController from '../../controllers/application';\n\nimport {\n    defineComponent,\n    computed\n} from 'vue';\n\nimport {\n    state,\n    update\n} from '../../store';\n\nexport default defineComponent({\n   \n    setup() {\n        const {\n            setLocation\n        } = applicationController;\n\n        const location = computed(() => state.location);\n        const actionsClass = computed(() => state.status === STATUS.loading && 'weather-actions--loading');\n\n        return {\n            location,\n            setLocation,\n            update,\n            actionsClass\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .weather-actions {\n        padding: var(--spacing__small) var(--spacing__medium);\n    }\n\n    .weather-actions--loading {\n\n        & .weather-actions__action {\n            pointer-events: none;\n            cursor: not-allowed;\n        }\n\n        & .weather-actions__action--update {\n\n            & .icon {\n                animation: rotate 750ms ease-out infinite;\n            }\n        }\n    }\n\n    @keyframes rotate {\n\n        from {\n            transform: rotate(0deg);\n        }\n\n        to {\n            transform: rotate(360deg);\n        }\n\n    }\n\n</style>\n"
  },
  {
    "path": "client/src/components/weather/observation.vue",
    "content": "<template>\n    <div class=\"weather-observation\" layout=\"row center-left\">\n        <icon class=\"margin__right--small\" :name=\"icon\" v-if=\"icon\"/>\n        <div class=\"text--truncate\">\n            <strong>\n                <slot name=\"label\">{{ label }}</slot>\n            </strong>\n            <div>\n                <slot></slot>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport {\n    defineComponent\n} from 'vue';\n\nexport default defineComponent({\n    \n    props: {\n\n        icon: {\n            type: String\n        },\n\n        label: {\n            type: String\n        }\n\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .weather-observation {\n        display: inline-flex;\n        width: auto;\n        overflow: hidden;\n        padding: var(--spacing__x-small) var(--spacing__small);\n        background: var(--background__colour--hover);\n        border-radius: var(--border__radius);\n    }\n\n</style>"
  },
  {
    "path": "client/src/constants/core/data.ts",
    "content": "export default {\n    lastUpdated: null,\n    location: null,\n    forecast: null\n};"
  },
  {
    "path": "client/src/constants/core/drawers.ts",
    "content": "export default {\n    maps: 'drawer:maps'\n} as const;"
  },
  {
    "path": "client/src/constants/core/events.ts",
    "content": "export default {\n    application: {\n        visible: 'application:visible',\n        resized: 'application:resized'\n    },\n    storage: {\n        dataSaved: 'storage:data-saved',\n        settingsSaved: 'storage:settings-saved'\n    },\n    location: {\n        set: 'location:set'\n    }\n};"
  },
  {
    "path": "client/src/constants/core/global.ts",
    "content": "export default {\n    updateThreshold: 1800000\n};"
  },
  {
    "path": "client/src/constants/core/migrations.ts",
    "content": "import SETTINGS from '../core/settings';\n\nimport {\n    arrayUnionWith,\n    objectTransform\n} from '@ocula/utilities';\n\nimport type {\n    ISettings\n} from '../../types/storage';\n\ntype Migration = (settings: ISettings) => ISettings;\n\n/*\nDictionary to migrate settings structures to new versions.\nFor example, removing a section using the object transformer:\n{\n    3: settings => objectTransform(settings, {\n        forecast: {\n            sections: sections => sections.filter(({ type }) => type !== 'today')\n        }\n    })\n}\n*/\n\nexport default {\n    '1': settings => objectTransform(settings, {\n        forecast: {\n            sections: sections => arrayUnionWith(sections, SETTINGS.forecast.sections, (a, b) => a.type === b.type)\n        }\n    })\n} as Record<number, Migration>"
  },
  {
    "path": "client/src/constants/core/modals.ts",
    "content": "export default {\n    locations: 'modal:locations'\n} as const;"
  },
  {
    "path": "client/src/constants/core/routes.ts",
    "content": "export default {\n    forecast: {\n        index: 'forecast:index',\n    },\n    maps: {\n        index: 'maps:index'\n    },\n    settings: {\n        index: 'settings:index',\n        forecast: {\n            locations: 'settings:forecast:locations',\n            sections: 'settings:forecast:sections',\n        },\n        maps: {\n            display: 'settings:maps:display'\n        },\n        general: {\n            theme: 'settings:general:theme',\n            about: 'settings:general:about'\n        }\n    },\n    error: {\n        index: 'error:index',\n        notFound: 'error:not:found'\n    }\n} as const;"
  },
  {
    "path": "client/src/constants/core/settings.ts",
    "content": "import UNITS from '../../enums/forecast/units';\nimport MAP from '../../enums/maps/map';\nimport FORECAST_SECTION from '../../enums/forecast/section';\n\nimport type {\n    ISettings\n} from '../../types/storage';\n\nexport default {\n    version: 1.1,\n    units: UNITS.metric,\n    theme: 'default',\n    location: null,\n    locations: [],\n    forecast: {\n        sections: [\n            {\n                type: FORECAST_SECTION.today,\n                visible: true\n            },\n            {\n                type: FORECAST_SECTION.dailyForecast,\n                visible: true\n            },\n            {\n                type: FORECAST_SECTION.hourlyForecast,\n                visible: true\n            },\n            {\n                type: FORECAST_SECTION.uvIndex,\n                visible: true\n            },\n            {\n                type: FORECAST_SECTION.tides,\n                visible: true\n            }\n        ]\n    },\n    maps: {\n        default: MAP.radar,\n        zoom: 6,\n        pitch: 0,\n        framerate: 500\n    }\n} as ISettings;"
  },
  {
    "path": "client/src/constants/core/storage-keys.ts",
    "content": "export default {\n    data: 'ocula:data',\n    settings: 'ocula:settings'\n};"
  },
  {
    "path": "client/src/constants/forecast/directions.ts",
    "content": "export default [\n    'N',\n    'NNE',\n    'NE',\n    'ENE',\n    'E',\n    'ESE',\n    'SE',\n    'SSE',\n    'S',\n    'SSW',\n    'SW',\n    'WSW',\n    'W',\n    'WNW',\n    'NW',\n    'NNW'\n];"
  },
  {
    "path": "client/src/constants/forecast/figure.ts",
    "content": "import figures from '../../assets/images/figures';\nimport PHASE from '../../enums/forecast/phase';\n\ninterface IFigure extends Record<number, string> {};\n\nexport default {\n    [PHASE.day]: {\n        200: figures.storm,\n        300: figures.lightRain,\n        500: figures.rain,\n        600: figures.snow,\n        700: figures.partlyCloudyDay,\n        800: figures.sun,\n        801: figures.partlyCloudyDay,\n        802: figures.partlyCloudyDay,\n        803: figures.partlyCloudyDay,\n        804: figures.partlyCloudyDay,\n    },\n    [PHASE.night]: {\n        200: figures.stormyNight,\n        300: figures.rainyNight,\n        500: figures.rainyNight,\n        600: figures.snow,\n        700: figures.partlyCloudyNight,\n        800: figures.fullMoon,\n        801: figures.partlyCloudyNight,\n        802: figures.partlyCloudyNight,\n        803: figures.partlyCloudyNight,\n        804: figures.partlyCloudyNight,\n    }\n} as Record<PHASE, IFigure>;"
  },
  {
    "path": "client/src/constants/forecast/formats.ts",
    "content": "import UNITS from '../../enums/forecast/units';\n\nimport FORMATTERS, {\n    defaultFormatter\n} from './formatters';\n\nimport {\n    objectMerge,\n    objectTransform\n} from '@ocula/utilities';\n\nimport type {\n    IForecastWeather\n} from '../../types/weather';\n\nconst {\n    general,\n    temperature,\n    distance,\n    speed,\n    pressure,\n    direction\n} = FORMATTERS;\n\nfunction weatherTransform(value: IForecastWeather[]): Record<string, any> {\n    return objectTransform(value[0], {\n        description: general.description\n    }, defaultFormatter);\n}\n\nconst BASE_FORMATS = {\n    current: {\n        dt: general.datetime,\n        sunrise: general.datetime,\n        sunset: general.datetime,\n        humidity: general.percentage,\n        clouds: general.percentage,\n        windDeg: direction.bearing,\n        weather: weatherTransform\n    },\n    tides: {\n        heights: [\n            {\n                dt: general.datetime,\n                height: distance.metres\n            }\n        ],\n        extremes: [\n            {\n                dt: general.datetime,\n                height: distance.metres\n            }\n        ]\n    },\n    radar: {\n        timestamps: [\n            general.datetime\n        ]\n    }\n};\n\nexport default {\n    [UNITS.metric]: objectMerge(BASE_FORMATS, {\n        current: {\n            temp: temperature.celcius,\n            feelsLike: temperature.celcius,\n            pressure: pressure.hectopascals,\n            dewPoint: temperature.celcius,\n            visibility: distance.metres,\n            windSpeed: speed.metresPerSecond,\n        },\n        daily: [\n            {\n                dt: general.datetime,\n                sunrise: general.datetime,\n                sunset: general.datetime,\n                temp: {\n                    day: temperature.celcius,\n                    min: temperature.celcius,\n                    max: temperature.celcius,\n                    night: temperature.celcius,\n                    eve: temperature.celcius,\n                    morn: temperature.celcius\n                },\n                feelsLike: {\n                    day: temperature.celcius,\n                    night: temperature.celcius,\n                    eve: temperature.celcius,\n                    morn: temperature.celcius\n                },\n                pressure: pressure.hectopascals,\n                humidity: general.percentage,\n                dewPoint: temperature.celcius,\n                windSpeed: speed.metresPerSecond,\n                windDeg: direction.bearing,\n                weather: weatherTransform,\n                clouds: general.percentage,\n                rain: distance.millimeters,\n                pop: general.fractional\n            }\n        ],\n        hourly: [\n            {\n                dt: general.datetime,\n                temp: temperature.celcius,\n                feelsLike: temperature.celcius,\n                pressure: pressure.hectopascals,\n                humidity: general.percentage,\n                dewPoint: temperature.celcius,\n                clouds: general.percentage,\n                visibility: distance.metres,\n                windSpeed: speed.metresPerSecond,\n                windDeg: direction.bearing,\n                pop: general.fractional,\n                weather: weatherTransform,\n            }\n        ]\n    }),\n    [UNITS.imperial]: objectMerge(BASE_FORMATS, {\n        current: {\n            temp: temperature.fahrenheit,\n            feelsLike: temperature.fahrenheit,\n            pressure: pressure.millibars,\n            dewPoint: temperature.fahrenheit,\n            visibility: distance.miles,\n            windSpeed: speed.milesPerHour,\n        },\n        daily: [\n            {\n                dt: general.datetime,\n                sunrise: general.datetime,\n                sunset: general.datetime,\n                temp: {\n                    day: temperature.fahrenheit,\n                    min: temperature.fahrenheit,\n                    max: temperature.fahrenheit,\n                    night: temperature.fahrenheit,\n                    eve: temperature.fahrenheit,\n                    morn: temperature.fahrenheit\n                },\n                feelsLike: {\n                    day: temperature.fahrenheit,\n                    night: temperature.fahrenheit,\n                    eve: temperature.fahrenheit,\n                    morn: temperature.fahrenheit\n                },\n                pressure: pressure.millibars,\n                humidity: general.percentage,\n                dewPoint: temperature.fahrenheit,\n                windSpeed: speed.milesPerHour,\n                windDeg: direction.bearing,\n                weather: weatherTransform,\n                clouds: general.percentage,\n                rain: distance.inches,\n                pop: general.fractional\n            }\n        ],\n        hourly: [\n            {\n                dt: general.datetime,\n                temp: temperature.fahrenheit,\n                feelsLike: temperature.fahrenheit,\n                pressure: pressure.millibars,\n                humidity: general.percentage,\n                dewPoint: temperature.fahrenheit,\n                clouds: general.percentage,\n                visibility: distance.miles,\n                windSpeed: speed.milesPerHour,\n                windDeg: direction.bearing,\n                pop: general.fractional,\n                weather: weatherTransform\n            }\n        ]\n    })\n}"
  },
  {
    "path": "client/src/constants/forecast/formatters.ts",
    "content": "import UNIT_OF_MEASURE from '../../enums/forecast/unit-of-measure';\n\nimport getIcon from '../../helpers/get-icon';\nimport getDirection from '../../helpers/get-direction';\n\nimport {\n    dateFromUnix,\n    functionIdentity,\n    stringCapitalize\n} from '@ocula/utilities';\n\nfunction baseFormatter<T>(raw: T, formatted: any) {\n    return {\n        raw,\n        formatted,\n\n        toString() {\n            return formatted;\n        }\n    };\n};\n\nfunction toSuffix(suffix: string, transformer: Function = functionIdentity) {\n    return value => baseFormatter(value, `${transformer(value)}${suffix}`);\n}\n\nexport function defaultFormatter(value) {\n    return baseFormatter(value, value);\n}\n\nexport default {\n    distance: {\n        millimeters: toSuffix(UNIT_OF_MEASURE.millimeters),\n        centimetres: toSuffix(UNIT_OF_MEASURE.centimetres),\n        metres: toSuffix(UNIT_OF_MEASURE.metres),\n        kilometres: toSuffix(UNIT_OF_MEASURE.kilometres),\n        miles: toSuffix(UNIT_OF_MEASURE.miles),\n        inches: toSuffix(UNIT_OF_MEASURE.inches)\n    },\n    speed: {\n        millimetresPerHour: toSuffix(UNIT_OF_MEASURE.millimetresPerHour),\n        kilometresPerHour: toSuffix(UNIT_OF_MEASURE.kilometresPerHour),\n        metresPerSecond: toSuffix(UNIT_OF_MEASURE.metresPerSecond),\n        inchesPerHour: toSuffix(UNIT_OF_MEASURE.inchesPerHour),\n        milesPerHour: toSuffix(UNIT_OF_MEASURE.milesPerHour)\n    },\n    temperature: {\n        celcius: toSuffix(UNIT_OF_MEASURE.celcius, Math.round),\n        fahrenheit: toSuffix(UNIT_OF_MEASURE.fahrenheit, Math.round),\n    },\n    pressure: {\n        hectopascals: toSuffix(UNIT_OF_MEASURE.hectopascals),\n        millibars: toSuffix(UNIT_OF_MEASURE.millibars)\n    },\n    direction: {\n        bearing: value => baseFormatter(value, getDirection(value))\n    },\n    general: {\n        description: value => baseFormatter(value, stringCapitalize(value)),\n        datetime: value => baseFormatter(value, dateFromUnix(value)),\n        icon: value => baseFormatter(value, getIcon(value)),\n        percentage: toSuffix(UNIT_OF_MEASURE.percentage, value => Math.round(value)),\n        fractional: toSuffix(UNIT_OF_MEASURE.percentage, value => Math.round(value * 100))\n    }\n};"
  },
  {
    "path": "client/src/constants/forecast/icon.ts",
    "content": "import PHASE from '../../enums/forecast/phase';\n\nexport default {\n    [PHASE.day]: {\n        200: 'thunderstorms-line',\n        300: 'drizzle-line',\n        500: 'showers-line',\n        502: 'heavy-showers-line',\n        503: 'heavy-showers-line',\n        504: 'heavy-showers-line',\n        511: 'snowy-line',\n        600: 'snowy-line',\n        700: 'cloudy-line',\n        701: 'mist-line',\n        711: 'haze-line',\n        721: 'haze-line',\n        731: 'haze-line',\n        741: 'sun-foggy-line',\n        751: 'haze-line',\n        761: 'haze-line',\n        781: 'tornado-line',\n        800: 'sun-line',\n        801: 'cloudy-line',\n        802: 'cloudy-line',\n        803: 'cloudy-line',\n        804: 'cloudy-line'\n    },\n    [PHASE.night]: {\n        200: 'thunderstorms-line',\n        300: 'drizzle-line',\n        500: 'showers-line',\n        502: 'heavy-showers-line',\n        503: 'heavy-showers-line',\n        504: 'heavy-showers-line',\n        511: 'snowy-line',\n        600: 'snowy-line',\n        700: 'moon-cloudy-line',\n        701: 'mist-line',\n        711: 'haze-line',\n        721: 'haze-line',\n        731: 'haze-line',\n        741: 'moon-foggy-line',\n        751: 'haze-line',\n        761: 'haze-line',\n        781: 'tornado-line',\n        800: 'moon-clear-line',\n        801: 'moon-cloudy-line',\n        802: 'moon-cloudy-line',\n        803: 'moon-cloudy-line',\n        804: 'moon-cloudy-line',\n    }\n};"
  },
  {
    "path": "client/src/constants/forecast/sections.ts",
    "content": "import FORECAST_SECTION from '../../enums/forecast/section';\n\nimport DailyForecast from '../../components/forecast/daily-forecast.vue';\nimport HourlyForecast from '../../components/forecast/hourly-forecast.vue';\nimport Today from '../../components/forecast/today.vue';\nimport UvIndex from '../../components/forecast/uv-index.vue';\nimport Tides from '../../components/forecast/tides.vue';\n\nimport type {\n    Formatted,\n    IMappedForecast\n} from '../../types/state';\n\ninterface IForecastSection {\n    label: string;\n    component: typeof DailyForecast,\n    condition?(forecast: Formatted<IMappedForecast>): boolean;\n}\n\nexport default {\n    [FORECAST_SECTION.dailyForecast]: {\n        label: 'Daily Forecast',\n        component: DailyForecast\n    },\n    [FORECAST_SECTION.hourlyForecast]: {\n        label: 'Hourly Forecast',\n        component: HourlyForecast\n    },\n    [FORECAST_SECTION.today]: {\n        label: 'Today',\n        component: Today\n    },\n    [FORECAST_SECTION.uvIndex]: {\n        label: 'UV Index',\n        component: UvIndex\n    },\n    [FORECAST_SECTION.tides]: {\n        label: 'Tides',\n        component: Tides,\n        condition: forecast => forecast.tides && forecast.tides.status.raw === 200\n    }\n} as Record<FORECAST_SECTION, IForecastSection>;"
  },
  {
    "path": "client/src/constants/forecast/theme.ts",
    "content": "import {\n    weather\n} from '../../themes';\n\nexport default {\n    200: weather.rainy,\n    300: weather.rainy,\n    500: weather.rainy,\n    600: weather.rainy,\n    700: weather.partlyCloudy,\n    800: weather.clear,\n    801: weather.partlyCloudy,\n    802: weather.partlyCloudy,\n    803: weather.partlyCloudy,\n    804: weather.partlyCloudy,\n};"
  },
  {
    "path": "client/src/constants/forecast/tides-chart-options.ts",
    "content": "import {\n    ILineOptions,\n    LINE_TYPE\n} from '@ocula/charts';\n\nimport {\n    dateFromUnix,\n    numberRound\n} from '@ocula/utilities';\n\nimport type {\n    Formatted\n} from '../../types/state';\n\nimport type {\n    IForecastTideHeight\n} from '../../types/weather';\n\ntype ChartOptions = ILineOptions<Formatted<IForecastTideHeight>>;\n\nexport default {\n    type: LINE_TYPE.spline,\n    scales: {\n        x: {\n            type: 'time',\n            value: ({ dt }) => dateFromUnix(dt.raw)\n        },\n        y: {\n            type: 'linear',\n            ticks: 5,\n            value: ({ height }) => height.raw\n        }\n    },\n    labels: {\n        content: (point, index) => index ? numberRound(point.value.height.raw, 2) : null\n    },\n    colours: {\n        line: '#47B1FA',\n        marker: '#47B1FA'\n    }\n} as ChartOptions; "
  },
  {
    "path": "client/src/constants/forecast/trends.ts",
    "content": "import TREND from '../../enums/forecast/trend';\nimport OBSERVATION from '../../enums/forecast/observation';\n\nimport {\n    LINE_TYPE,\n    SCALE_TYPE,\n    ILineOptions\n} from '@ocula/charts';\n\nimport {\n    objectMerge,\n    dateFromUnix,\n    numberPercentage\n} from '@ocula/utilities';\n\nimport type {\n    IForecastHour\n} from '../../types/weather';\n\nimport {\n    Formatted\n} from '../../types/state';\n\ntype ChartOptions = ILineOptions<Formatted<IForecastHour>>;\n\ninterface ITrend {\n    icon: string;\n    label: string;\n    observation: OBSERVATION;\n    chartOptions: ChartOptions;\n}\n\nconst BASE_OPTIONS = {\n    type: LINE_TYPE.spline,\n    scales: {\n        x: {\n            type: SCALE_TYPE.time,\n            value: ({ dt }) => dateFromUnix(dt.raw)\n        },\n        y: {\n            type: SCALE_TYPE.linear,\n            ticks: 5\n        }\n    },\n    labels: {\n        content: (point, index) => index ? Math.round(point.yValue) : null\n    },\n    colours: {\n        tick: '#AAA'\n    },\n    padding: {\n        bottom: 0\n    }\n} as ChartOptions;\n\nexport default {\n    [TREND.temperature]: {\n        icon: 'temp-cold-line',\n        label: 'Temperature',\n        observation: OBSERVATION.temperature,\n        chartOptions: objectMerge(BASE_OPTIONS, {\n            scales: {\n                y: {\n                    value: ({ temp }) => temp.raw\n                }\n            },\n            colours: {\n                line: '#47B1FA',\n                marker: '#47B1FA'\n            }\n        })\n    },\n    [TREND.rainfall]: {\n        icon: 'rainy-line',\n        label: 'Precipitation',\n        observation: OBSERVATION.precipitation,\n        chartOptions: objectMerge(BASE_OPTIONS, {\n            type: LINE_TYPE.step,\n            scales: {\n                y: {\n                    value: ({ pop }) => pop.raw\n                }\n            },\n            labels: {\n                content: (point, index) => index ? numberPercentage(point.yValue, 1) : null\n            },\n            colours: {\n                line: '#47B1FA',\n                marker: '#47B1FA'\n            }\n        })\n    },\n    [TREND.wind]: {\n        icon: 'windy-line',\n        label: 'Wind',\n        observation: OBSERVATION.windSpeed,\n        chartOptions: objectMerge(BASE_OPTIONS, {\n            scales: {\n                y: {\n                    value: ({ windSpeed }) => windSpeed.raw\n                }\n            },\n            colours: {\n                line: '#47B1FA',\n                marker: '#47B1FA'\n            }\n        })\n    }\n} as Record<TREND, ITrend>;"
  },
  {
    "path": "client/src/constants/forecast/unit-of-measure.ts",
    "content": "import UNITS from '../../enums/forecast/units';\nimport OBSERVATION from '../../enums/forecast/observation';\nimport UNIT_OF_MEASURE from '../../enums/forecast/unit-of-measure';\n\ntype UnitOfMeasure = Record<OBSERVATION, UNIT_OF_MEASURE>\n\nexport default {\n    [UNITS.metric]: {\n        [OBSERVATION.temperature]: UNIT_OF_MEASURE.celcius,\n        [OBSERVATION.pressure]: UNIT_OF_MEASURE.hectopascals,\n        [OBSERVATION.windSpeed]: UNIT_OF_MEASURE.metresPerSecond,\n        [OBSERVATION.precipitation]: UNIT_OF_MEASURE.percentage\n    },\n    [UNITS.imperial]: {\n        [OBSERVATION.temperature]: UNIT_OF_MEASURE.fahrenheit,\n        [OBSERVATION.pressure]: UNIT_OF_MEASURE.millibars,\n        [OBSERVATION.windSpeed]: UNIT_OF_MEASURE.milesPerHour,\n        [OBSERVATION.precipitation]: UNIT_OF_MEASURE.percentage\n    }\n} as Record<UNITS, UnitOfMeasure>"
  },
  {
    "path": "client/src/constants/forecast/units.ts",
    "content": "import UNITS from '../../enums/forecast/units';\n\nexport default {\n    [UNITS.metric]: {\n        label: 'Metric'\n    },\n    [UNITS.imperial]: {\n        label: 'Imperial'\n    }\n} as const;"
  },
  {
    "path": "client/src/constants/forecast/uv-index.ts",
    "content": "interface IUVIndex {\n    id: string;\n    label: string;\n    start: number;\n    colour: string;\n};\n\nexport default [\n    {\n        id: 'low',\n        label: 'Low',\n        start: 0,\n        colour: '#3FA72D'\n    },\n    {\n        id: 'moderate',\n        label: 'Moderate',\n        start: 3,\n        colour: '#FFF301'\n    },\n    {\n        id: 'high',\n        label: 'High',\n        start: 6,\n        colour: '#F18B00'\n    },\n    {\n        id: 'veryHigh',\n        label: 'Very High',\n        start: 8,\n        colour: '#E53110'\n    },\n    {\n        id: 'extreme',\n        label: 'Extreme',\n        start: 11,\n        colour: '#B467A4'\n    }\n] as IUVIndex[];"
  },
  {
    "path": "client/src/constants/maps/maps.ts",
    "content": "import MAP from '../../enums/maps/map';\n\nimport type {\n    Formatted,\n    IFormatter,\n    IMappedForecast\n} from '../../types/state';\n\ninterface IMapLegend {\n    colour: string;\n    label: string;\n}\n\ninterface IMapLayer {\n    id: string;\n    url: string;\n    label?: string;\n}\n\ninterface IMap {\n    label: string;\n    icon: string;\n    layers: IMapLayer[] | (() => IMapLayer[]);\n    legend?: IMapLegend[];\n}\n\nfunction getOwmTileUrl(layer: string): string {\n    return `https://tile.openweathermap.org/map/${layer}/{z}/{x}/{y}.png?appid=${process.env.OWM_API_KEY}`;\n}\n\nfunction getRadarLayers(forecast: Formatted<IMappedForecast>, format: IFormatter, smooth: boolean = true, snow: boolean = true): IMapLayer[] {\n    let timestamps = forecast.radar.timestamps;\n\n    return timestamps.map(({ raw, formatted }) => ({\n        id: raw.toString(),\n        label: format.time(formatted),\n        url: `https://tilecache.rainviewer.com/v2/radar/${raw}/256/{z}/{x}/{y}/2/${+smooth}_${+snow}.png`\n    }));\n}\n\nexport default {\n    [MAP.radar]: {\n        label: 'Radar',\n        icon: 'radar-line',\n        layers: getRadarLayers,\n        legend: [\n            {\n                colour: '#8EE',\n                label: 'Light Drizzle'\n            },\n            {\n                colour: '#09C',\n                label: 'Drizzle'\n            },\n            {\n                colour: '#07A',\n                label: 'Light Rain'\n            },\n            {\n                colour: '#058',\n                label: 'Light Rain'\n            },\n            {\n                colour: '#FE0',\n                label: 'Rain'\n            },\n            {\n                colour: '#FA0',\n                label: 'Rain'\n            },\n            {\n                colour: '#F70',\n                label: 'Heavy Rain'\n            },\n            {\n                colour: '#F40',\n                label: 'Heavy Rain'\n            },\n            {\n                colour: '#E00',\n                label: 'Thunderstorm'\n            },\n            {\n                colour: '#900',\n                label: 'Thunderstorm'\n            },\n            {\n                colour: '#FAF',\n                label: 'Hail'\n            },\n            {\n                colour: '#F7F',\n                label: 'Hail'\n            }\n        ]\n    },\n    [MAP.precipitation]: {\n        label: 'Precipitation',\n        icon: 'drop-line',\n        layers: [\n            {\n                id: MAP.precipitation,\n                url: getOwmTileUrl('precipitation_new')\n            }\n        ]\n    },\n    [MAP.temperature]: {\n        label: 'Temperature',\n        icon: 'temp-cold-line',\n        layers: [\n            {\n                id: MAP.temperature,\n                url: getOwmTileUrl('temp_new')\n            }\n        ]\n    },\n    [MAP.cloud]: {\n        label: 'Cloud',\n        icon: 'cloudy-line',\n        layers: [\n            {\n                id: MAP.cloud,\n                url: getOwmTileUrl('clouds_new')\n            }\n        ]\n    },\n    [MAP.wind]: {\n        label: 'Wind',\n        icon: 'windy-line',\n        layers: [\n            {\n                id: MAP.wind,\n                url: getOwmTileUrl('wind_new')\n            }\n        ]\n    },\n    [MAP.pressure]: {\n        label: 'Pressure',\n        icon: 'swap-line',\n        layers: [\n            {\n                id: MAP.pressure,\n                url: getOwmTileUrl('pressure_new')\n            }\n        ]\n    }\n} as Record<MAP, IMap>;"
  },
  {
    "path": "client/src/controllers/application.ts",
    "content": "import MAP from '../enums/maps/map';\n\nimport EVENTS from '../constants/core/events';\nimport MODALS from '../constants/core/modals';\nimport DRAWERS from '../constants/core/drawers';\n\nimport eventEmitter from '@ocula/event-emitter';\n\nimport {\n    componentsController\n} from '@ocula/components';\n\nimport {\n    functionDebounce\n} from '@ocula/utilities';\n\nconst resize = functionDebounce(event => eventEmitter.emit(EVENTS.application.resized, event), 300);\n\nfunction visibilityChanged() {\n    if (!document.hidden) {\n        eventEmitter.emit(EVENTS.application.visible);\n    }     \n}\n\nwindow.addEventListener('resize', resize)\ndocument.addEventListener('visibilitychange', visibilityChanged);\n\nexport class ApplicationController {\n\n    constructor() {\n\n    }\n\n    async setLocation() {\n        return componentsController.open(MODALS.locations);\n    }\n\n    async setMapType(): Promise<MAP> {\n        return componentsController.open(DRAWERS.maps);\n    }\n\n    async notify(title: string, options?: NotificationOptions): Promise<Notification> {\n        if (Notification.permission !== 'granted') {\n            await Notification.requestPermission();\n        }\n        \n        return new Notification(title, options);\n    }\n\n}\n\nexport default new ApplicationController();"
  },
  {
    "path": "client/src/enums/core/status.ts",
    "content": "const enum STATUS {\n    loading = 'loading',\n    error = 'error'\n};\n\nexport default STATUS;"
  },
  {
    "path": "client/src/enums/forecast/location.ts",
    "content": "const enum LOCATION {\n    current = 'current'\n};\n\nexport default LOCATION;"
  },
  {
    "path": "client/src/enums/forecast/observation.ts",
    "content": "const enum OBSERVATION {\n    temperature = 'temperature',\n    precipitation = 'precipitation',\n    humidity = 'humidity',\n    sunrise = 'sunrise',\n    sunset = 'sunset',\n    pressure = 'pressure',\n    windSpeed = 'windSpeed',\n    windDirection = 'windDirection',\n};\n\nexport default OBSERVATION;"
  },
  {
    "path": "client/src/enums/forecast/phase.ts",
    "content": "const enum PHASE {\n    day = 'day',\n    night = 'night'\n};\n\nexport default PHASE;"
  },
  {
    "path": "client/src/enums/forecast/section.ts",
    "content": "const enum FORECAST_SECTION {\n    today = 'today',\n    dailyForecast = 'daily-forecast',\n    hourlyForecast = 'hourly-forecast',\n    uvIndex = 'uv-index',\n    tides = 'tides'\n};\n\nexport default FORECAST_SECTION;"
  },
  {
    "path": "client/src/enums/forecast/trend.ts",
    "content": "const enum TREND {\n    temperature = 'temperature',\n    rainfall = 'rainfallprobability',\n    wind = 'wind'\n};\n\nexport default TREND;"
  },
  {
    "path": "client/src/enums/forecast/unit-of-measure.ts",
    "content": "const enum UNIT_OF_MEASURE {\n    // Distance\n    millimeters = 'mm',\n    centimetres = 'cm',\n    metres = 'm',\n    kilometres = 'km',\n    miles = 'mi',\n    inches = 'in',\n    \n    // Speed\n    millimetresPerHour = 'mm/h',\n    kilometresPerHour = 'km/h',\n    metresPerSecond = 'm/s',\n    inchesPerHour = 'mi/h',\n    milesPerHour = 'mi/h',\n    \n    // Temperature\n    celcius = '°C',\n    fahrenheit = '°F',\n    \n    // Pressure\n    hectopascals = 'hPa',\n    millibars = 'bar',\n\n    // General\n    percentage = '%'\n};\n\nexport default UNIT_OF_MEASURE;"
  },
  {
    "path": "client/src/enums/forecast/units.ts",
    "content": "const enum UNITS {\n    metric = 'metric',\n    imperial = 'imperial'\n};\n\nexport default UNITS;"
  },
  {
    "path": "client/src/enums/maps/map.ts",
    "content": "export const enum MAP {\n    radar = 'radar',\n    precipitation = 'precipitation',\n    temperature = 'temperature',\n    cloud = 'cloud',\n    wind = 'wind',\n    pressure = 'pressure',\n};\n\nexport default MAP;"
  },
  {
    "path": "client/src/helpers/get-direction.ts",
    "content": "import DIRECTIONS from '../constants/forecast/directions';\n\nconst divisor = 360 / DIRECTIONS.length;\n\nexport default function getDirection(bearing: number): string {\n    return DIRECTIONS[Math.floor(bearing / divisor)] || 'Unknown';\n}"
  },
  {
    "path": "client/src/helpers/get-figure.ts",
    "content": "import FIGURE from '../constants/forecast/figure';\n\nimport {\n    phase\n} from '../store';\n\nexport default function getFigure(conditionId: number): string {\n    const figurePhase = FIGURE[phase.value] || FIGURE.day;\n\n    return figurePhase[conditionId] || figurePhase[Math.floor(conditionId / 100) * 100] || figurePhase['800'];\n}"
  },
  {
    "path": "client/src/helpers/get-icon.ts",
    "content": "import ICON from '../constants/forecast/icon';\nimport PHASE from '../enums/forecast/phase';\n\nimport getPhase from './get-phase';\n\nexport default function getIcon(conditionId: number, timestamp?: number): string {\n    let phase = PHASE.day;\n\n    if (timestamp) {\n        phase = getPhase(timestamp);\n    }\n    \n    const phaseIcon = ICON[phase];\n\n    return phaseIcon[conditionId] || phaseIcon[Math.floor(conditionId / 100) * 100] || phaseIcon['800'];\n}"
  },
  {
    "path": "client/src/helpers/get-phase.ts",
    "content": "import PHASE from '../enums/forecast/phase';\n\nimport {\n    state\n} from '../store';\n\nexport default function getPhase(timestamp: number): PHASE {\n    if (!state.forecast) {\n        return PHASE.day;\n    }\n\n    const isDay = state.forecast.daily.some(({ sunrise, sunset }) => {\n        return timestamp > sunrise && timestamp < sunset;\n    });\n\n    return isDay ? PHASE.day : PHASE.night;\n}"
  },
  {
    "path": "client/src/helpers/set-theme-meta.ts",
    "content": "import {\n    domSetMeta\n} from '@ocula/utilities';\n\nexport default function setThemeMeta(colour: string): void {\n    domSetMeta('theme-color', colour);\n}"
  },
  {
    "path": "client/src/index.ejs",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <title>Ocula</title>\n    <meta charset=\"UTF-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <meta name=\"Description\" content=\"Ocula - The free and open-source progressive weather app\">\n    <link href=\"https://fonts.googleapis.com/css?family=DM+Sans:400,500,700&display=swap\" rel=\"preconnect\">\n\n    <% if (process.env.NODE_ENV === 'production') { %>\n    <meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self'; font-src fonts.googleapis.com; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' *.googletagmanager.com *.google-analytics.com; img-src 'self' data: blob: *.mapbox.com *.google-analytics.com; worker-src 'self' blob:; child-src 'self' blob:; connect-src 'self' sentry.io *.google-analytics.com *.mapbox.com tilecache.rainviewer.com tile.openweathermap.org\">\n    <script src=\"https://www.googletagmanager.com/gtag/js?id=<%= process.env.GA_TRACKING_ID %>\" async></script>\n    <script>\n        window.dataLayer = window.dataLayer || [];\n\n        window.gtag = function () {\n            dataLayer.push(arguments);\n        }\n\n        gtag('js', new Date());\n        gtag('config', '<%= process.env.GA_TRACKING_ID %>');\n    </script>\n    <% } %>\n\n    <style>\n\n        html,\n        body {\n            width: 100%;\n            height: 100%;\n            margin: 0;\n            padding: 0;\n            overflow: auto;\n        }\n\n        html {\n            font-size: 16px;\n            box-sizing: border-box;\n            -moz-box-sizing: border-box;\n        }\n\n        *,\n        *:before,\n        *:after {\n            box-sizing: inherit;\n        }\n\n        body {\n            font-family: sans-serif;\n            font-size: 16px;\n            font-weight: 400;\n            line-height: 1.5;\n            color: #353539;\n            background-color: #FFFFFF;\n        }\n\n        .pre-app {\n            display: flex;\n            width: 100%;\n            height: 100%;\n            align-items: center;\n            justify-content: center;\n        }\n        \n        .pre-app__body {\n            width: auto;\n            text-align: center;\n        }\n\n        .pre-app__logo {\n            display: block;\n        }\n\n        .pre-app__title-block {\n            margin-top: 2rem;\n        }\n\n        .pre-app__title {\n            font-weight: bold;\n        }\n\n    </style>\n</head>\n\n<body>\n    <noscript>Javascript must be enabled to run this app</noscript>\n    <div class=\"pre-app\">\n        <div class=\"pre-app__body\">\n            <svg class=\"pre-app__logo\" width=\"192\" height=\"192\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n                <circle cx=\"97.5\" cy=\"97.5\" r=\"78.75\" fill=\"url(#paint0_linear)\" />\n                <path opacity=\".75\"\n                    d=\"M95.344 140.359c-25.647 0-48.74-24.584-48.74-49.41 0-24.825 28.466-43.465 54.113-43.465 25.646 0 38.762 18.64 38.762 43.466 0 24.825-18.488 49.409-44.135 49.409z\"\n                    fill=\"#fff\" />\n                <path opacity=\".5\"\n                    d=\"M57.507 104.35C52.608 79.938 74.09 60.546 98.502 55.647c24.412-4.898 46.458 11.08 51.357 35.492 4.898 24.412-10.178 46.05-34.59 50.949-24.412 4.898-52.864-13.326-57.762-37.738z\"\n                    fill=\"#fff\" />\n                <g opacity=\".5\" filter=\"url(#filter0_d)\">\n                    <path\n                        d=\"M98.215 54.872c24.686 2.444 44.495 29.089 42.051 53.775-2.444 24.686-31.678 40.508-56.364 38.064-24.686-2.444-35.475-22.228-33.032-46.914 2.444-24.686 22.66-47.369 47.345-44.925z\"\n                        fill=\"#fff\" />\n                </g>\n                <defs>\n                    <linearGradient id=\"paint0_linear\" x1=\"97.5\" y1=\"18.75\" x2=\"97.5\" y2=\"176.25\"\n                        gradientUnits=\"userSpaceOnUse\">\n                        <stop stop-color=\"#FDC830\" />\n                        <stop offset=\"1\" stop-color=\"#F37335\" />\n                    </linearGradient>\n                    <filter id=\"filter0_d\" x=\"42.592\" y=\"46.666\" width=\"106.538\" height=\"109.201\"\n                        filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n                        <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\" />\n                        <feColorMatrix in=\"SourceAlpha\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" />\n                        <feOffset />\n                        <feGaussianBlur stdDeviation=\"2\" />\n                        <feColorMatrix values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0\" />\n                        <feBlend in2=\"BackgroundImageFix\" result=\"effect1_dropShadow\" />\n                        <feBlend in=\"SourceGraphic\" in2=\"effect1_dropShadow\" result=\"shape\" />\n                    </filter>\n                </defs>\n            </svg>\n            <div class=\"pre-app__title-block\">\n                <div class=\"pre-app__title\">Ocula</div>\n                <div class=\"pre-app__subtitle\">Loading. Please wait.</div>\n            </div>\n        </div>\n    </div>\n</body>\n\n</html>"
  },
  {
    "path": "client/src/index.ts",
    "content": "import start from './startup';\n\nexport default start();"
  },
  {
    "path": "client/src/routes/error/index.ts",
    "content": "import ROUTES from '../../constants/core/routes';\n\nimport Index from './index.vue';\nimport NotFound from './not-found.vue';\n\nimport type {\n    RouteRecordRaw\n} from '@ocula/router';\n\nexport default [\n    {\n        path: '',\n        name: ROUTES.error.index,\n        component: Index,\n    },\n    {\n        path: 'not-found',\n        name: ROUTES.error.notFound,\n        component: NotFound,\n    }\n] as RouteRecordRaw[];"
  },
  {
    "path": "client/src/routes/error/index.vue",
    "content": "<template>\n    <div class=\"route error-index\">\n        <div class=\"text--centre\">\n            <h1>Error</h1>\n            <h3>An error has occurred</h3>\n        </div>\n    </div>\n</template>"
  },
  {
    "path": "client/src/routes/error/not-found.vue",
    "content": "<template>\n    <div class=\"route error-not-found\">\n        <div class=\"text--centre\">\n            <h1>404</h1>\n            <h3>Not Found</h3>\n        </div>\n    </div>\n</template>"
  },
  {
    "path": "client/src/routes/error.vue",
    "content": "<template>\n    <div class=\"route error-master\">\n        <router-view />\n    </div>\n</template>"
  },
  {
    "path": "client/src/routes/forecast/index.ts",
    "content": "import ROUTES from '../../constants/core/routes';\n\nimport type {\n    RouteRecordRaw\n} from '@ocula/router';\n\nimport {\n    defineAsyncComponent\n} from 'vue';\n\nexport default [\n    {\n        path: '',\n        name: ROUTES.forecast.index,\n        component: defineAsyncComponent(() => import(/* webpackChunkName: 'forecast' */ './index.vue'))\n    }\n] as RouteRecordRaw[];"
  },
  {
    "path": "client/src/routes/forecast/index.vue",
    "content": "<template>\n    <div class=\"route forecast-index transition-theme-change\" layout=\"column top-stretch\" :class=\"theme.weather.class\">\n        <container class=\"forecast-index__header\">\n            <weather-actions></weather-actions>\n            <header class=\"forecast-index__summary\" layout=\"column center-stretch\" v-if=\"forecast\">\n                <forecast-summary></forecast-summary>\n            </header>\n        </container>\n        <div class=\"forecast-index__body\" self=\"size-x1\" v-if=\"forecast\">\n            <container class=\"forecast-index__container\">\n                <block class=\"forecast-index__block\"\n                    v-for=\"section in sections\"\n                    :key=\"section.id\"\n                    :class=\"section.class\"\n                    :title=\"section.label\">\n                    <component :is=\"section.component\"/>\n                </block>\n            </container>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport FORECAST_SECTIONS from '../../constants/forecast/sections';\n\nimport WeatherActions from '../../components/weather/actions.vue';\nimport ForecastSummary from '../../components/forecast/summary.vue';\nimport ForecastTides from '../../components/forecast/tides.vue';\n\nimport setThemeMeta from '../../helpers/set-theme-meta';\n\nimport {\n    defineComponent,\n    watch,\n    computed\n} from 'vue';\n\nimport {\n    theme,\n    state,\n    forecast\n} from '../../store';\n\nexport default defineComponent({\n\n    components: {\n        WeatherActions,\n        ForecastSummary,\n        ForecastTides\n    },\n    \n    setup() {\n        const sections = computed(() => {\n            const visibleSections = state.settings.forecast.sections.filter(({ type,  visible }) => {\n                const {\n                    condition\n                } = FORECAST_SECTIONS[type]; \n\n                return !!visible && (!condition || condition(forecast.value))\n            });\n\n            return visibleSections.map(({ type }) => ({\n                id: type,\n                class: `forecast-index__block--${type}`,\n                ...FORECAST_SECTIONS[type]\n            }));\n        });\n\n        watch(() => theme.value.weather, ({ colour }) => setThemeMeta(colour));\n\n        return {\n            theme,\n            forecast,\n            sections\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n    @import \"~@ocula/style/src/_mixins.scss\";\n\n    .forecast-index {\n        color: var(--font__colour--weather);\n        background: var(--background__colour--weather);\n    }\n\n    .forecast-index__summary {\n        min-height: 30vh;\n        padding: var(--spacing__large);\n        padding-top: 0;\n    }\n\n    .forecast-index__body {\n        padding-top: var(--spacing__small);\n        color: var(--font__colour);\n        background: var(--background__colour);\n        border-top-left-radius: var(--border__radius--large);\n        border-top-right-radius: var(--border__radius--large);\n    }\n\n    .forecast-index__block {\n        \n        &:not(:last-of-type) {\n            margin-bottom: var(--spacing__small);\n        }\n\n        & .block__header,\n        & .block__body {\n            padding-left: var(--spacing__large);\n            padding-right: var(--spacing__large);\n        }\n    }\n\n    .forecast-index__block--hourly-forecast,\n    .forecast-index__block--tides {\n\n        & .block__body {\n            padding-left: 0;\n            padding-right: 0;\n        }\n    }\n\n    @include breakpoint(\"lg\") {\n\n        .forecast-index__body {\n            border-radius: 0;\n        }\n\n    }\n\n</style>"
  },
  {
    "path": "client/src/routes/forecast.vue",
    "content": "<template>\n    <weather-layout class=\"route forecast-master\">\n        <router-view />\n    </weather-layout>    \n</template>\n\n<script lang=\"ts\">\nimport WeatherLayout from '../components/layouts/weather.vue';\n\nimport {\n    defineComponent\n} from 'vue';\n\nexport default defineComponent({\n\n    components: {\n        WeatherLayout\n    }\n\n});\n</script>"
  },
  {
    "path": "client/src/routes/index.ts",
    "content": "import Forecast from './forecast.vue';\nimport Maps from './maps.vue';\nimport Settings from './settings.vue';\nimport Error from './error.vue';\n\nimport forecast from './forecast';\nimport maps from './maps';\nimport settings from './settings';\nimport error from './error';\n\nimport type {\n    RouteRecordRaw\n} from '@ocula/router';\n\nexport default [\n    {\n        path: '/forecast',\n        alias: '/',\n        component: Forecast,\n        children: forecast\n    },\n    {\n        path: '/maps',\n        component: Maps,\n        children: maps\n    },\n    {\n        path: '/settings',\n        component: Settings,\n        children: settings\n    },\n    {\n        path: '/error',\n        component: Error,\n        children: error\n    },\n    {\n        path: '/:catchAll(.*)',\n        redirect: '/error/not-found'\n    }\n] as RouteRecordRaw[];"
  },
  {
    "path": "client/src/routes/maps/index.ts",
    "content": "import ROUTES from '../../constants/core/routes';\n\nimport {\n    defineAsyncComponent\n} from 'vue';\n\nimport type {\n    RouteRecordRaw\n} from '@ocula/router';\n\nexport default [\n    {\n        path: ':type?',\n        name: ROUTES.maps.index,\n        props: true,\n        component: defineAsyncComponent(() => import(/* webpackChunkName: 'maps' */ './index.vue'))\n    }\n] as RouteRecordRaw[];"
  },
  {
    "path": "client/src/routes/maps/index.vue",
    "content": "<template>\n    <div class=\"route maps-index\" layout=\"column top-stretch\">\n        <div>\n            <container>\n                <weather-actions></weather-actions>\n            </container>\n            <div class=\"maps-index__options\">\n                <container class=\"maps-index__options-container\" layout=\"row center-justify\">\n                    <icon-button class=\"maps-index__drawer-button\" self=\"size-x1\" :icon=\"map.icon\" @click.native=\"changeMap\">\n                        <div layout=\"row center-justify\">\n                            <div>{{ map.label }}</div>\n                            <loader v-if=\"status.loading\"/>\n                        </div>\n                    </icon-button>\n                    <icon-button icon=\"focus-3-line\" v-tooltip:left=\"'Recentre'\" @click.native=\"recentre\"></icon-button>\n                </container>\n            </div>\n        </div>\n        <div class=\"maps-index__body\" layout=\"column justify-stretch\" self=\"size-x1\">\n            <div self=\"size-x1\">\n                <maps-drawer class=\"maps-index__drawer\"/>\n                <mapbox-map class=\"maps-index__map\"\n                    ref=\"mapboxMap\"\n                    v-if=\"forecast\"\n                    :key=\"theme.core.mapStyle\"\n                    :latitude=\"forecast.lat.raw\"\n                    :longitude=\"forecast.lon.raw\"\n                    :zoom=\"settings.maps.zoom\"\n                    :pitch=\"settings.maps.pitch\"\n                    :style=\"theme.core.mapStyle\"\n                    @movestart=\"onMoveStart\"\n                    @moveend=\"onMoveEnd\"\n                    @idle=\"onIdle\"\n                    @sourcedataloading=\"onSourceDataLoading\">\n                    <mapbox-legend v-if=\"map.legend\">\n                        <div class=\"maps-index__legend\">\n                            <template v-for=\"key in map.legend\" :key=\"key.colour\">\n                                <div class=\"maps-index__legend-colour\" :style=\"{ background: key.colour }\"></div>\n                                <div class=\"maps-index__legend-label\">{{ key.label }}</div>\n                            </template>\n                        </div>\n                    </mapbox-legend>\n                    <mapbox-raster-layer v-for=\"layer in layers\"\n                        class=\"maps-index__map-layer\"\n                        :key=\"layer.id\"\n                        :id=\"layer.id\"\n                        :tiles=\"[layer.url]\"\n                        :layout=\"layer.layout\"\n                        :paint=\"layer.paint\"/>\n                </mapbox-map>\n            </div>\n            <div class=\"maps-index__controls\" v-if=\"canPlay\">\n                <container class=\"maps-index__controls-container\" layout=\"row center-justify\">\n                    <icon-button class=\"maps-index__control-loop\" :icon=\"controlIcon\" @click.native=\"togglePlaying\"></icon-button>\n                    <div class=\"padding__horizontal--small\" self=\"size-x1\">\n                        <input class=\"maps-index__control-slider\"\n                            v-model.number=\"layerIndex\"\n                            type=\"range\"\n                            min=\"0\"\n                            step=\"1\"\n                            :max=\"layers.length - 1\"\n                            :disabled=\"status.loading\">\n                    </div>\n                    <div class=\"maps-index__control-label\">{{ layer.label }}</div>\n                </container>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport MAP from '../../enums/maps/map';\nimport MAPS from '../../constants/maps/maps';\n\nimport WeatherActions from '../../components/weather/actions.vue';\nimport MapsDrawer from '../../components/drawers/maps.vue';\n\nimport applicationController from '../../controllers/application';\n\nimport {\n    defineComponent,\n    ref,\n    computed,\n    PropType,\n    watch,\n    reactive\n} from 'vue';\n\nimport {\n    state,\n    theme,\n    forecast,\n    format\n} from '../../store';\n\nimport {\n    typeIsFunction,\n    numberClamp\n} from '@ocula/utilities';\n\nexport default defineComponent({\n\n    components: {\n        WeatherActions,\n        MapsDrawer\n    },\n\n    props: {\n\n        type: {\n            type: String as PropType<MAP>\n        }\n\n    },\n    \n    setup(props) {\n        const mapboxMap = ref(null);\n\n        let moveHandle = null;\n        let intervalHandle = null;\n        let resumePlaying = false;\n\n        const status = reactive({\n            loading: false,\n            playing: false,\n        });\n\n        let layerIndex = ref(0);\n\n        const settings = computed(() => state.settings);\n\n        const map = computed(() => {\n            let {\n                layers,\n                ...other\n            } = MAPS[props.type || state.settings.maps.default];\n\n            if (typeIsFunction(layers) && forecast.value) {\n                layers = layers(forecast.value, format.value);\n            }\n\n            clearMoveHandle();\n            stopPlaying();\n            layerIndex.value = layers.length - 1;\n\n            return {\n                ...other,\n                layers\n            };\n        });\n\n        const layers = computed(() => {\n            let {\n                layers\n            } = map.value;\n\n            return layers.map((layer, index) => {\n                let visibility = index === layerIndex.value ? 'visible' : 'none';\n                let opacity = index === layerIndex.value ? 0.75 : 0;\n\n                if (status.playing) {\n                    visibility = 'visible';\n                }\n\n                return {\n                    ...layer,\n                    layout: {\n                        visibility\n                    },\n                    paint: {\n                        'raster-opacity': opacity\n                    }\n                };\n            });\n        });\n\n        const layer = computed(() => map.value.layers[layerIndex.value]);\n        const controlIcon = computed(() => status.playing ? 'stop-fill' : 'play-fill');\n        const canPlay = computed(() => layers.value.length > 1);\n\n        function recentre() {\n            mapboxMap.value.updateLocation();\n        }\n\n        function onIdle() {\n            status.loading = false;\n        }\n\n        function onSourceDataLoading() {\n            status.loading = true;\n        }\n\n        function clearMoveHandle() {\n            if (moveHandle) {\n                window.clearTimeout(moveHandle);\n                moveHandle = null;\n            }\n        }\n\n        function runLoop() {\n            intervalHandle = window.setInterval(() => {\n                layerIndex.value = layerIndex.value === layers.value.length - 1 ? 0 : layerIndex.value + 1;\n            }, state.settings.maps.framerate);\n        }\n\n        function startPlaying() {\n            if (!mapboxMap.value) {\n                return;\n            }\n\n            status.playing = true;\n\n            mapboxMap.value.once('idle', runLoop);\n        }\n\n        function stopPlaying() {\n            if (!mapboxMap.value) {\n                return;\n            }\n\n            status.playing = false;\n\n            if (intervalHandle) {\n                window.clearInterval(intervalHandle);\n                intervalHandle = null;\n            }\n\n            mapboxMap.value.off('idle', runLoop);\n        }\n\n        function togglePlaying() {\n            if (status.playing) {\n                return stopPlaying();\n            }\n\n            startPlaying();\n        }\n\n        function onMoveStart(event, map) {\n            resumePlaying = status.playing;\n\n            clearMoveHandle();\n            stopPlaying();\n        }\n\n        function onMoveEnd(event, map) {\n            if (resumePlaying && canPlay.value) {\n                moveHandle = window.setTimeout(startPlaying, 1000);\n            }\n        }\n\n        return {\n            theme,\n            forecast,\n            settings,\n            map,\n            status,\n            layers,\n            layer,\n            layerIndex,\n            mapboxMap,\n            recentre,\n            controlIcon,\n            onIdle,\n            onSourceDataLoading,\n            onMoveStart,\n            onMoveEnd,\n            canPlay,\n            togglePlaying,\n            changeMap: applicationController.setMapType\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .maps-index__options {\n        border-top: 1px solid var(--border__colour);\n        border-bottom: 1px solid var(--border__colour);\n    }\n\n    .maps-index__options-container,\n    .maps-index__controls-container {\n        padding: var(--spacing__x-small) var(--spacing__medium);\n    }\n\n    .maps-index__body {\n        position: relative;\n    }\n\n    .maps-index__legend {\n        display: grid;\n        grid-template-columns: auto max-content;\n        gap: var(--spacing__xx-small) var(--spacing__x-small);\n        align-items: center;\n    }\n\n    .maps-index__legend-colour {\n        width: 1em;\n        height: 1em;\n        border-radius: 2px;\n    }\n\n    .maps-index__drawer {\n        position: absolute;\n        z-index: 1000;\n    }\n\n    .maps-index__controls {\n        border-top: 1px solid var(--border__colour);\n    }\n\n    .maps-index__control-slider {\n        display: block;\n        width: 100%;\n    }\n\n</style>"
  },
  {
    "path": "client/src/routes/maps.vue",
    "content": "<template>\n    <weather-layout class=\"route maps-master\">\n        <router-view />\n    </weather-layout>    \n</template>\n\n<script lang=\"ts\">\nimport WeatherLayout from '../components/layouts/weather.vue';\n\nimport {\n    defineComponent\n} from 'vue';\n\nexport default defineComponent({\n\n    components: {\n        WeatherLayout\n    }\n\n});\n</script>"
  },
  {
    "path": "client/src/routes/settings/forecast/index.ts",
    "content": "import ROUTES from '../../../constants/core/routes';\n\nimport Locations from './locations.vue';\nimport Sections from './sections.vue';\n\nimport type {\n    RouteRecordRaw\n} from '@ocula/router';\n\nexport default [\n    {\n        path: 'forecast/locations',\n        name: ROUTES.settings.forecast.locations,\n        component: Locations\n    },\n    {\n        path: 'forecast/sections',\n        name: ROUTES.settings.forecast.sections,\n        component: Sections\n    }\n] as RouteRecordRaw[];"
  },
  {
    "path": "client/src/routes/settings/forecast/locations.vue",
    "content": "<template>\n    <settings-layout class=\"route settings-locations\" title=\"Locations\" :back-route=\"backRoute\">\n        <div class=\"settings-locations__locations menu\">\n            <div class=\"menu-item\" layout=\"row center-justify\" v-for=\"location in locations\" :key=\"location.id\">\n                <div class=\"text--truncate\" self=\"size-x1\">{{ location.longName }}</div>\n                <div v-tooltip:left=\"'Remove Location'\" @click.stop=\"removeLocation(location)\">\n                    <icon name=\"delete-bin-line\" class=\"margin__left--small\"/>\n                </div>\n            </div>\n        </div>\n    </settings-layout>\n</template>\n\n<script lang=\"ts\">\nimport ROUTES from '../../../constants/core/routes';\n\nimport SettingsLayout from '../../../components/layouts/settings.vue';\n\nimport {\n    defineComponent,\n    computed\n} from 'vue';\n\nimport {\n    state,\n    removeLocation\n} from '../../../store';\n\nexport default defineComponent({\n    \n    components: {\n        SettingsLayout\n    },\n\n    setup() {\n        const backRoute = {\n            name: ROUTES.settings.index\n        };\n\n        const locations = computed(() => state.settings.locations);\n\n        return {\n            backRoute,\n            locations,\n            removeLocation\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .settings-locations__locations {\n        padding: 0 var(--spacing__small);\n    }\n\n</style>"
  },
  {
    "path": "client/src/routes/settings/forecast/sections.vue",
    "content": "<template>\n    <settings-layout class=\"route settings-sections\" title=\"Sections\" :back-route=\"backRoute\">\n        <transition-group tag=\"div\" name=\"sections\" class=\"settings-sections__sections menu\">\n            <div class=\"settings-sections__section menu-item\" layout=\"row center-justify\" v-for=\"(section, index) in sections\" :key=\"section.type\">\n                <div class=\"text--truncate margin__right--x-small\" self=\"size-x1\">{{ section.label }}</div>\n                <icon-button icon=\"arrow-up-line\" @click.native.stop=\"moveSection(section.type, -1)\" v-visible=\"index > 0\"></icon-button>\n                <icon-button icon=\"arrow-down-line\" @click.native.stop=\"moveSection(section.type, 1)\" v-visible=\"index < sections.length - 1\"></icon-button>\n                <icon-button :icon=\"getVisibilityIcon(section)\" @click.native.stop=\"setSectionVisibility(section.type, !section.visible)\"></icon-button>\n            </div>\n        </transition-group>\n    </settings-layout>\n</template>\n\n<script lang=\"ts\">\nimport ROUTES from '../../../constants/core/routes';\nimport FORECAST_SECTIONS from '../../../constants/forecast/sections';\n\nimport SettingsLayout from '../../../components/layouts/settings.vue';\n\nimport {\n    defineComponent,\n    computed\n} from 'vue';\n\nimport {\n    state,\n    moveSection,\n    setSectionVisibility\n} from '../../../store';\n\nexport default defineComponent({\n    \n    components: {\n        SettingsLayout\n    },\n\n    setup() {\n        const backRoute = {\n            name: ROUTES.settings.index\n        };\n\n        const sections = computed(() => {\n            return state.settings.forecast.sections.map(section => {\n                const {\n                    label\n                } = FORECAST_SECTIONS[section.type];\n\n                return {\n                    ...section,\n                    label\n                };\n            })\n        });\n\n        function getVisibilityIcon(section): string {\n            return section.visible ? 'eye-line' : 'eye-off-line';\n        }\n\n        return {\n            backRoute,\n            sections,\n            moveSection,\n            getVisibilityIcon,\n            setSectionVisibility\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .settings-sections__sections {\n        padding: 0 var(--spacing__small);\n    }\n\n    .settings-sections__section {\n        padding: var(--spacing__x-small) var(--spacing__small);\n        cursor: default;\n    }\n\n    .sections-move {\n        transition: transform var(--transition__timing) var(--transition__easing--default);\n    }\n\n</style>"
  },
  {
    "path": "client/src/routes/settings/general/about.vue",
    "content": "<template>\n    <settings-layout class=\"route settings-about\" title=\"About\" :back-route=\"backRoute\">\n        <div class=\"settings-about__body\">\n            <div class=\"settings-about__header\">\n                <img class=\"settings-about__logo\" :src=\"logo\" alt=\"Ocula\">\n                <h1 class=\"settings-about__title\">{{ manifest.title }}</h1>\n                <div class=\"settings-about__subtitle\">{{ manifest.description }}</div>\n            </div>\n            <block class=\"settings-about__block\" title=\"Details\">\n                <div class=\"settings-about__details-grid\">\n                    <strong>Version</strong>\n                    <div>{{ manifest.version }}</div>\n                    <strong>Author</strong>\n                    <div>{{ manifest.author }}</div>\n                    <strong>Licence</strong>\n                    <div>{{ manifest.license }}</div>\n                    <strong>Source</strong>\n                    <a :href=\"manifest.repository\" target=\"_blank\">Github</a>\n                </div>\n            </block>\n            <block class=\"settings-about__block\" title=\"Attribution\">\n                <div class=\"settings-about__details-grid\">\n                    <strong>Forecast Data</strong>\n                    <a href=\"https://openweathermap.org\" target=\"_blank\">Open Weather Maps</a>\n                    <strong>Tide Data</strong>\n                    <a href=\"https://www.worldtides.info\" target=\"_blank\">World Tides</a>\n                    <strong>Maps/Geocoding</strong>\n                    <a href=\"https://www.mapbox.com\" target=\"_blank\">Mapbox</a>\n                    <strong>Radar Imagery</strong>\n                    <a href=\"https://www.rainviewer.com\" target=\"_blank\">RainViewer</a>\n                    <strong>Logo Design</strong>\n                    <a href=\"https://github.com/ethanroxburgh\" target=\"_blank\">Ethan Roxburgh</a>\n                    <strong>Icons</strong>\n                    <a href=\"https://remixicon.com\" target=\"_blank\">Remix Icons</a>\n                </div>\n            </block>\n            <block class=\"settings-about__block\" title=\"Sponsors\">\n                <div>Kerry Tarrant</div>\n            </block>\n            <div class=\"margin__top--large text--centre\">\n                <small class=\"text--meta\">Psalm 147:8</small>\n            </div>\n        </div>\n    </settings-layout>   \n</template>\n\n<script lang=\"ts\">\nimport ROUTES from '../../../constants/core/routes';\n\nimport SettingsLayout from '../../../components/layouts/settings.vue';\n\nimport manifest from '../../../../package.json';\nimport logo from '../../../assets/images/logo/logo-192.svg';\n\nimport {\n    defineComponent\n} from 'vue';\n\nexport default defineComponent({\n\n    components: {\n        SettingsLayout\n    },\n\n    setup() {\n        const backRoute = {\n            name: ROUTES.settings.index\n        };\n\n        return {\n            backRoute,\n            logo,\n            manifest\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .settings-about__body {\n        padding: 0 var(--spacing__small);\n    }\n\n    .settings-about__header {\n        text-align: center;\n        margin-bottom: var(--spacing__large);\n    }\n\n    .settings-about__title {\n        text-transform: uppercase;\n        letter-spacing: 0.2em;\n    }\n\n    .settings-about__logo {\n        width: 96px;\n    }\n\n    .settings-about__block {\n        \n        &:not(:last-of-type) {\n            margin-bottom: var(--spacing__small);\n        }\n\n        & .block__title {\n            color: var(--font__colour--meta);\n        }\n    }\n\n    .settings-about__details-grid {\n        display: grid;\n        grid-template-columns: auto 1fr;\n        grid-gap: var(--spacing__x-small) var(--spacing__small);\n    }\n\n</style>"
  },
  {
    "path": "client/src/routes/settings/general/index.ts",
    "content": "import ROUTES from '../../../constants/core/routes';\n\nimport Theme from './theme.vue';\nimport About from './about.vue';\n\nimport type {\n    RouteRecordRaw\n} from '@ocula/router';\n\nexport default [\n    {\n        path: 'general/theme',\n        name: ROUTES.settings.general.theme,\n        component: Theme\n    },\n    {\n        path: 'general/about',\n        name: ROUTES.settings.general.about,\n        component: About\n    }\n] as RouteRecordRaw[];"
  },
  {
    "path": "client/src/routes/settings/general/theme.vue",
    "content": "<template>\n    <settings-layout class=\"route settings-themes\" title=\"Themes\" :back-route=\"backRoute\">\n        <div class=\"settings-themes__themes\" grid=\"2 md-4 lg-4\">\n            <div class=\"settings-themes__theme\"\n                layout=\"row center-center\"\n                v-for=\"(value, key) in themes\"\n                :key=\"key\"\n                :class=\"value.class\"\n                @click=\"setTheme(value.id)\">\n                <div>{{ value.name }}</div>\n                <div class=\"settings-themes__theme-check\" v-show=\"isCurrentTheme(value.id)\">\n                    <small class=\"text--x-small\">\n                        <icon name=\"check-line\"/>\n                    </small>\n                </div>\n            </div>\n        </div>\n    </settings-layout>\n</template>\n\n<script lang=\"ts\">\nimport ROUTES from '../../../constants/core/routes';\n\nimport SettingsLayout from '../../../components/layouts/settings.vue';\n\nimport {\n    defineComponent,\n    computed\n} from 'vue';\n\nimport {\n    state,\n    theme,\n    updateSettings\n} from '../../../store';\n\nimport {\n    core as themes\n} from '../../../themes';\n\nexport default defineComponent({\n\n    components: {\n        SettingsLayout\n    },\n    \n    setup() {\n        const backRoute = {\n            name: ROUTES.settings.index\n        };\n\n        const theme = computed(() => state.settings.theme);\n\n        function isCurrentTheme(id: string) {\n            return id === theme.value;\n        }\n\n        function setTheme(id: string): void {\n            updateSettings({\n                theme: id\n            });\n        }\n\n        return {\n            backRoute,\n            theme,\n            themes,\n            isCurrentTheme,\n            setTheme\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .settings-themes__themes {\n        padding: 0 var(--spacing__large);\n    }\n\n    .settings-themes__theme {\n        position: relative;\n        color: var(--font__colour);\n        background-color: var(--background__colour);\n        border: 1px solid var(--border__colour);\n        border-radius: var(--border__radius);\n        overflow: hidden;\n\n        &::after {\n            display: block;\n            content: '';\n            padding-bottom: 100%;\n        }\n    }\n\n    .settings-themes__theme-check {\n        position: absolute;\n        top: var(--spacing__x-small);\n        right: var(--spacing__x-small);\n        padding: var(--spacing__xx-small);\n        color: var(--font__colour--compliment);\n        background-color: var(--colour__primary);\n        border-radius: 50%;\n\n        & .icon {\n            display: block;\n        }\n    }\n\n</style>"
  },
  {
    "path": "client/src/routes/settings/index.ts",
    "content": "import ROUTES from '../../constants/core/routes';\n\nimport Index from './index.vue';\n\nimport forecast from './forecast';\nimport general from './general';\nimport maps from './maps';\n\nimport type {\n    RouteRecordRaw\n} from '@ocula/router';\n\nexport default [\n    {\n        path: '',\n        name: ROUTES.settings.index,\n        component: Index\n    },\n\n    ...forecast,\n    ...maps,\n    ...general\n    \n] as RouteRecordRaw[];"
  },
  {
    "path": "client/src/routes/settings/index.vue",
    "content": "<template>\n    <settings-layout class=\"route settings-index\">\n        <div class=\"settings-index__body\">\n            <block class=\"settings-index__block\" title=\"Forecast\">\n                <div class=\"menu\">\n                    <settings-item class=\"menu-item\" label=\"Units\" :value=\"unit.label\">\n                        <select name=\"units\" v-model=\"units\">\n                            <option v-for=\"(value, key) in unitOptions\" :key=\"key\" :value=\"key\">{{ value.label }}</option>\n                        </select>\n                    </settings-item>\n                    <router-link class=\"link--inherit\" :to=\"routes.forecast.locations\">\n                        <settings-item class=\"menu-item\" label=\"Locations\" :value=\"locationsLabel\"></settings-item>\n                    </router-link>\n                    <router-link class=\"link--inherit\" :to=\"routes.forecast.sections\">\n                        <settings-item class=\"menu-item\" label=\"Sections\"></settings-item>\n                    </router-link>\n                </div>\n            </block>\n            <block class=\"settings-index__block\" title=\"Maps\">\n                <div class=\"menu\">\n                    <settings-item class=\"menu-item\" label=\"Default Map\" :value=\"map.label\">\n                        <select name=\"default-map\" v-model=\"defaultMap\">\n                            <option v-for=\"(value, key) in mapOptions\" :key=\"key\" :value=\"key\">{{ value.label }}</option>\n                        </select>\n                    </settings-item>\n                    <router-link class=\"link--inherit\" :to=\"routes.maps.display\">\n                        <settings-item class=\"menu-item\" label=\"Display\"></settings-item>\n                    </router-link>\n                    <settings-item class=\"menu-item\" label=\"Framerate\" :value=\"framerate\">\n                        <select name=\"framerate\" v-model=\"framerate\">\n                            <option v-for=\"value in framerates\" :key=\"value\" :value=\"value\">{{ value }}ms</option>\n                        </select>\n                    </settings-item>\n                </div>\n            </block>\n            <block class=\"settings-index__block\" title=\"General\">\n                <div class=\"menu\">\n                    <router-link class=\"link--inherit\" :to=\"routes.general.theme\">\n                        <settings-item class=\"menu-item\" label=\"Theme\" :value=\"theme.core.name\"></settings-item>\n                    </router-link>\n                    <settings-item class=\"menu-item\" label=\"Update\" @click.native=\"updateApplication\">\n                        <template #value>\n                            <loader v-if=\"updating\"></loader>\n                        </template>\n                    </settings-item>\n                    <settings-item class=\"menu-item\" label=\"Reset\" @click.native=\"reset\"></settings-item>\n                    <router-link class=\"link--inherit\" :to=\"routes.general.about\">\n                        <settings-item class=\"menu-item\" label=\"About\"></settings-item>\n                    </router-link>\n                </div>\n            </block>\n        </div>\n    </settings-layout>\n</template>\n\n<script lang=\"ts\">\nimport UNITS from '../../constants/forecast/units';\nimport ROUTES from '../../constants/core/routes';\nimport MAPS from '../../constants/maps/maps';\n\nimport SettingsLayout from '../../components/layouts/settings.vue';\nimport SettingsItem from '../../components/settings/settings-item.vue';\n\nimport {\n    defineComponent,\n    ref,\n    computed\n} from 'vue';\n\nimport {\n    state,\n    theme,\n    updateSettings,\n    resetSettings,\n    update\n} from '../../store';\n\nimport {\n    core as themeOptions\n} from '../../themes';\n\nimport {\n    componentsController\n} from '@ocula/components';\n\nexport default defineComponent({\n\n    components: {\n        SettingsLayout,\n        SettingsItem\n    },\n    \n    setup() {\n        const updating = ref(false);\n\n        const routes = {\n            forecast: {\n                locations: {\n                    name: ROUTES.settings.forecast.locations\n                },\n                sections: {\n                    name: ROUTES.settings.forecast.sections\n                }\n            },\n            maps: {\n                display: {\n                    name: ROUTES.settings.maps.display\n                }\n            },\n            general: {\n                theme: {\n                    name: ROUTES.settings.general.theme\n                },\n                about: {\n                    name: ROUTES.settings.general.about\n                }\n            }\n        };\n\n        const units = computed({\n            get: () => state.settings.units,\n            set: units => {\n                updateSettings({ units });\n                update(true);\n            }\n        });\n\n        const defaultMap = computed({\n            get: () => state.settings.maps.default,\n            set: value => updateSettings({\n                maps: {\n                    ...state.settings.maps, \n                    default: value\n                }\n            })\n        });\n\n        const framerate = computed({\n            get: () => state.settings.maps.framerate,\n            set: value => updateSettings({\n                maps: {\n                    ...state.settings.maps, \n                    framerate: value\n                }\n            })\n        });\n\n        const framerates = Array.from({ length: 6 }, (_, index) => ++index * 500);\n        \n        const unit = computed(() => UNITS[state.settings.units]);\n        const map = computed(() => MAPS[state.settings.maps.default]);\n\n        const locationsLabel = computed(() => `${state.settings.locations.length} saved`);\n\n        async function updateApplication() {\n            const registration = await navigator.serviceWorker.getRegistration();\n\n            if (!registration) {\n                return;\n            }\n\n            try {\n                updating.value = true;\n\n                await registration.update();\n            } finally {\n                updating.value = false;\n            }\n        }\n\n        async function reset() {\n            try {\n                await componentsController.confirm({\n                    message: 'This will reset all settings back to default. This cannot be undone. Do you wish to continue?',\n                    confirmLabel: 'Yes, Reset'\n                });\n\n                resetSettings();\n            } catch {\n                // do nothing\n            }\n        }\n\n        return {\n            routes,\n            units,\n            unit,\n            defaultMap,\n            framerate,\n            framerates,\n            map,\n            locationsLabel,\n            theme,\n            themeOptions,\n            mapOptions: MAPS,\n            unitOptions: UNITS,\n            updateApplication,\n            updating,\n            reset\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .settings-index__header {\n        padding: var(--spacing__large);\n    }\n\n    .settings-index__body {\n        padding: 0 var(--spacing__small);\n    }\n\n    .settings-index__block {\n\n        &:not(:last-of-type) {\n            margin-bottom: var(--spacing__small);\n        }\n\n        & .block__title {\n            color: var(--font__colour--meta);\n        }\n\n        & .block__body {\n            padding: var(--spacing__small) 0;\n        }\n    }\n\n</style>"
  },
  {
    "path": "client/src/routes/settings/maps/display.vue",
    "content": "<template>\n    <settings-layout class=\"route settings-maps-display\" title=\"Display\" :back-route=\"backRoute\">\n        <div class=\"settings-maps-display__options\">\n            <block title=\"Zoom\">\n                <template #secondary>{{ zoom }}</template>\n                <input class=\"settings-maps-display__slider\" type=\"range\" min=\"1\" max=\"12\" step=\"1\" v-model.number=\"zoom\">\n            </block>\n            <block class=\"margin__top--large\" title=\"Pitch\">\n                <template #secondary>{{ pitch }} &deg;</template>\n                <input class=\"settings-maps-display__slider\" type=\"range\" min=\"0\" max=\"85\" step=\"5\" v-model.number=\"pitch\">\n            </block>\n            <block class=\"margin__top--large\" title=\"Preview\">\n                <mapbox-map class=\"settings-maps-display__map\"\n                    :latitude=\"location.latitude\"\n                    :longitude=\"location.longitude\"\n                    :style=\"theme.core.mapStyle\"\n                    :zoom=\"zoom\"\n                    :pitch=\"pitch\"\n                    :interactive=\"false\">\n                </mapbox-map>\n            </block>\n        </div>\n    </settings-layout>\n</template>\n\n<script lang=\"ts\">\nimport ROUTES from '../../../constants/core/routes';\n\nimport SettingsLayout from '../../../components/layouts/settings.vue';\n\nimport {\n    defineComponent,\n    ref,\n    computed\n} from 'vue';\n\nimport {\n    state,\n    theme,\n    updateSettings\n} from '../../../store';\n\nimport {\n    functionDebounce\n} from '@ocula/utilities';\n\nimport type {\n    IMapSettings\n} from '../../../types/storage';\n\nexport default defineComponent({\n    \n    components: {\n        SettingsLayout\n    },\n\n    setup() {\n        const backRoute = {\n            name: ROUTES.settings.index\n        };\n\n        // Sydney, Australia\n        const location = {\n            latitude: -33.8688,\n            longitude: 151.2093\n        }\n\n        function getMapsComputed(key: keyof IMapSettings) {\n            return {\n                get: () => state.settings.maps[key],\n                set: functionDebounce(value => {\n                    updateSettings({\n                        maps: {\n                            ...state.settings.maps,\n                            [key]: value\n                        }\n                    })\n                }, 100)\n            }\n        }\n\n        const zoom = computed(getMapsComputed('zoom'));\n        const pitch = computed(getMapsComputed('pitch'));\n\n        return {\n            backRoute,\n            location,\n            theme,\n            zoom,\n            pitch\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .settings-maps-display__options {\n        padding: 0 var(--spacing__large);\n    }\n\n    .settings-maps-display__slider {\n        display: block;\n        width: 100%;\n    }\n\n    .settings-maps-display__map {\n        width: 100%;\n        height: 33vh;\n        border: 1px solid var(--border__colour);\n        border-radius: var(--border__radius);\n        overflow: hidden;\n    }\n\n</style>"
  },
  {
    "path": "client/src/routes/settings/maps/index.ts",
    "content": "import ROUTES from '../../../constants/core/routes';\n\nimport Display from './display.vue';\n\nimport type {\n    RouteRecordRaw\n} from '@ocula/router';\n\nexport default [\n    {\n        path: 'maps/display',\n        name: ROUTES.settings.maps.display,\n        component: Display\n    }\n] as RouteRecordRaw[];"
  },
  {
    "path": "client/src/routes/settings.vue",
    "content": "<template>\n    <div class=\"route settings-master\">\n        <container class=\"settings-master__container\">\n            <router-view />\n        </container>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport {\n    defineComponent\n} from 'vue';\n\nexport default defineComponent({\n\n});\n</script>\n\n<style lang=\"scss\">\n\n</style>"
  },
  {
    "path": "client/src/services/location.ts",
    "content": "import {\n    ILocation\n} from '../types/location';\n\nexport async function searchLocations(query: string): Promise<ILocation[]> {\n    const response = await fetch(`/api/location/search?query=${query}`);\n\n    return response.json();\n}\n\nexport async function getLocation(latitude: number, longitude: number): Promise<ILocation> {  \n    const response = await fetch(`/api/location/coordinates?latitude=${latitude}&longitude=${longitude}`);\n\n    return response.json();\n}"
  },
  {
    "path": "client/src/services/weather.ts",
    "content": "import type {\n    IForecast\n} from '../types/weather';\n\nexport async function getForecast(latitude: number, longitude: number, units?: string): Promise<IForecast> {\n    const response = await fetch(`/api/weather/forecast?latitude=${latitude}&longitude=${longitude}&units=${units}`);\n\n    return response.json();\n}\n"
  },
  {
    "path": "client/src/startup/application.ts",
    "content": "import {\n    createApp\n} from 'vue';\n\nimport App from '../app.vue';\n\nexport default function initialiseApplication() {\n    return createApp(App);\n}"
  },
  {
    "path": "client/src/startup/components.ts",
    "content": "import Components from '@ocula/components';\n\nimport type {\n    App\n} from 'vue';\n\nexport default function initialiseComponents(application: App) {\n    return application.use(Components);\n}"
  },
  {
    "path": "client/src/startup/index.ts",
    "content": "import './vendor';\n\nimport initialiseComponents from './components';\nimport initialiseRouter from './router';\nimport initialiseApplication from './application';\nimport initialiseState from './state';\nimport initialiseLogging from './logging';\nimport initialiseWorker from './worker';\n\nexport default async function start() {\n    const application = initialiseApplication();\n\n    const router = initialiseRouter(application);\n    \n    initialiseState(application);\n    initialiseComponents(application);\n    \n    initialiseWorker();\n    initialiseLogging();\n\n    await router.isReady();\n\n    application.mount('body');\n\n    return {\n        router,\n        application\n    };\n}"
  },
  {
    "path": "client/src/startup/logging.ts",
    "content": "import {\n    envIsProduction\n} from '@ocula/utilities';\n\nimport {\n    init\n} from '@sentry/browser';\n\n\n// import {\n//     Vue as SentryVue\n// } from '@sentry/integrations';\n\nexport default function initialiseLogging() {\n    init({\n        enabled: envIsProduction,\n        dsn: process.env.SENTRY_DSN,\n        // integrations: [\n        //     new SentryVue({\n        //         Vue,\n        //         attachProps: true \n        //     })\n        // ]\n    });\n}"
  },
  {
    "path": "client/src/startup/router.ts",
    "content": "import ROUTES from '../constants/core/routes';\n\nimport routes from '../routes';\n\nimport setThemeMeta from '../helpers/set-theme-meta';\n\nimport Router, {\n    router\n} from '@ocula/router';\n\nimport type {\n    App\n} from 'vue';\n\nimport {\n    theme\n} from '../store';\n\ndeclare global {\n    interface Window {\n        gtag?(key: string, trackingId: string, meta: any): void\n    }\n}\n\nexport default function initialiseRouter(application: App) {\n    application.use(Router, routes);\n\n    router.beforeEach((to, from, next) => {\n        if (!theme.value) {\n            return next();\n        }\n\n        const isForecast = to.matched.some(({ name }) => name === ROUTES.forecast.index);\n\n        let {\n            colour\n        } = theme.value.core;\n\n        if (isForecast) {\n            colour = theme.value.weather.colour || colour;\n        }\n\n        setThemeMeta(colour);\n\n        next();\n    });\n\n    if ('gtag' in window) {\n        router.afterEach(to => window.gtag('config', process.env.GA_TRACKING_ID, {\n            'page_path': to.path\n        }));\n    }\n\n    return router;\n}"
  },
  {
    "path": "client/src/startup/state.ts",
    "content": "import {\n    plugin\n} from '@ocula/state';\n\nimport type {\n    App\n} from 'vue';\n\nexport default function initialiseComponents(application: App) {\n    return application.use(plugin);\n}"
  },
  {
    "path": "client/src/startup/vendor.ts",
    "content": "import 'core-js/stable';\nimport 'regenerator-runtime/runtime';"
  },
  {
    "path": "client/src/startup/worker.ts",
    "content": "import {\n    Workbox,\n    messageSW\n} from 'workbox-window';\n\nimport {\n    clearData\n} from '../store/helpers/storage';\n\nimport {\n    componentsController\n} from '@ocula/components';\n\nimport {\n    envIsDevelopment\n} from '@ocula/utilities';\n\nimport type {\n    WorkboxLifecycleWaitingEvent\n} from 'workbox-window/utils/WorkboxEvent';\n\nexport default async function initialiseWorker() {\n    if (envIsDevelopment || !navigator.serviceWorker) {\n        return;\n    }\n\n    const workbox = new Workbox('/service-worker.js');\n\n    let registration: ServiceWorkerRegistration;\n\n    async function handleUpdate(event: WorkboxLifecycleWaitingEvent) {\n        try {\n            await componentsController.confirm({\n                message: 'An update to Ocula has been installed. Would you like to reload now and complete the update?',\n                confirmLabel: 'Yes, update',\n                cancelLabel: 'Later'\n            });\n\n            workbox.addEventListener('controlling', () => {\n                clearData();\n                window.location.reload();\n            });\n    \n            if (registration && registration.waiting) {\n                messageSW(registration.waiting, {  \n                    type: 'SKIP_WAITING'\n                });\n            }\n        } catch {\n            // do nothing\n        }\n    }\n\n    workbox.addEventListener('waiting', handleUpdate);\n    workbox.addEventListener('externalwaiting', handleUpdate);\n\n    registration = await workbox.register();\n}"
  },
  {
    "path": "client/src/static/.well-known/somthing.txt",
    "content": "sdfgdsf"
  },
  {
    "path": "client/src/static/robots.txt",
    "content": "Sitemap: https://app.ocula.io/sitemap.xml"
  },
  {
    "path": "client/src/static/sitemap.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"> \n    <url>\n        <loc>https://app.ocula.io</loc>\n        <priority>1.0</priority>\n    </url>\n    <url>\n        <loc>https://app.ocula.io/maps</loc>\n        <priority>0.8</priority>\n    </url>\n</urlset>"
  },
  {
    "path": "client/src/store/actions/add-location.ts",
    "content": "import updateSettings from './update-settings';\nimport setLocation from './set-location';\n\nimport type {\n    ILocation\n} from '../../types/location';\n\nimport {\n    state\n} from '../store';\n\nimport {\n    arrayUniqueBy\n} from '@ocula/utilities';\n\nexport default function addLocation(location: ILocation, setAsCurrent: boolean = false): void {\n    const {\n        locations\n    } = state.settings;\n    \n    updateSettings({\n        locations: arrayUniqueBy([...locations, location], ({ id }) => id) \n    });\n\n    if (setAsCurrent) {\n        setLocation(location);\n    }\n}"
  },
  {
    "path": "client/src/store/actions/load-forecast.ts",
    "content": "import {\n    state,\n    mutate\n} from '../store';\n\nimport {\n    getForecast\n} from '../../services/weather';\n\nexport default async function loadForecast(latitude: number, longitude: number) {\n    const {\n        units\n    } = state.settings;\n\n    const forecast = await getForecast(latitude, longitude, units);\n\n    mutate('set-forecast', state => state.forecast = forecast);\n}"
  },
  {
    "path": "client/src/store/actions/load-location.ts",
    "content": "import LOCATION from '../../enums/forecast/location';\n\nimport {\n    state,\n    mutate\n} from '../store';\n\nimport {\n    getPosition\n} from '../helpers/location';\n\nimport {\n    getLocation\n} from '../../services/location';\n\nimport type {\n    ILocation\n} from '../../types/location';\n\nexport default async function loadLocation(): Promise<ILocation> {\n    const {\n        location: savedLocation\n    } = state.settings;\n\n    let location = savedLocation;\n\n    if (location === LOCATION.current) {\n        const {\n            latitude,\n            longitude\n        } = await getPosition();\n\n        if (!latitude || !longitude) {\n            return;\n        }\n\n        location = await getLocation(latitude, longitude);\n    } \n\n    mutate('set-location', state => state.location = location as ILocation);\n\n    return location;\n} "
  },
  {
    "path": "client/src/store/actions/move-section.ts",
    "content": "import FORECAST_SECTION from '../../enums/forecast/section';\n\nimport updateSettings from './update-settings';\n\nimport {\n    state\n} from '../store';\n\nimport {\n    arraySwapBy\n} from '@ocula/utilities';\n\nexport default function moveSection(type: FORECAST_SECTION, offset: number = 1): void {\n    let {\n        sections\n    } = state.settings.forecast;\n\n    const index = sections.findIndex(section => section.type === type);\n\n    sections = arraySwapBy(sections, index, index + offset);\n\n    updateSettings({\n        forecast: {\n            sections\n        }\n    });\n}"
  },
  {
    "path": "client/src/store/actions/remove-location.ts",
    "content": "import updateSettings from './update-settings';\n\nimport type {\n    ILocation\n} from '../../types/location';\n\nimport {\n    state\n} from '../store';\n\nexport default function removeLocation(location: ILocation): void {\n    const {\n        locations\n    } = state.settings;\n\n    updateSettings({\n        locations: locations.filter(({ id }) => id !== location.id)\n    });\n}"
  },
  {
    "path": "client/src/store/actions/reset-settings.ts",
    "content": "import SETTINGS from '../../constants/core/settings';\n\nimport {\n    mutate\n} from '../store';\n\nimport {\n    saveSettings,\n    clearData\n} from '../helpers/storage';\n\nexport default function resetSettings() {\n    mutate('reset-settings', state => state.settings = SETTINGS);\n    saveSettings(SETTINGS);\n    clearData();\n}"
  },
  {
    "path": "client/src/store/actions/search-locations.ts",
    "content": "import {\n    searchLocations\n} from '../../services/location';\n\nimport type {\n    ILocation\n} from '../../types/location';\n\nexport default async function (query: string): Promise<ILocation[]> {\n    return searchLocations(query);\n}"
  },
  {
    "path": "client/src/store/actions/set-current-location.ts",
    "content": "import LOCATION from '../../enums/forecast/location';\n\nimport setLocation from './set-location';\n\nexport default function setCurrentLocation(): void {\n    setLocation(LOCATION.current);\n}"
  },
  {
    "path": "client/src/store/actions/set-location.ts",
    "content": "import LOCATION from '../../enums/forecast/location';\nimport EVENTS from '../../constants/core/events';\n\nimport setLastUpdated from '../mutations/set-last-updated';\n\nimport updateSettings from './update-settings';\n\nimport eventEmitter from '@ocula/event-emitter';\n\nimport {\n    typeIsNil\n} from '@ocula/utilities';\n\nimport type {\n    ILocation\n} from '../../types/location';\n\nexport default function setLocation(location: ILocation | LOCATION): void {\n    if (typeIsNil(location)) {\n        return;\n    }\n    \n    setLastUpdated(null);\n\n    updateSettings({\n        location\n    });\n\n    eventEmitter.emit(EVENTS.location.set, location);\n}"
  },
  {
    "path": "client/src/store/actions/set-section-visibility.ts",
    "content": "import FORECAST_SECTION from '../../enums/forecast/section';\n\nimport updateSettings from './update-settings';\n\nimport {\n    state\n} from '../store';\n\nexport default function setSectionVisibility(type: FORECAST_SECTION, isVisible = true): void {\n    let {\n        sections\n    } = state.settings.forecast;\n\n    sections = sections.map(section => {\n        const visible = section.type === type ? isVisible : section.visible;\n\n        return {\n            ...section,\n            visible\n        };\n    });\n\n    updateSettings({\n        forecast: {\n            sections\n        }\n    });\n}"
  },
  {
    "path": "client/src/store/actions/update-settings.ts",
    "content": "import setSettings from '../mutations/set-settings';\n\nimport type {\n    ISettings\n} from '../../types/storage';\n\nexport default function updateSettings(settings: Partial<ISettings>): void {\n    setSettings(settings);\n}"
  },
  {
    "path": "client/src/store/actions/update.ts",
    "content": "import STATUS from '../../enums/core/status';\n\nimport GLOBAL from '../../constants/core/global';\n\nimport setStatus from '../mutations/set-status';\nimport setLastUpdated from '../mutations/set-last-updated';\n\nimport loadLocation from './load-location';\nimport loadForecast from './load-forecast';\n\nimport {\n    state\n} from '../store';\n\nimport {\n    saveData\n} from '../helpers/storage';\n\nexport default async function update(force: boolean = false) {\n    const lastUpdated = state.lastUpdated;\n\n    if (!force && lastUpdated && Date.now() - +lastUpdated < GLOBAL.updateThreshold) {\n        return;\n    }\n\n    setStatus(STATUS.loading);\n    \n    try {\n        const {\n            latitude,\n            longitude\n        } = await loadLocation();\n        \n        await loadForecast(latitude, longitude);\n        \n        setLastUpdated();\n        \n        const {\n            lastUpdated,\n            location,\n            forecast\n        } = state;\n        \n        saveData({\n            lastUpdated,\n            location,\n            forecast\n        });\n    } catch (error) {\n        setStatus(STATUS.error);\n    }\n\n    setStatus(null);\n}"
  },
  {
    "path": "client/src/store/getters/forecast.ts",
    "content": "import UNITS from '../../enums/forecast/units';\nimport FORMATS from '../../constants/forecast/formats';\n\nimport {\n    defaultFormatter\n} from '../../constants/forecast/formatters';\n\nimport {\n    getter\n} from '../store';\n\nimport {\n    objectTransform\n} from '@ocula/utilities';\n\nimport type {\n    Formatted,\n    IMappedForecast\n} from '../../types/state';\n\nimport type {\n    IForecast\n} from '../../types/weather';\n\nexport default getter<Formatted<IMappedForecast>>(state => {\n    const {\n        forecast,\n        settings\n    } = state;\n\n    if (!forecast) {\n        return;\n    }\n\n    const format = FORMATS[settings.units] || FORMATS[UNITS.metric];\n\n    const {\n        daily,\n        ...other\n    } = objectTransform<IForecast, Formatted<IMappedForecast>>(forecast, format, defaultFormatter);\n\n    const today = daily.shift();\n    \n    return {\n        ...other,\n        today,\n        daily\n    };\n});"
  },
  {
    "path": "client/src/store/getters/format.ts",
    "content": "import {\n    getter\n} from '../store';\n\nimport {\n    dateFormat,\n    dateUtcToZoned\n} from '@ocula/utilities';\n\nimport type {\n    IFormatter\n} from '../../types/state';\n\nexport default getter<IFormatter>(({ forecast }) => {\n    let options;\n    let converter = value => value;\n\n    const output = {\n        date: (value: Date, format: string = 'EEEE, d MMM') => dateFormat(converter(value), format, options),\n        time: (value: Date, format: string = 'h:mm a') => dateFormat(converter(value), format, options).toLowerCase()\n    };\n\n    if (forecast && forecast.timezone) {\n        converter = value => dateUtcToZoned(value, forecast.timezone);\n        \n        options = {\n            timeZone: forecast.timezone\n        };\n    }\n\n    return output;\n});"
  },
  {
    "path": "client/src/store/getters/phase.ts",
    "content": "import getPhase from '../../helpers/get-phase';\n\nimport {\n    getter\n} from '../store';\n\nimport {\n    dateToUnix\n} from '@ocula/utilities';\n\nexport default getter(() => getPhase(dateToUnix(new Date())));"
  },
  {
    "path": "client/src/store/getters/theme.ts",
    "content": "import THEME from '../../constants/forecast/theme';\n\nimport phase from './phase';\n\nimport {\n    getter\n} from '../store';\n\nimport {\n    core,\n    weather\n} from '../../themes';\n\nimport type {\n    ITheme\n} from '../../types/themes';\n\nimport {\n    typeIsPlainObject\n} from '@ocula/utilities';\n\nfunction getPhasedTheme(theme: ITheme): ITheme {\n    let {\n        colour,\n        mapStyle,\n        ...value\n    } = theme;\n\n    if (typeIsPlainObject(colour)) {\n        colour = colour[phase.value]\n    }\n\n    if (typeIsPlainObject(mapStyle)) {\n        mapStyle = mapStyle[phase.value];\n    }\n\n    return {\n        ...value,\n        colour,\n        mapStyle\n    };\n}\n\nexport default getter(({ settings, forecast }) => {\n    const {\n        theme\n    } = settings;\n    \n    let coreTheme = core[theme] || core.default;\n    let weatherTheme = weather.default;\n\n    coreTheme = getPhasedTheme(coreTheme);\n    \n    if (forecast && forecast.current) {\n        const conditionId = forecast.current.weather[0].id;\n\n        weatherTheme = THEME[conditionId] || THEME[Math.floor(conditionId / 100) * 100] || weather.default;\n        weatherTheme = getPhasedTheme(weatherTheme);\n    }\n\n    return {\n        core: coreTheme,\n        weather: weatherTheme\n    };\n});"
  },
  {
    "path": "client/src/store/getters/unit-of-measure.ts",
    "content": "import UNITS from '../../enums/forecast/units';\nimport UNIT_OF_MEASURE from '../../constants/forecast/unit-of-measure';\n\nimport {\n    getter\n} from '../store';\n\nexport default getter(({ settings }) => UNIT_OF_MEASURE[settings.units || UNITS.metric])"
  },
  {
    "path": "client/src/store/helpers/location.ts",
    "content": "import type {\n    ICoordinate\n} from '../../types/location';\n\nexport async function getPosition(): Promise<ICoordinate> {\n    const position: Position = await new Promise((resolve, reject) => {\n        navigator.geolocation.getCurrentPosition(resolve, reject, {\n            maximumAge: 0,\n            enableHighAccuracy: true\n        });\n    });\n\n    if (position) {\n        return position.coords;\n    }\n\n    return {\n        latitude: 0,\n        longitude: 0,\n    };\n}"
  },
  {
    "path": "client/src/store/helpers/storage.ts",
    "content": "import DATA from '../../constants/core/data';\nimport SETTINGS from '../../constants/core/settings';\nimport MIGRATIONS from '../../constants/core/migrations';\nimport STORAGE_KEYS from '../../constants/core/storage-keys';\n\nimport {\n    objectMerge, objectMergeWith, typeIsArray\n} from '@ocula/utilities';\n\nimport type {\n    ISettings,\n    IStoredData\n} from '../../types/storage';\n\nexport function getSettings(): ISettings {\n    const storedSettings = localStorage.getItem(STORAGE_KEYS.settings);\n\n    if (!storedSettings) {\n        return SETTINGS;\n    }\n\n    let settings = JSON.parse(storedSettings) as ISettings;\n\n    settings = objectMergeWith(SETTINGS, settings, (obj, src) => {\n        if (typeIsArray(obj)) {\n            return src;\n        }\n    });\n\n    if (settings.version < SETTINGS.version && settings.version in MIGRATIONS) {\n        try {\n            settings = MIGRATIONS[settings.version.toString()](settings);\n            settings.version = SETTINGS.version;\n\n            saveSettings(settings);\n        } catch (error) {\n            console.warn('Failed to migrate settings');\n        }\n    }\n\n    return settings;\n}\n\nexport function getData(): IStoredData {\n    const storedData = localStorage.getItem(STORAGE_KEYS.data);\n\n    if (!storedData) {\n        return DATA;\n    }\n\n    const data = JSON.parse(storedData) as IStoredData;\n\n    data.lastUpdated = new Date(data.lastUpdated);\n\n    return objectMerge(DATA, data);\n}\n\nexport function saveSettings(settings: ISettings): void {\n    localStorage.setItem(STORAGE_KEYS.settings, JSON.stringify(settings));\n}\n\nexport function saveData({ lastUpdated, location, forecast }: IStoredData): void {\n    localStorage.setItem(STORAGE_KEYS.data, JSON.stringify({\n        location,\n        forecast,\n        lastUpdated\n    }));\n}\n\nexport function clearData(): void {\n    localStorage.removeItem(STORAGE_KEYS.data);\n}"
  },
  {
    "path": "client/src/store/index.ts",
    "content": "export { state } from './store';\n\nexport { default as forecast } from './getters/forecast';\nexport { default as phase } from './getters/phase';\nexport { default as format } from './getters/format';\nexport { default as theme } from './getters/theme';\nexport { default as unitOfMeasure } from './getters/unit-of-measure';\n\nexport { default as update } from './actions/update';\nexport { default as loadLocation } from './actions/load-location';\nexport { default as loadForecast } from './actions/load-forecast';\nexport { default as searchLocations } from './actions/search-locations';\nexport { default as addLocation } from './actions/add-location';\nexport { default as removeLocation } from './actions/remove-location';\nexport { default as setLocation } from './actions/set-location';\nexport { default as setCurrentLocation } from './actions/set-current-location';\nexport { default as moveSection } from './actions/move-section';\nexport { default as setSectionVisibility } from './actions/set-section-visibility';\nexport { default as updateSettings } from './actions/update-settings';\nexport { default as resetSettings } from './actions/reset-settings';"
  },
  {
    "path": "client/src/store/mutations/set-last-updated.ts",
    "content": "import {\n    mutate\n} from '../store';\n\nexport default function setLastUpdated(value: Date | null = new Date()): void {\n    mutate('set-last-updated', state => state.lastUpdated = value);\n}"
  },
  {
    "path": "client/src/store/mutations/set-settings.ts",
    "content": "import {\n    state,\n    mutate\n} from '../store';\n\nimport {\n    saveSettings\n} from '../helpers/storage';\n\nimport type {\n    ISettings\n} from '../../types/storage';\n\nexport default function setSettings(value: Partial<ISettings>) {\n    const settings = {\n        ...state.settings,\n        ...value\n    };\n\n    mutate('set-settings', state => state.settings = settings);\n    saveSettings(settings);\n}"
  },
  {
    "path": "client/src/store/mutations/set-status.ts",
    "content": "import STATUS from '../../enums/core/status';\n\nimport {\n    mutate\n} from '../store';\n\nexport default function setStatus(status: STATUS = null): void {\n    mutate('set-status', state => state.status = status);\n}"
  },
  {
    "path": "client/src/store/state/index.ts",
    "content": "import {\n    getSettings,\n    getData\n} from '../helpers/storage';\n\nimport {\n    IState\n} from '../../types/state';\n\nfunction getState(): IState {\n    const settings = getSettings();\n\n    const {\n        location,\n        forecast,\n        lastUpdated\n    } = getData();\n\n    return {\n        settings,\n        location,\n        forecast,\n        lastUpdated,\n        status: null\n    };\n}\n\nexport default getState();"
  },
  {
    "path": "client/src/store/store.ts",
    "content": "import createStore from '@ocula/state';\n\nimport _state from './state';\n\nexport const {\n    state,\n    getter,\n    mutate\n} = createStore('ocula', _state);"
  },
  {
    "path": "client/src/themes/core/dark/_dark.scss",
    "content": "@mixin dark {\n    --font__colour: #F7F7F7;\n    --background__colour: #333333;\n    --background__colour--hover: #444444;\n    --border__colour: #777777;\n}"
  },
  {
    "path": "client/src/themes/core/dark/index.scss",
    "content": "@import \"./_dark\";\n\n.theme--dark {\n    @include dark;\n}"
  },
  {
    "path": "client/src/themes/core/dark/index.ts",
    "content": "import './index.scss';\n\nimport type {\n    ITheme\n} from '../../../types/themes';\n\nexport default {\n    id: 'dark',\n    name: 'Dark',\n    colour: '#333333',\n    class: 'theme--dark',\n    mapStyle: 'dark'\n} as ITheme;"
  },
  {
    "path": "client/src/themes/core/default/index.scss",
    "content": "@import \"../light/_light\";\n@import \"../dark/_dark\";\n\n.theme--default {\n    @include light;\n}\n\n.phase--night {\n\n    &.theme--default {\n        @include dark;\n    }\n}\n\n@media (prefers-color-scheme: dark) {\n\n    .theme--default {\n        @include dark;\n    }\n\n}"
  },
  {
    "path": "client/src/themes/core/default/index.ts",
    "content": "import './index.scss';\n\nimport PHASE from '../../../enums/forecast/phase';\n\nimport type {\n    ITheme\n} from '../../../types/themes';\n\nexport default {\n    id: 'default',\n    name: 'Default',\n    colour: {\n        [PHASE.night as string]: '#333333'\n    },\n    class: 'theme--default',\n    mapStyle: {\n        [PHASE.night as string]: 'dark'\n    }\n} as ITheme;"
  },
  {
    "path": "client/src/themes/core/index.ts",
    "content": "import _default from './default';\nimport light from './light';\nimport dark from './dark';\n\nexport default {\n    default: _default,\n    light,\n    dark\n};"
  },
  {
    "path": "client/src/themes/core/light/_light.scss",
    "content": "@mixin light {\n    --font__colour: #353539;\n    --background__colour: #FFFFFF;\n    --background__colour--hover: #EEEEEE;\n    --border__colour: #EDEDED;\n}"
  },
  {
    "path": "client/src/themes/core/light/index.scss",
    "content": "@import \"./_light\";\n\n.theme--light {\n    @include light;\n}"
  },
  {
    "path": "client/src/themes/core/light/index.ts",
    "content": "import './index.scss';\n\nimport type {\n    ITheme\n} from '../../../types/themes';\n\nexport default {\n    id: 'light',\n    name: 'Light',\n    colour: '#FFFFFF',\n    class: 'theme--light',\n    mapStyle: 'light'\n} as ITheme;"
  },
  {
    "path": "client/src/themes/index.ts",
    "content": "export { default as core } from './core';\nexport { default as weather } from './weather';"
  },
  {
    "path": "client/src/themes/weather/clear/index.scss",
    "content": ".theme--weather-clear {\n    --font__colour--weather: #FFFFFF;\n    --background__colour--weather: #5D9BE5;\n}\n\n.phase--night {\n    \n    & .theme--weather-clear {\n        --background__colour--weather: #44296A;\n    }\n}"
  },
  {
    "path": "client/src/themes/weather/clear/index.ts",
    "content": "import './index.scss';\n\nimport PHASE from '../../../enums/forecast/phase';\n\nimport type {\n    ITheme\n} from '../../../types/themes';\n\nexport default {\n    id: 'weather-clear',\n    name: 'Clear',\n    colour: {\n        [PHASE.day as string]: '#5D9BE5',\n        [PHASE.night as string]: '#44296A'\n    },\n    class: 'theme--weather-clear',\n    mapStyle: {\n        [PHASE.night as string]: 'dark'\n    }\n} as ITheme;"
  },
  {
    "path": "client/src/themes/weather/default/index.ts",
    "content": "import type {\n    ITheme\n} from '../../../types/themes';\n\nexport default {\n    id: 'weather-default',\n    name: 'Default',\n    colour: '',\n    class: ''\n} as ITheme;"
  },
  {
    "path": "client/src/themes/weather/index.ts",
    "content": "import _default from './default';\nimport clear from './clear';\nimport partlyCloudy from './partly-cloudy';\nimport rainy from './rainy';\n\nexport default {\n    default: _default,\n    clear,\n    partlyCloudy,\n    rainy\n};"
  },
  {
    "path": "client/src/themes/weather/partly-cloudy/index.scss",
    "content": ".theme--weather-partly-cloudy {\n    --font__colour--weather: #FFFFFF;\n    --background__colour--weather: #5D9BE5;\n}\n\n.phase--night {\n    \n    & .theme--weather-partly-cloudy {\n        --background__colour--weather: #44296A;\n    }\n}"
  },
  {
    "path": "client/src/themes/weather/partly-cloudy/index.ts",
    "content": "import './index.scss';\n\nimport PHASE from '../../../enums/forecast/phase';\n\nimport type {\n    ITheme\n} from '../../../types/themes';\n\nexport default {\n    id: 'weather-partly-cloudy',\n    name: 'Partly Cloudy',\n    colour: {\n        [PHASE.day as string]: '#5D9BE5',\n        [PHASE.night as string]: '#44296A'\n    },\n    class: 'theme--weather-partly-cloudy',\n    mapStyle: {\n        [PHASE.night as string]: 'dark'\n    }\n} as ITheme;"
  },
  {
    "path": "client/src/themes/weather/rainy/index.scss",
    "content": ".theme--weather-rainy {\n    --font__colour--weather: #FFFFFF;\n    --background__colour--weather: #AAAAAA;\n}"
  },
  {
    "path": "client/src/themes/weather/rainy/index.ts",
    "content": "import './index.scss';\n\nimport type {\n    ITheme\n} from '../../../types/themes';\n\nexport default {\n    id: 'weather-rainy',\n    name: 'Rainy',\n    colour: '#AAAAAA',\n    class: 'theme--weather-rainy'\n} as ITheme;"
  },
  {
    "path": "client/src/types/location.ts",
    "content": "export interface ICoordinate {\n    latitude: number;\n    longitude: number;\n}\n\nexport interface ILocation extends ICoordinate {\n    id: string;\n    shortName: string;\n    longName: string;\n}"
  },
  {
    "path": "client/src/types/state.ts",
    "content": "import type {\n    ISettings\n} from './storage';\n\nimport type {\n    ILocation\n} from './location';\n\nimport type {\n    IForecast,\n    IForecastCurrent,\n    IForecastWeather,\n    IForecastDay,\n    IForecastHour\n} from './weather';\nimport STATUS from '../enums/core/status';\n\nexport type Formatted<T, U = string> =  {\n    [P in keyof T]: T[P] extends string | number ? {\n        raw: T[P];\n        formatted: U\n    } : Formatted<T[P]>\n}\n\nexport interface IState {\n    status: STATUS;\n    lastUpdated: Date;\n    settings: ISettings;\n    location: ILocation;\n    forecast: IForecast;\n};\n\nexport interface IMappedForecastCurrent extends Omit<IForecastCurrent, 'weather'> {\n    weather: IForecastWeather\n};\n\nexport interface IMappedForecastDay extends Omit<IForecastDay, 'weather'> {\n    weather: IForecastWeather\n};\n\nexport interface IMappedForecastHour extends Omit<IForecastHour, 'weather'> {\n    weather: IForecastWeather\n};\n\nexport interface IMappedForecast extends Omit<IForecast, 'current' | 'daily' | 'hourly'> {\n    current: IMappedForecastCurrent;\n    today: IMappedForecastDay;\n    daily: IMappedForecastDay[];\n    hourly: IMappedForecastHour[];\n};\n\nexport interface IFormatter {\n    date(value: Date, format?: string): string;\n    time(value: Date, format?: string): string;\n};"
  },
  {
    "path": "client/src/types/storage.ts",
    "content": "import type LOCATION from '../enums/forecast/location';\nimport UNITS from '../enums/forecast/units';\nimport type MAP from '../enums/maps/map';\nimport type FORECAST_SECTION from '../enums/forecast/section';\n\nimport type {\n    ILocation\n} from './location';\n\nimport type {\n    IForecast\n} from './weather';\n\ninterface ISection {\n    type: FORECAST_SECTION;\n    visible: boolean;\n    options: any;\n}\n\nexport interface IForecastSettings {\n    sections: ISection[];\n}\n\nexport interface IMapSettings {\n    default: MAP;\n    zoom: number;\n    pitch: number;\n    framerate: number;\n}\n\nexport interface ISettings {\n    version: number;\n    units: UNITS;\n    theme: string;\n    location?: ILocation | LOCATION;\n    locations?: ILocation[];\n    forecast: IForecastSettings;\n    maps: IMapSettings;\n};\n\nexport interface IStoredData {\n    lastUpdated: Date;\n    location: ILocation;\n    forecast: IForecast;\n}"
  },
  {
    "path": "client/src/types/themes.ts",
    "content": "export interface ITheme {\n    id: string;\n    name: string;\n    colour: string | Record<string, string>;\n    class: string | string[];\n    mapStyle: string | Record<string, string>;\n}"
  },
  {
    "path": "client/src/types/weather.ts",
    "content": "export interface IForecast {\n    lat: number;\n    lon: number;\n    timezone: string;\n    timezoneOffset: number;\n    current: IForecastCurrent;\n    hourly: IForecastHour[];\n    daily: IForecastDay[];\n    tides: IForecastTides;\n    radar: IForecastRadar;\n}\n\nexport interface IForecastWeather {\n    id: number;\n    main: string;\n    description: string;\n    icon: string;\n}\n\nexport interface IForecastFeelsLike {\n    day: number;\n    night: number;\n    eve: number;\n    morn: number;\n}\n\nexport interface IForecastTemperature {\n    day: number;\n    min: number;\n    max: number;\n    night: number;\n    eve: number;\n    morn: number;\n}\n\nexport interface IForecastCurrent {\n    dt: number;\n    sunrise?: number;\n    sunset?: number;\n    temp: number;\n    feelsLike: number;\n    pressure: number;\n    humidity: number;\n    dewPoint: number;\n    uvi?: number;\n    clouds: number;\n    visibility: number;\n    windSpeed: number;\n    windDeg: number;\n    weather: IForecastWeather[];\n    rain?: Record<string, number>;\n    snow?: Record<string, number>;\n}\n\nexport interface IForecastHour {\n    dt: number;\n    sunrise?: number;\n    sunset?: number;\n    temp: number;\n    feelsLike: number;\n    pressure: number;\n    humidity: number;\n    dewPoint: number;\n    uvi?: number;\n    clouds: number;\n    visibility: number;\n    windSpeed: number;\n    windDeg: number;\n    weather: IForecastWeather[];\n    pop: number;\n    rain?: Record<string, number>;\n    snow?: Record<string, number>;\n}\n\nexport interface IForecastDay {\n    dt: number;\n    sunrise: number;\n    sunset: number;\n    temp: IForecastTemperature;\n    feelsLike: IForecastFeelsLike;\n    pressure: number;\n    humidity: number;\n    dewPoint: number;\n    windSpeed: number;\n    windDeg: number;\n    weather: IForecastWeather[];\n    clouds: number;\n    pop: number;\n    uvi: number;\n    rain?: Record<string, number>;\n    snow?: Record<string, number>;\n}\n\nexport interface IForecastTideHeight {\n    dt:     number;\n    date:   string;\n    height: number;\n}\n\nexport interface IForecastTideExtreme extends IForecastTideHeight {\n    type?:  string;\n}\n\nexport interface IForecastTides {\n    status:      number;\n    callCount:   number;\n    copyright:   string;\n    requestLat:  number;\n    requestLon:  number;\n    responseLat: number;\n    responseLon: number;\n    atlas:       string;\n    station:     string;\n    heights:     IForecastTideHeight[];\n    extremes:    IForecastTideExtreme[];\n}\n\nexport interface IForecastRadar {\n    timestamps: number[];\n}"
  },
  {
    "path": "client/src/vue-shim.d.ts",
    "content": "declare module \"*.vue\" {\n    import Vue from 'vue'\n    export default Vue\n}"
  },
  {
    "path": "client/tsconfig.json",
    "content": "{\n    \"extends\": \"../tsconfig.json\",\n    \"compilerOptions\": {\n        \"resolveJsonModule\": true\n    }\n}"
  },
  {
    "path": "client/webpack.config.babel.js",
    "content": "module.exports = env => require(`./build/${env}`);"
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"@ocula/app\",\n    \"repository\": \"https://github.com/andrewcourtice/ocula.git\",\n    \"author\": \"Andrew Courtice\",\n    \"description\": \"The free and open-source progressive weather app\",\n    \"license\": \"MIT\",\n    \"private\": true,\n    \"workspaces\": [\n        \"api\",\n        \"client\",\n        \"packages/*\"\n    ],\n    \"scripts\": {\n        \"dev\": \"cd client && yarn start --port $PORT\",\n        \"build\": \"cd client && yarn build\"\n    },\n    \"devDependencies\": {\n        \"typescript\": \"^4.1.2\"\n    }\n}\n"
  },
  {
    "path": "packages/charts/package.json",
    "content": "{\n    \"name\": \"@ocula/charts\",\n    \"version\": \"1.0.0\",\n    \"main\": \"./src/index.ts\",\n    \"license\": \"MIT\",\n    \"dependencies\": {\n        \"@ocula/utilities\": \"1.0.0\",\n        \"d3-array\": \"^2.9.1\",\n        \"d3-axis\": \"^2.0.0\",\n        \"d3-ease\": \"^2.0.0\",\n        \"d3-path\": \"^2.0.0\",\n        \"d3-scale\": \"^3.2.3\",\n        \"d3-selection\": \"^2.0.0\",\n        \"d3-shape\": \"^2.0.0\",\n        \"d3-transition\": \"^2.0.0\"\n    }\n}\n"
  },
  {
    "path": "packages/charts/src/charts/_base/chart.ts",
    "content": "import * as d3 from '../../d3';\n\nimport {\n    objectMerge,\n    stringUniqueId\n} from '@ocula/utilities';\n\nexport interface IChartOptions {\n    padding?: {\n        top?: number;\n        bottom?: number;\n        left?: number;\n        right?: number;\n    },\n    classes?: {\n        svg?: string;\n        canvas?: string;\n    },\n    animation?: {\n        duration?: number;\n    }\n}\n\nexport default abstract class Chart<T extends IChartOptions = IChartOptions> {\n\n    protected id: string;\n    protected element: Element;\n    protected options: T;\n    protected rendering: boolean;\n\n    protected width: number;\n    protected height: number;\n\n    protected svg: d3.Selection<SVGSVGElement, unknown, null, undefined>;\n    protected canvas: d3.Selection<SVGGElement, unknown, null, undefined>;\n\n    constructor(element: Element) {\n        this.id = stringUniqueId();\n        this.element = element;\n\n        this.width = 0;\n        this.height = 0;\n        \n        this.svg = d3.select(this.element)\n            .append('svg')\n            .attr('id', `chart-${this.id}`)\n            .attr('width', '100%')\n            .attr('height', '100%')\n            .style('display', 'block');\n\n        this.canvas = this.svg.append('g');\n    }\n\n    protected get defaultOptions(): IChartOptions {\n        return {\n            classes: {\n                svg: 'chart',\n                canvas: 'chart__canvas'\n            },\n            padding: {\n                top: 10,\n                bottom: 10,\n                left: 10,\n                right: 10\n            },\n            animation: {\n                duration: 1000\n            }\n        };\n    }\n\n    protected bootstrap(options: T) {\n        this.options = objectMerge(this.defaultOptions, options);\n\n        const {\n            width,\n            height\n        } = this.element.getBoundingClientRect();\n\n        const {\n            top,\n            left,\n            bottom,\n            right\n        } = this.options.padding;\n\n        this.width = width - (left + right);\n        this.height = height - (top + bottom);\n\n        this.svg.classed(this.options.classes.svg, true)\n            .attr('viewBox', `0 0 ${width} ${height}`);\n\n        this.canvas.classed(this.options.classes.canvas, true)\n            .attr('transform', `translate(${left}, ${top})`);\n    }\n\n    protected reset() {\n        this.canvas.selectAll('*').remove();\n    }\n\n}"
  },
  {
    "path": "packages/charts/src/charts/line/constants/curve.ts",
    "content": "import LINE_TYPE from '../enums/line-type';\n\nimport * as d3 from '../../../d3';\n\nexport default {\n    [LINE_TYPE.line]: d3.curveLinear,\n    [LINE_TYPE.spline]: d3.curveCatmullRom.alpha(1),\n    [LINE_TYPE.step]: d3.curveStep\n};"
  },
  {
    "path": "packages/charts/src/charts/line/enums/line-type.ts",
    "content": "const enum LINE_TYPE {\n    line = 'line',\n    spline = 'spline',\n    step = 'step'\n};\n\nexport default LINE_TYPE;"
  },
  {
    "path": "packages/charts/src/charts/line/enums/marker-type.ts",
    "content": "const enum MARKER_TYPE {\n    point = 'point',\n    arrow = 'arrow'\n};\n\nexport default MARKER_TYPE;"
  },
  {
    "path": "packages/charts/src/charts/line/index.ts",
    "content": "import LINE_TYPE from './enums/line-type';\nimport MARKER_TYPE from './enums/marker-type';\n\nimport SCALE from '../../enums/scale';\nimport CURVE from './constants/curve';\n\nimport Chart from '../_base/chart';\n\nimport * as d3 from '../../d3';\n\nimport {\n    getScale\n} from '../../scales';\n\nimport {\n    valueGetAccessor\n} from '@ocula/utilities';\n\nimport type {\n    ILineOptions,\n    ILinePoint\n} from './types';\n\nconst lineGenerator = d3.line<ILinePoint>()\n    .defined(data => !!data.y1)\n    .x(data => data.x)\n    .y(data => data.y1);\n\nconst areaGenerator = d3.area<ILinePoint>()\n    .defined(data => !!data.y1)\n    .x(data => data.x)\n    .y0(data => data.y0)\n    .y1(data => data.y1);\n\nexport default class LineChart extends Chart<ILineOptions> {\n\n    private lineGroup: d3.Selection<SVGGElement, ILinePoint[], null, undefined>;\n    private markerGroup: d3.Selection<SVGGElement, ILinePoint[], null, undefined>;\n    private axisGroup: d3.Selection<SVGGElement, ILinePoint[], null, undefined>;\n    private xAxis: d3.Selection<SVGGElement, ILinePoint[], null, undefined>;\n    private yAxis: d3.Selection<SVGGElement, ILinePoint[], null, undefined>;\n\n    constructor(element: Element) {\n        super(element);\n\n        this.axisGroup = this.canvas.append('g');\n        this.lineGroup = this.canvas.append('g');\n        this.markerGroup = this.canvas.append('g');\n\n        this.xAxis = this.axisGroup.append('g');\n        this.yAxis = this.axisGroup.append('g');\n    }\n\n    protected get defaultOptions(): ILineOptions {\n        return {\n            ...super.defaultOptions,\n\n            type: LINE_TYPE.line,\n            scales: {\n                x: {\n                    type: SCALE.point,\n                    value: item => item.x,\n                    format: value => value\n                },\n                y: {\n                    type: SCALE.linear,\n                    value: item => item.y,\n                    format: value => value\n                }\n            },\n            markers: {\n                visible: true,\n                type: MARKER_TYPE.point\n            },\n            labels: {\n                visible: true,\n                content: value => value.yValue\n            },\n            padding: {\n                top: 32,\n                bottom: 32,\n                left: 0,\n                right: 0\n            },\n            classes: {\n                svg: 'spline-chart__svg',\n                canvas: 'spline-chart__canvas',\n                lineGroup: 'spline-chart__line-group',\n                markerGroup: 'spline-chart__marker-group',\n                line: 'spline-chart__line',\n                area: 'spline-chart__area',\n                marker: 'spline-chart__marker'\n            },\n            colours: {\n                line: '#000000',\n                marker: '#000000',\n                axis: '#000000',\n                tick: '#000000',\n                label: '#000000'\n            }\n        };\n    }\n\n    protected bootstrap(options: ILineOptions) {\n        super.bootstrap(options);\n\n        const {\n            classes,\n            padding\n        } = this.options;\n\n        this.lineGroup.classed(classes.lineGroup, true);\n        this.markerGroup.classed(classes.markerGroup, true);\n\n        this.xAxis.attr('transform', `translate(0, ${padding.top + this.height})`);\n    }\n\n    protected reset() {\n        this.lineGroup.selectAll('*').remove();\n        this.markerGroup.selectAll('*').remove();\n    }\n\n    private async drawMarkers() {\n        const {\n            colours,\n            labels,\n            animation\n        } = this.options;\n\n        const getLabel = valueGetAccessor(labels.content);\n\n        const updates = this.markerGroup.selectAll('g')\n            .data(data => data, item => item.xValue);\n\n        const entries = updates.enter()\n            .append('g')\n            .attr('transform', data => `translate(${data.x}, ${data.y1})`);\n\n        updates.exit().remove();\n\n        entries.append('text')\n            .attr('text-anchor', 'middle')\n            .attr('dominant-baseline', 'middle')\n            .style('fill', 'var(--font__colour)')\n            .style('font-size', 'var(--font__size--small)');\n\n        entries.append('circle')\n            .attr('r', 3);\n\n        const merges = updates.merge(entries);\n\n        merges.select('circle')\n            .attr('fill', colours.marker);\n\n        merges.select('text')\n            .text((data, index) => getLabel(data, index))\n            .attr('dy', data => Math.sign(data.yValue) * -16);\n\n        return updates.transition()\n            .duration(animation.duration)\n            .ease(d3.easePolyOut.exponent(4))\n            .attr('transform', data => `translate(${data.x}, ${data.y1})`);\n    }\n\n    private async drawArea() {\n        const {\n            type,\n            classes,\n            colours,\n            animation\n        } = this.options;\n\n        const curve = CURVE[type] || CURVE.line;\n\n        let area = this.lineGroup.select(`.${classes.area}`);\n\n        if (area.empty()) {\n            area = this.lineGroup.append('path')\n                .classed(classes.area, true)\n                .attr('fill', colours.line)\n                .attr('fill-opacity', 0.5)\n                .attr('d', areaGenerator.curve(curve));\n        }\n\n        return area.transition()\n            .duration(animation.duration)\n            .ease(d3.easePolyOut.exponent(4))\n            .attr('d', areaGenerator.curve(curve))\n            .attr('fill', colours.line)\n            .end();\n    }\n\n    private async drawLine() {\n        const {\n            type,\n            classes,\n            colours,\n            animation\n        } = this.options;\n\n        const curve = CURVE[type] || CURVE.line;\n\n        let line = this.lineGroup.select(`.${classes.line}`);\n\n        if (line.empty()) {\n            line = this.lineGroup.append('path')\n                .classed(classes.line, true)\n                .attr('stroke-width', 2)\n                .attr('stroke', colours.line)\n                .attr('fill', 'none')\n                .attr('d', lineGenerator.curve(curve));\n        }\n\n        return line.transition()\n            .duration(animation.duration)\n            .ease(d3.easePolyOut.exponent(4))\n            .attr('d', lineGenerator.curve(curve))\n            .attr('stroke', colours.line)\n            .end();\n    }\n\n    private drawAxes() {\n        const {\n            colours\n        } = this.options;\n        \n        const {\n            xAxis,\n            yAxis\n        } = this.axisGroup.datum();\n        \n        function styleAxis(group, axis) {\n            group.call(axis);\n\n            if (colours.axis === false) {\n                group.select('.domain').remove();\n            }\n\n            group.select('.domain')\n                .attr('stroke', colours.axis);\n\n            group.selectAll('.tick line')\n                .attr('stroke', colours.tick);\n\n            group.selectAll('.tick text')\n                .attr('fill', colours.label);\n        }\n\n        styleAxis(this.xAxis, xAxis);\n        styleAxis(this.yAxis, yAxis);\n\n        const yScale = yAxis.scale();\n        const yMin = yScale.domain()[0];\n        const translation = yScale(yMin);\n\n        this.xAxis.attr('transform', `translate(0, ${translation})`);\n    }\n\n    private async draw() {\n        return Promise.all([\n            this.drawArea(),\n            this.drawLine(),\n            this.drawMarkers()\n        ]);\n    }\n\n    private getAxis(axisType, scale, options) {\n        const {\n            ticks,\n            format,\n            size\n        } = options;\n\n        const axis = axisType(scale)\n            .tickFormat(format);\n\n        if (ticks && typeof ticks === 'number') {\n            axis.ticks(ticks);\n        }\n\n        if (ticks && typeof ticks === 'function') {\n            axis.tickValues(ticks(scale.domain()));\n        }\n\n        if (size > 0) {\n            axis.tickSize(size);\n        }\n\n        return axis;\n    }\n    \n    private calculate<T>(data: T[]) {\n        const {\n            x: xOptions,\n            y: yOptions\n        } = this.options.scales;\n    \n        const xScale = getScale(data, xOptions, [0, this.width]);\n        const yScale = getScale(data, yOptions, [this.height - 2, 2]);\n\n        const xAxis = this.getAxis(d3.axisBottom, xScale, xOptions);\n        const yAxis = this.getAxis(d3.axisRight, yScale, yOptions);\n\n        const points = data.map(value => {\n            const xValue = xOptions.value(value);\n            const yValue = yOptions.value(value);\n    \n            return {\n                value,\n                xValue,\n                yValue,\n                x: xScale(xValue),\n                y0: yScale(0),\n                y1: yScale(yValue)\n            };\n        });\n\n        this.axisGroup.datum({\n            xAxis,\n            yAxis\n        });\n        \n        this.lineGroup.datum(points);\n        this.markerGroup.datum(points);\n    }\n\n    public async render<T>(data: T[], options: ILineOptions) {\n        if (this.rendering) {\n            this.reset();\n        }\n        \n        this.rendering = true;\n\n        this.bootstrap(options);\n        this.calculate<T>(data);\n\n        try {\n            await this.draw();\n        } finally {\n            this.rendering = false;\n        }\n    }\n\n}"
  },
  {
    "path": "packages/charts/src/charts/line/types/index.ts",
    "content": "import LINE_TYPE from '../enums/line-type';\nimport MARKER_TYPE from '../enums/marker-type';\n\nimport SCALE from '../../../enums/scale';\n\nimport type {\n    IChartOptions\n} from '../../_base/chart';\n\nexport interface ILinePoint<T = any> {\n    value: T;\n    xValue: number;\n    yValue: number;\n    x: number;\n    y0: number;\n    y1: number;\n};\n\nexport interface ILineScaleOptions<T> {\n    type?: SCALE,\n    value?(value: T, index?: number): any;\n    format?(value: any): string;\n}\n\nexport interface ILineOptions<T = any> extends IChartOptions {\n    type?: LINE_TYPE,\n    scales: {\n        x?: ILineScaleOptions<T>;\n        y?: ILineScaleOptions<T>;\n    },\n    markers?: {\n        type?: MARKER_TYPE;\n        visible?: boolean;\n    },\n    labels?: {\n        visible?: boolean;\n        content?(value: ILinePoint<T>, index?: number): any;\n    },\n    classes?: {\n        svg?: string;\n        canvas?: string;\n        lineGroup?: string;\n        markerGroup?: string;\n        line?: string;\n        area?: string;\n        marker?: string;\n    },\n    colours?: {\n        line?: string;\n        marker?: string;\n        axis?: string;\n        tick?: string;\n        label?: string;\n    }\n}"
  },
  {
    "path": "packages/charts/src/d3/index.ts",
    "content": "export * from 'd3-array';\nexport * from 'd3-axis';\nexport * from 'd3-ease';\nexport * from 'd3-path';\nexport * from 'd3-scale';\nexport * from 'd3-selection';\nexport * from 'd3-shape';\nexport * from 'd3-transition';"
  },
  {
    "path": "packages/charts/src/enums/scale.ts",
    "content": "const enum SCALE {\n    linear = 'linear',\n    point = 'point',\n    time = 'time'\n};\n\nexport default SCALE;"
  },
  {
    "path": "packages/charts/src/index.ts",
    "content": "export { default as SCALE_TYPE } from './enums/scale';\n\nexport { default as LINE_TYPE } from './charts/line/enums/line-type';\nexport { default as MARKER_TYPE } from './charts/line/enums/marker-type';\nexport { default as LineChart } from './charts/line';\nexport * from './charts/line/types';"
  },
  {
    "path": "packages/charts/src/scales/index.ts",
    "content": "import SCALE from '../enums/scale';\n\nimport linear from './linear';\nimport point from './point';\nimport time from './time';\n\nexport const SCALES = {\n    [SCALE.linear]: linear,\n    [SCALE.point]: point,\n    [SCALE.time]: time\n};\n\nexport function getScale(data, options, range) {\n    const {\n        type\n    } = options;\n\n    return (SCALES[type] || SCALES[SCALE.linear])(data, options, range);\n}\n\nexport default SCALES;"
  },
  {
    "path": "packages/charts/src/scales/linear.ts",
    "content": "import * as d3 from '../d3';\n\nexport default function(data, options, range) {\n    const {\n        value\n    } = options;\n\n    const extent = d3.extent(data, value);\n    const domain = [Math.min(extent[0], 0), Math.max(extent[1], 0)];\n    \n    return d3.scaleLinear()\n        .domain(domain)\n        .range(range)\n        .nice();\n}"
  },
  {
    "path": "packages/charts/src/scales/point.ts",
    "content": "import * as d3 from '../d3';\n\nexport default function(data, options, range) {\n    const {\n        value\n    } = options;\n\n    const domain = data.map(value);\n\n    return d3.scalePoint()\n        .domain(domain)\n        .range(range);\n}"
  },
  {
    "path": "packages/charts/src/scales/time.ts",
    "content": "import * as d3 from '../d3';\n\nexport default function(data, options, range) {\n    const {\n        value\n    } = options;\n\n    const domain = d3.extent(data, value);\n\n    return d3.scaleTime()\n        .domain(domain)\n        .range(range);\n}"
  },
  {
    "path": "packages/components/package.json",
    "content": "{\n    \"name\": \"@ocula/components\",\n    \"version\": \"1.0.0\",\n    \"main\": \"./src/index.ts\",\n    \"license\": \"MIT\",\n    \"dependencies\": {\n        \"@ocula/event-emitter\": \"1.0.0\",\n        \"@ocula/style\": \"1.0.0\",\n        \"@ocula/task-queue\": \"1.0.0\",\n        \"@ocula/utilities\": \"1.0.0\",\n        \"mapbox-gl\": \"^2.0.0\"\n    },\n    \"peerDependencies\": {\n        \"vue\": \"3.0.5\"\n    },\n    \"devDependencies\": {\n        \"@types/mapbox-gl\": \"^1.12.9\"\n    }\n}\n"
  },
  {
    "path": "packages/components/src/components/accordion/accordion-pane.vue",
    "content": "<template>\n    <transition-box-resize class=\"accordion-pane\">\n        <div class=\"accordion-pane__header\" v-if=\"$slots.header || label\" @click=\"toggle\">\n            <slot name=\"header\">{{ label }}</slot>\n        </div>\n        <div class=\"accordion-pane__body\" v-if=\"isOpen\">\n            <slot></slot>\n        </div>\n    </transition-box-resize>\n</template>\n\n<script lang=\"ts\">\nimport EVENTS from './constants/events';\n\nimport {\n    defineComponent,\n    inject,\n    ref,\n    onBeforeUnmount\n} from 'vue';\n\nimport {\n    stringUniqueId\n} from '@ocula/utilities';\n\nimport {\n    EventEmitter\n} from '@ocula/event-emitter';\n\nexport default defineComponent({\n\n    props: {\n\n        id: {\n            type: [String, Number],\n            default: () => stringUniqueId()\n        },\n\n        label: {\n            type: String\n        }\n\n    },\n    \n    setup(props, { emit }) {\n        const eventEmitter = inject<EventEmitter>('accordion');\n\n        const isOpen = ref(false);\n\n        function open() {\n            isOpen.value = true;\n\n            emit('open');\n            eventEmitter.emit(EVENTS.paneOpened, props.id);\n        }\n\n        function close() {\n            isOpen.value = false;\n\n            emit('close');\n            eventEmitter.emit(EVENTS.paneClosed, props.id);\n        }\n\n        function toggle() {\n            (!!isOpen.value ? close : open)();\n        }\n\n        function closeExcept(id: string | number) {\n            if (id !== props.id) {\n                close();\n            }\n        }\n\n        const listeners = [\n            eventEmitter.on(EVENTS.closeAll, close),\n            eventEmitter.on(EVENTS.closeExcept, closeExcept),\n            eventEmitter.on(EVENTS.openPane, id => id === props.id && open()),\n            eventEmitter.on(EVENTS.closePane, id => id === props.id && close()),\n            eventEmitter.on(EVENTS.togglePane, id => id === props.id && toggle())\n        ];\n\n        onBeforeUnmount(() => listeners.forEach(({ dispose }) => dispose()));\n\n        return {\n            isOpen,\n            toggle\n        };\n    }\n\n});\n</script>"
  },
  {
    "path": "packages/components/src/components/accordion/accordion.vue",
    "content": "<template>\n    <div class=\"accordion\">\n        <slot :open=\"open\" :close=\"close\" :toggle=\"toggle\"></slot>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport EVENTS from './constants/events';\n\nimport {\n    defineComponent,\n    provide,\n    onBeforeUnmount\n} from 'vue';\n\nimport {\n    EventEmitter\n} from '@ocula/event-emitter';\n\nexport default defineComponent({\n\n    props: {\n\n        multiple: {\n            type: Boolean,\n            default: false\n        }\n\n    },\n   \n    setup(props, { emit }) {\n        const eventEmitter = new EventEmitter();\n\n        const listener = eventEmitter.on(EVENTS.paneOpened, (id: string | number) => {\n            if (!props.multiple) {\n                eventEmitter.emit(EVENTS.closeExcept, id);\n            }\n\n            emit(EVENTS.paneOpened, id);\n        });\n\n        function open(id: string | number) {\n            eventEmitter.emit(EVENTS.openPane, id);\n        }\n\n        function close(id: string | number) {\n            eventEmitter.emit(EVENTS.openPane, id);\n        }\n\n        function toggle(id: string | number) {\n            eventEmitter.emit(EVENTS.togglePane, id);\n        }\n\n        provide('accordion', eventEmitter);\n        onBeforeUnmount(() => listener.dispose());\n\n        return {\n            open,\n            close,\n            toggle\n        };\n    }\n\n});\n</script>"
  },
  {
    "path": "packages/components/src/components/accordion/constants/events.ts",
    "content": "export default {\n    openPane: 'open-pane',\n    closePane: 'close-pane',\n    togglePane: 'toggle-pane',\n    paneOpened: 'pane-opened',\n    paneClosed: 'pane-closed',\n    closeAll: 'close-all-panes',\n    closeExcept: 'close-all-panes-except'\n} as const;"
  },
  {
    "path": "packages/components/src/components/block/block.vue",
    "content": "<template>\n    <section class=\"block\">\n        <div class=\"block__header\" layout=\"row center-justify\" v-if=\"$slots.title || title\">\n            <div class=\"block__title\">\n                <slot name=\"title\">{{ title }}</slot>\n            </div>\n            <div class=\"block__secondary\" v-if=\"$slots.secondary\">\n                <slot name=\"secondary\"></slot>\n            </div>\n        </div>\n        <div class=\"block__body\">\n            <slot></slot>\n        </div>\n    </section>\n</template>\n\n<script lang=\"ts\">\nimport {\n    defineComponent\n} from 'vue';\n\nexport default defineComponent({\n    \n    props: {\n\n        title: {\n            type: String\n        }\n\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .block__header,\n    .block__body {\n        padding: var(--spacing__small);\n    }\n\n    .block__title {\n        font-size: var(--font__size--small);\n        font-weight: var(--font__weight--heavy);\n        text-transform: uppercase;\n    }\n\n</style>"
  },
  {
    "path": "packages/components/src/components/container/container.vue",
    "content": "<template>\n    <div class=\"container\">\n        <slot></slot>\n    </div>\n</template>\n\n<style lang=\"scss\">\n\n    .container {\n        width: 100%;\n        max-width: 1200px;\n        margin: 0 auto;\n    }\n\n</style>"
  },
  {
    "path": "packages/components/src/components/core/confirm-modal.vue",
    "content": "<template>\n    <modal :id=\"id\" class=\"confirm-modal\" @open=\"onOpen\">\n        <template #default=\"{ close, cancel }\">\n            <div class=\"margin__bottom--large\">{{ message }}</div>\n            <div class=\"confirm-modal__actions\" layout=\"row center-right\">\n                <button class=\"button margin__right--small\" @click=\"cancel\">{{ cancelLabel }}</button>\n                <button class=\"button button--primary\" @click=\"close\">{{ confirmLabel }}</button>\n            </div>\n        </template>\n    </modal>\n</template>\n\n<script lang=\"ts\">\nimport MODALS from '../../constants/modals';\n\nimport Modal from '../modal/modal.vue';\n\nimport {\n    defineComponent,\n    ref\n} from 'vue';\n\nimport type {\n    IConfirmModalPayload\n} from '../../types';\n\nexport default defineComponent({\n\n    components: {\n        Modal\n    },\n\n    setup() {\n        const id = MODALS.confirm;\n        const message = ref('');\n        const confirmLabel = ref('');\n        const cancelLabel = ref('');\n\n        function onOpen(payload: IConfirmModalPayload): void {\n            message.value = payload.message;\n\n            confirmLabel.value = payload.confirmLabel || 'Ok';\n            cancelLabel.value = payload.cancelLabel || 'Cancel';\n        }\n\n        return {\n            id,\n            onOpen,\n            message,\n            confirmLabel,\n            cancelLabel\n        };\n    }\n\n});\n</script>"
  },
  {
    "path": "packages/components/src/components/core/index.vue",
    "content": "<template>\n    <confirm-modal />\n</template>\n\n<script lang=\"ts\">\nimport ConfirmModal from './confirm-modal.vue';\n\nimport {\n    defineComponent\n} from 'vue';\n\nexport default defineComponent({\n    \n    components: {\n        ConfirmModal\n    }\n\n});\n</script>"
  },
  {
    "path": "packages/components/src/components/drawer/drawer.vue",
    "content": "<template>\n    <transition name=\"drawer\">\n        <div class=\"drawer\" v-if=\"isOpen\" @click.self=\"close()\">\n            <aside class=\"drawer__panel\" :class=\"panelClass\">\n                <slot :open=\"open\" :close=\"close\"></slot>\n            </aside>\n        </div>\n    </transition>\n</template>\n\n<script lang=\"ts\">\nimport {\n    defineComponent,\n    computed\n} from 'vue';\n\nimport useLayer from '../../compositions/layer';\n\nexport default defineComponent({\n\n    props: {\n\n        id: {\n            type: String\n        },\n\n        position: {\n            type: String,\n            default: 'left'\n        }\n\n    },\n\n    setup(props, context) {\n        const {\n            isOpen,\n            open,\n            close\n        } = useLayer(props.id, context);\n\n        const panelClass = computed(() => `drawer__panel--${props.position}`);\n\n        return {\n            isOpen,\n            open,\n            close,\n            panelClass\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .drawer {\n        position: fixed;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n        overflow: hidden;\n        background-color: rgba(0, 0, 0, 0.5);\n        z-index: 100;\n    }\n\n    .drawer__panel {\n        position: absolute;\n        background-color: var(--background__colour);\n    }\n\n    .drawer__panel--left,\n    .drawer__panel--right {\n        top: 0;\n        height: 100%;\n        max-width: 80%;\n    }\n\n    .drawer__panel--top,\n    .drawer__panel--bottom {\n        left: 0;\n        width: 100%;\n        max-height: 80%;\n    }\n\n    .drawer__panel--left {\n        left: 0;\n    }\n\n    .drawer__panel--right {\n        right: 0;\n    }\n\n    .drawer__panel--top {\n        top: 0;\n    }\n\n    .drawer__panel--bottom {\n        bottom: 0;\n    }\n\n    .drawer-enter-from,\n    .drawer-leave-to {\n        background-color: transparent;\n\n        & .drawer__panel--left {\n            transform: translateX(-100%);\n        }\n\n        & .drawer__panel--right {\n            transform: translateX(100%);\n        }\n\n        & .drawer__panel--top {\n            transform: translateY(-100%);\n        }\n\n        & .drawer__panel--bottom {\n            transform: translateY(100%);\n        }\n    }\n\n    .drawer-enter-active,\n    .drawer-leave-active {\n        transition: background var(--transition__timing--long) var(--transition__easing--default);\n\n        & .drawer__panel {\n            transition: transform var(--transition__timing--long) var(--transition__easing--default);\n        }\n    }\n\n</style>"
  },
  {
    "path": "packages/components/src/components/icon/icon.vue",
    "content": "<template>\n    <svg class=\"icon\" :class=\"className\">\n        <use v-bind:xlink:href=\"href\"/>\n    </svg>\n</template>\n\n<script lang=\"ts\">\nimport {\n    defineComponent,\n    computed\n} from 'vue';\n\nexport default defineComponent({\n\n    props: {\n\n        name: {\n            type: String\n        }\n\n    },\n\n    setup(props) {\n        const href = computed(() => `/icons.svg#${props.name}`);\n        const className = computed(() => `icon--${props.name}`);\n    \n        return {\n            href,\n            className\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .icon {\n        display: inline-block;\n        width: 1.5em;\n        height: 1.5em;\n        fill: currentColor;\n        stroke: none;\n        vertical-align: middle;\n    }\n\n</style>"
  },
  {
    "path": "packages/components/src/components/icon-button/icon-button.vue",
    "content": "<template>\n    <div class=\"icon-button\" :class=\"buttonClass\" layout=\"row center-left\" v-bind=\"$attrs\">\n        <icon class=\"icon-button__icon\" :name=\"icon\"></icon>\n        <div class=\"icon-button__label\" self=\"size-x1\" v-if=\"$slots.default\">\n            <slot></slot>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport Icon from '../icon/icon.vue';\n\nimport {\n    defineComponent,\n    computed,\n    PropType\n} from 'vue';\n\ntype Layout = 'horizontal' | 'vertical';\n\nexport default defineComponent({\n\n    components: {\n        Icon\n    },\n    \n    props: {\n\n        icon: {\n            type: String\n        },\n\n        layout: {\n            type: String as PropType<Layout>,\n            default: 'horizontal'\n        }\n\n    },\n\n    setup(props) {\n        const buttonClass = computed(() => `icon-button--${props.layout}`);\n\n        return {\n            buttonClass\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .icon-button {\n        display: inline-flex;\n        width: auto;\n        padding: var(--spacing__x-small);\n        cursor: pointer;\n    }\n\n    .icon-button__label {\n        margin: 0 0 0 var(--spacing__x-small);\n    }\n\n    .icon-button--vertical {\n        flex-direction: column;\n        align-items: center;\n\n        & .icon-button__label {\n            margin: var(--spacing__x-small) 0 0 0;\n        }\n    }\n\n    @media (hover: hover) {\n\n        .icon-button {\n            border-radius: var(--border__radius);\n\n            &:hover {\n                background-color: var(--background__colour--hover);\n            }\n        }\n\n    }\n\n</style>"
  },
  {
    "path": "packages/components/src/components/icon-label/icon-label.vue",
    "content": "<template>\n    <div class=\"icon-label\" layout=\"row center-left\">\n        <icon class=\"icon-label__icon\" :name=\"icon\"></icon>\n        <div class=\"icon-label__label\" v-if=\"$slots.default\">\n            <slot></slot>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport Icon from '../icon/icon.vue';\n\nimport {\n    defineComponent\n} from 'vue';\n\nexport default defineComponent({\n\n    components: {\n        Icon\n    },\n    \n    props: {\n\n        icon: {\n            type: String\n        }\n\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .icon-label {\n        display: inline-flex;\n        width: auto;\n    }\n\n    .icon-label__label {\n        margin-left: var(--spacing__x-small);\n    }\n\n</style>"
  },
  {
    "path": "packages/components/src/components/index.ts",
    "content": "import Accordion from './accordion/accordion.vue';\nimport AccordionPane from './accordion/accordion-pane.vue';\nimport Block from './block/block.vue';\nimport Container from './container/container.vue';\nimport Drawer from './drawer/drawer.vue';\nimport Icon from './icon/icon.vue';\nimport IconButton from './icon-button/icon-button.vue';\nimport IconLabel from './icon-label/icon-label.vue';\nimport MapboxMap from './mapbox/mapbox-map.vue';\nimport MapboxLegend from './mapbox/mapbox-legend.vue';\nimport MapboxRasterLayer from './mapbox/mapbox-raster-layer.vue';\nimport Layout from './layout/layout.vue';\nimport Loader from './loader/loader.vue';\nimport Modal from './modal/modal.vue';\nimport SearchBox from './search-box/search-box.vue';\nimport TransitionBoxResize from './transitions/box-resize.vue';\n\nimport CoreComponents from './core/index.vue';\n\nexport default {\n    Accordion,\n    AccordionPane,\n    Block,\n    Container,\n    Drawer,\n    Icon,\n    IconButton,\n    IconLabel,\n    MapboxMap,\n    MapboxLegend,\n    MapboxRasterLayer,\n    Layout,\n    Loader,\n    Modal,\n    SearchBox,\n    TransitionBoxResize,\n    CoreComponents\n};"
  },
  {
    "path": "packages/components/src/components/layout/layout.vue",
    "content": "<template>\n    <div name=\"layout\" class=\"layout\">\n        <header class=\"layout__header\" v-if=\"$slots.header && header\">\n            <slot name=\"header\"></slot>\n        </header>\n        <aside class=\"layout__sidebar layout__sidebar--left\" v-if=\"$slots.sidebarLeft && sidebarLeft\">\n            <slot name=\"sidebarLeft\"></slot>\n        </aside>\n        <div class=\"layout__body\">\n            <slot></slot>\n        </div>\n        <aside class=\"layout__sidebar layout__sidebar--right\" v-if=\"$slots.sidebarRight && sidebarRight\">\n            <slot name=\"sidebarRight\"></slot>\n        </aside>\n        <footer class=\"layout__footer\" v-if=\"$slots.footer && footer\">\n            <slot name=\"footer\"></slot>\n        </footer>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport {\n    defineComponent\n} from 'vue';\n\nexport default defineComponent({\n    \n    props: {\n\n        header: {\n            type: Boolean,\n            default: false\n        },\n\n        sidebarLeft: {\n            type: Boolean,\n            default: false\n        },\n\n        sidebarRight: {\n            type: Boolean,\n            default: false\n        },\n\n        footer: {\n            type: Boolean,\n            default: false\n        }\n\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .layout {\n        display: grid;\n        width: 100%;\n        height: 100%;\n        margin: 0;\n        padding: 0;\n        overflow: hidden;\n        grid-template-columns: auto 1fr auto;\n        grid-template-rows: auto 1fr auto;\n        grid-template-areas: \n            \"header header header\"\n            \"sidebar-left body sidebar-right\"\n            \"footer footer footer\";\n    }\n\n    .layout__header {\n        grid-area: header;\n    }\n\n    .layout__body {\n        grid-area: body;\n        overflow: hidden;\n        overflow-y: auto;\n        -webkit-overflow-scrolling: touch;\n    }\n\n    .layout__footer {\n        grid-area: footer;\n    }\n\n    .layout__sidebar--left {\n        grid-area: sidebar-left;\n    }\n\n    .layout__sidebar--right {\n        grid-area: sidebar-right;\n    }\n\n</style>"
  },
  {
    "path": "packages/components/src/components/loader/loader.vue",
    "content": "<template>\n    <div class=\"loader\" :class=\"className\"></div>\n</template>\n\n<script lang=\"ts\">\nimport {\n    defineComponent,\n    computed\n} from 'vue';\n\nexport default defineComponent({\n\n    props: {\n\n        disabled: {\n            type: Boolean,\n            default: false\n        }\n\n    },\n\n    setup(props) {\n        const className = computed(() => ({\n            'loader--loading': !props.disabled\n        }));\n\n        return {\n            className\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .loader {\n        display: inline-block;\n        position: relative;\n        width: 1.5em;\n        height: 1.5em;\n        vertical-align: middle;\n\n        &:before,\n        &:after {\n            display: block;\n            content: '';\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            border-width: 2px;\n            border-style: solid;\n            border-radius: 50%;\n        }\n\n        &:before {\n            border-color: var(--background__colour--hover);\n        }\n\n        &:after {\n            border-color: transparent;\n        }\n\n    }\n\n    .loader--loading {\n\n        &:after {\n            animation: loader 500ms infinite linear;\n            border-right-color: var(--colour__primary);\n        }\n    }\n\n    @keyframes loader {\n\n        0% {\n            transform: rotate(-90deg);\n        }\n\n        100% {\n            transform: rotate(270deg);\n        }\n\n    }\n\n</style>"
  },
  {
    "path": "packages/components/src/components/mapbox/compositions/layer.ts",
    "content": "import {\n    inject,\n    watch,\n    onMounted,\n    onBeforeUnmount,\n    PropType\n} from 'vue';\n\nimport {\n    stringUniqueId\n} from '@ocula/utilities';\n\nimport type {\n    IInteractiveMap\n} from '../types';\n\nimport type {\n    Layer,\n    Layout,\n    AnyPaint\n} from 'mapbox-gl';\n\nexport const layerProps = {\n\n    id: {\n        type: String,\n        default: () => stringUniqueId()\n    },\n\n    minzoom: {\n        type: Number,\n        default: 0\n    },\n    \n    maxzoom: {\n        type: Number,\n        default: 22\n    },\n\n    layout: {\n        type: Object as PropType<Layout>\n    },\n\n    paint: {\n        type: Object as PropType<AnyPaint>\n    }\n    \n};\n\nexport function useLayer(props: any, layer: Layer) {\n    const map = inject<IInteractiveMap>('map');\n      \n    watch(() => props.layout, value => map.updateLayout(layer.id, value));\n    watch(() => props.paint, value => map.updatePaint(layer.id, value));\n\n    onMounted(() => map.addLayer(layer));\n    onBeforeUnmount(() => map.removeLayer(layer));\n\n    return {\n        map\n    };\n}"
  },
  {
    "path": "packages/components/src/components/mapbox/mapbox-legend.vue",
    "content": "<template>\n    <div class=\"mapbox-legend\" :class=\"legendClass\">\n        <div class=\"mapbox-legend__header\" layout=\"row center-left\">\n            <icon-button :icon=\"buttonIcon\" v-tooltip:right=\"legendToolip\" @click=\"toggleOpen\"></icon-button>\n            <div class=\"margin__left--xx-small\" v-show=\"isOpen\">Legend</div>\n        </div>\n        <transition-box-resize class=\"mapbox-legend__body\">\n            <div class=\"mapbox-legend__legend\" v-if=\"isOpen\">\n                <slot></slot>\n            </div>\n        </transition-box-resize>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport {\n    defineComponent,\n    ref,\n    computed\n} from 'vue';\n\nexport default defineComponent({\n    \n    setup() {\n        const isOpen = ref(false);\n\n        const legendClass = computed(() => isOpen.value && 'mapbox-legend--open');\n        const legendToolip = computed(() => `${isOpen.value ? 'Hide' : 'Show'} Legend`);\n        const buttonIcon = computed(() => isOpen.value ? 'close-line' : 'information-line');''\n\n        function toggleOpen(): void {\n            isOpen.value = !isOpen.value;\n        }\n\n        return {\n            isOpen,\n            legendClass,\n            legendToolip,\n            buttonIcon,\n            toggleOpen\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .mapbox-legend {\n        position: absolute;\n        top: var(--spacing__small);\n        left: var(--spacing__small);\n        padding: var(--spacing__x-small);\n        background: none;\n        border-radius: var(--border__radius);\n        z-index: 1;\n    }\n\n    .mapbox-legend--open {\n        max-height: calc(100% - var(--spacing__large));\n        background: var(--background__colour);\n        box-shadow: 0 2px 4px rgba(30, 30, 30, 0.1);\n        overflow: hidden;\n        overflow-y: auto;\n    }\n\n    .mapbox-legend__header {\n        border-radius: var(--border__radius);\n        background-color: var(--background__colour);\n    }\n\n    .mapbox-legend__legend {\n        padding: var(--spacing__x-small);\n    }\n\n</style>"
  },
  {
    "path": "packages/components/src/components/mapbox/mapbox-map.vue",
    "content": "<template>\n    <div class=\"map\" ref=\"element\">\n        <loader class=\"map__loader\" v-if=\"loading\" />\n        <slot></slot>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport Loader from '../loader/loader.vue';\n\nimport getListeners from '../../helpers/get-listeners';\n\nimport {\n    TaskQueue\n} from '@ocula/task-queue';\n\nimport {\n    defineComponent,\n    ref,\n    watch,\n    onMounted,\n    provide,\n    PropType,\n    onBeforeUnmount\n} from 'vue';\n\nimport type {\n    AnyPaint,\n    Layer,\n    Layout\n} from 'mapbox-gl';\n\nimport {\n    functionDebounce\n} from '@ocula/utilities';\n\ndeclare class ResizeObserver {\n\n    constructor(callback: Function);\n\n    public observe(element: Element);\n    public unobserve(element: Element);\n    public disconnect(): void;\n}\n\nconst STYLE = {\n    light: 'light-v10',\n    dark: 'dark-v10',\n    streets: 'streets-v10'\n};\n\nexport default defineComponent({\n\n    props: {\n\n        latitude: {\n            type: Number\n        },\n\n        longitude: {\n            type: Number\n        },\n\n        zoom: {\n            type: Number,\n            default: 6\n        },\n\n        pitch: {\n            type: Number,\n            default: 0\n        },\n\n        style: {\n            type: String as PropType<keyof typeof STYLE>,\n            default: 'light',\n            //validator: value => value in STYLE\n        },\n\n        interactive: {\n            type: Boolean,\n            default: true\n        }\n\n    },\n\n    setup(props, { attrs }) {\n        let map: mapboxgl.Map;\n\n        const element = ref<HTMLElement>(null);\n        const listeners = getListeners(attrs);\n        const taskQueue = new TaskQueue(true);\n        const loading = ref(false);\n\n        async function loadMapbox() {\n            loading.value = true;\n\n            try {\n                const [\n                    mapboxModule\n                ] = await Promise.all([\n                    import(\n                    /* webpackChunkName: 'mapbox' */\n                    /* webpackPrefetch: true */\n                    'mapbox-gl'),\n                    import(\n                    /* webpackChunkName: 'mapbox' */\n                    /* webpackPrefetch: true */\n                    'mapbox-gl/dist/mapbox-gl.css'),\n                ]);\n                    \n                const mapboxgl = mapboxModule.default;\n\n                mapboxgl.accessToken = process.env.MAPBOX_API_KEY;\n\n                return mapboxgl;\n            } finally {\n                loading.value = false;\n            }\n        }\n\n        function updateStyle(style): void {\n            map.setStyle(`mapbox://styles/mapbox/${STYLE[style]}`);            \n        }\n\n        function handleListeners(invokee: Function) {\n            Object.keys(listeners).forEach(key => {\n                invokee.call(map, key, event => listeners[key](event, map));\n            });\n        }\n        \n        function addLayer(layer: Layer): void {\n            const task = () => map.addLayer(layer);\n\n            if (!map) {\n                return taskQueue.add(task);\n            }\n\n            task();\n        }\n\n        function removeLayer(layer: Layer): void {\n            const task = () => {\n                map.removeLayer(layer.id);\n                map.removeSource(layer.id);\n            };\n\n            if (!map) {\n                return taskQueue.add(task);\n            }\n\n            task();\n        }\n\n        function invokeLayerUpdate(invokee: string, layerId: string, obj: Record<string, any>): void {\n            if (!map) {\n                return;\n            }\n\n            for (const key in obj) {\n                map[invokee].call(map, layerId, key, obj[key]);\n            }\n        }\n\n        function updateLayout(layerId: string, layout: Layout): void {\n            invokeLayerUpdate('setLayoutProperty', layerId, layout);\n        }\n\n        function updatePaint(layerId: string, paint: AnyPaint): void {\n            invokeLayerUpdate('setPaintProperty', layerId, paint);\n        }\n\n        function on(event: string, callback: (event: any) => void) {\n            map && map.on(event, callback);\n        }\n\n        function off(event: string, callback: (event: any) => void) {\n            map && map.off(event, callback);\n        }\n\n        function once(event: string, callback: (event: any) => void) {\n            map && map.once(event, callback);\n        }\n\n        function resize() {\n            map && map.resize();\n        }\n\n        function updateLocation() {\n            if (!map) {\n                return;\n            }\n\n            map.easeTo({\n                zoom: props.zoom,\n                pitch: props.pitch,\n                center: [\n                    props.longitude,\n                    props.latitude\n                ]\n            });\n        }\n\n        const resizeMap = functionDebounce(resize, 300, {\n            leading: true,\n            trailing: true\n        });\n\n        const resizeObserver = new ResizeObserver(resizeMap);\n\n        onMounted(async () => {\n            const mapboxgl = await loadMapbox();\n\n            map = new mapboxgl.Map({\n                container: element.value,\n                style: `mapbox://styles/mapbox/${STYLE[props.style]}`,\n                zoom: props.zoom,\n                pitch: props.pitch,\n                interactive: props.interactive,\n                center: [\n                    props.longitude,\n                    props.latitude\n                ]\n            });\n\n            map.on('load', () => {\n                taskQueue.run();\n            });\n\n            handleListeners(map.on);\n            resizeObserver.observe(element.value);\n        });\n\n        onBeforeUnmount(() => {\n            handleListeners(map.off);\n            resizeObserver.disconnect();\n        });\n\n        watch([\n            () => props.latitude,\n            () => props.longitude,\n            () => props.zoom,\n            () => props.pitch\n        ], updateLocation);\n\n        watch(() => props.style, updateStyle);\n\n        provide('map', {\n            addLayer,\n            removeLayer,\n            updateLayout,\n            updatePaint\n        });\n\n        return {\n            element,\n            loading,\n            on,\n            off,\n            once,\n            resize,\n            updateLocation\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .map {\n        position: relative;\n        width: 100%;\n        height: 100%;\n        overflow: hidden;\n    }\n\n    .map__loader {\n        position: absolute;\n        top: 50%;\n        left: 50%;\n        transform: translate(-50%, -50%);\n        z-index: 1;\n    }\n\n</style>"
  },
  {
    "path": "packages/components/src/components/mapbox/mapbox-raster-layer.vue",
    "content": "<script lang=\"ts\">\nimport {\n    layerProps,\n    useLayer\n} from './compositions/layer';\n\nimport {\n    defineComponent,\n    PropType\n} from 'vue';\n\nimport type {\n    Layer\n} from 'mapbox-gl';\n\nexport default defineComponent({\n\n    props: {\n\n        ...layerProps,\n\n        tiles: {\n            type: Array as PropType<string[]>\n        },\n\n        tileSize: {\n            type: Number,\n            default: 256\n        }\n\n    },\n   \n    setup(props, { attrs }) {\n        const {\n            tiles,\n            tileSize,\n            ...base\n        } = props;\n\n        const layer: Layer = {\n            ...base,\n            type: 'raster',\n            source: {\n                tiles,\n                tileSize,\n                type: 'raster',\n            }\n        };\n\n        useLayer(props, layer);\n    },\n\n    render() {\n        return null;\n    }\n\n});\n</script>"
  },
  {
    "path": "packages/components/src/components/mapbox/types/index.ts",
    "content": "import type {\n    AnyPaint,\n    Layer,\n    Layout\n} from 'mapbox-gl';\n\nexport interface IInteractiveMap {\n    addLayer(layer: Layer): void;\n    removeLayer(layer: Layer): void;\n    updateLayout(layerId: string, layout: Layout): void;\n    updatePaint(layerId: string, paint: AnyPaint): void;\n}"
  },
  {
    "path": "packages/components/src/components/modal/modal.vue",
    "content": "<template>\n    <transition name=\"modal\">\n        <div class=\"modal\" layout=\"row center-center\" v-if=\"isOpen\" @click.self.stop=\"close()\">\n            <div class=\"modal__body\" :self=\"size\">\n                <slot :open=\"open\" :close=\"close\" :cancel=\"cancel\"></slot>\n            </div>\n        </div>\n    </transition>\n</template>\n\n<script lang=\"ts\">\nimport {\n    defineComponent\n} from 'vue';\n\nimport useLayer from '../../compositions/layer';\n\nexport default defineComponent({\n\n    props: {\n\n        id: {\n            type: String\n        },\n\n        size: {\n            type: String,\n            default: 'size-small'\n        } \n\n    },\n\n    setup(props, context) {\n        const {\n            isOpen,\n            close,\n            cancel\n        } = useLayer(props.id, context);\n\n        return {\n            isOpen,\n            open,\n            close,\n            cancel\n        };\n    }\n});\n</script>\n\n<style lang=\"scss\">\n\n    .modal {\n        position: fixed;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n        padding: var(--spacing__medium);\n        background-color: rgba(0, 0, 0, 0.5);\n        overflow: hidden;\n        z-index: 100;\n    }\n\n    .modal__body {\n        max-height: 100%;\n        padding: var(--spacing__medium);\n        background-color: var(--background__colour);\n        border-radius: var(--border__radius--large);\n        overflow-y: auto;\n        -webkit-overflow-scrolling: touch;\n    }\n\n    .modal-enter-from,\n    .modal-leave-to {\n        opacity: 0;\n\n        & .modal__body {\n            transform: scale(0.75, 0.75);\n        }\n    }\n\n    .modal-enter-active,\n    .modal-leave-active {\n        transition: opacity var(--transition__timing) var(--transition__easing--default);\n\n        & .modal__body {\n            transition: transform var(--transition__timing) var(--transition__easing--default);\n        }\n    }\n\n</style>"
  },
  {
    "path": "packages/components/src/components/search-box/search-box.vue",
    "content": "<template>\n    <div class=\"search-box\" layout=\"row center-justify\">\n        <icon name=\"search-line\" class=\"margin__right--small\"/>\n        <div self=\"size-x1\">\n            <input type=\"text\" class=\"search-box__input\" autocomplete=\"off\" :value=\"content\" @input=\"content = $event.target.value\" v-bind=\"$attrs\">\n        </div>\n        <loader class=\"margin__left--small\" v-show=\"loading\"/>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport {\n    defineComponent,\n    computed\n} from 'vue';\n\nexport default defineComponent({\n    \n    props: {\n\n        modelValue: {\n            type: null\n        },\n\n        loading: {\n            type: Boolean,\n            default: false\n        }\n\n    },\n\n    setup(props, { emit }) {\n        const content = computed({\n            get() {\n                return props.modelValue;\n            },\n            set(value) {\n                emit('update:modelValue', value);\n            }\n        });\n\n        return {\n            content\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .search-box {\n        display: inline-flex;\n        padding: var(--spacing__x-small) var(--spacing__small);\n        border: 1px solid var(--border__colour);\n        border-radius: var(--border__radius);\n\n        & .search-box__input {\n            width: 100%;\n            padding: 0;\n            border: none;\n        }\n    }\n\n</style>"
  },
  {
    "path": "packages/components/src/components/transitions/box-resize.vue",
    "content": "<template>\n    <div ref=\"element\"\n        class=\"transition-box-resize\"\n        @transitionend.self.passive=\"reset\">\n        <slot></slot>\n    </div>\n</template>\n\n<script lang=\"ts\">\nimport {\n    defineComponent,\n    ref,\n    onBeforeUpdate,\n    computed\n} from 'vue';\n\ninterface IBox {\n    width: number;\n    height: number;\n}\n\nconst CLASSES = {\n    enter: 'transition-box-resize-enter',\n    leave: 'transition-box-resize-leave',\n    enterActive: 'transition-box-resize-enter-active',\n    leaveActive: 'transition-box-resize-leave-active'\n};\n\nfunction isSameBox(a: IBox, b: IBox): boolean {\n    return a.width === b.width && a.height === b.height;\n}\n\nexport default defineComponent({\n   \n    setup(props) {\n        let isUpdating = false;\n\n        const element = ref<HTMLElement>(null);\n\n        let before: IBox,\n            after: IBox;\n\n        function setStyle(property: keyof CSSStyleDeclaration, value: any): void {\n            element.value.style[property as string] = value;\n        }\n\n        function reset(): void {\n            element.value.classList.remove(\n                CLASSES.enter,\n                CLASSES.leave,\n                CLASSES.enterActive,\n                CLASSES.leaveActive,\n            );\n\n            setStyle('width', null);\n            setStyle('height', null);\n            \n            isUpdating = false;\n        }\n\n        function getSnapshot(): IBox {\n            return {\n                width: element.value.scrollWidth,\n                height: element.value.scrollHeight\n            };\n        }\n\n        function update(): void {\n            reset();\n\n            element.value.classList.add(CLASSES.enter, CLASSES.leave);\n            \n            after = getSnapshot();\n\n            if (isSameBox(before, after)) {\n                return reset();\n            }\n            \n            setStyle('width', `${before.width}px`);\n            setStyle('height', `${before.height}px`);\n\n            element.value.classList.add(CLASSES.enterActive, CLASSES.leaveActive);\n\n            requestAnimationFrame(() => {                \n                setStyle('width', `${after.width}px`);\n                setStyle('height', `${after.height}px`);\n            });\n        }\n\n        function beforeUpdate(): void {\n            if (isUpdating) {\n                return;\n            }\n\n            isUpdating = true;\n\n            before = getSnapshot();\n            requestAnimationFrame(update);\n        }\n\n        onBeforeUpdate(beforeUpdate);\n\n        return {\n            element,\n            reset,\n            onBeforeUpdate: beforeUpdate\n        };\n    }\n\n});\n</script>\n\n<style lang=\"scss\">\n\n    .transition-box-resize-enter,\n    .transition-box-resize-leave {\n        overflow: hidden !important;\n    }\n\n    .transition-box-resize-enter-active,\n    .transition-box-resize-leave-active {\n        transition: width var(--transition__timing--long) var(--transition__easing--default),\n                    height var(--transition__timing--long) var(--transition__easing--default);\n    }\n\n</style>"
  },
  {
    "path": "packages/components/src/compositions/layer.ts",
    "content": "import eventEmitter from '../event-emitter';\n\nimport {\n    ref,\n    onBeforeMount,\n    onUnmounted\n} from 'vue';\n\nimport type {\n    IPromisePayload\n} from '../types';\n\nimport type {\n    IListener\n} from '@ocula/event-emitter';\n\nimport type {\n    SetupContext\n} from '@vue/runtime-core';\n\nexport default function(id: string, { emit }: SetupContext) {\n    const isOpen = ref(false);\n\n    let promise: IPromisePayload;\n\n    function open(payload: any, promisePayload: IPromisePayload) {\n        emit('open', payload);\n\n        promise = promisePayload;\n        isOpen.value = true;\n    }\n    \n    function close(payload: any) {\n        emit('close', payload);\n\n        if (promise) {\n            promise.resolve(payload);\n            promise = null;\n        }\n        \n        isOpen.value = false;\n    }\n\n    function cancel(payload: any) {\n        emit('cancel', payload);\n\n        if (promise) {\n            promise.reject(payload);\n            promise = null;\n        }\n\n        isOpen.value = false;\n    }\n\n    let listeners = [] as IListener[];\n\n    onBeforeMount(() => {\n        listeners = [\n            eventEmitter.on(`open:${id}`, open),\n            eventEmitter.on(`close:${id}`, close),\n        ];\n    });\n\n    onUnmounted(() => {\n        listeners.forEach(({ dispose }) => dispose());\n\n        if (promise) {\n            promise.reject();\n        }\n    });\n\n    return {\n        isOpen,\n        open,\n        close,\n        cancel\n    };\n}"
  },
  {
    "path": "packages/components/src/compositions/subscriber.ts",
    "content": "import eventEmitter from '@ocula/event-emitter';\n\nimport {\n    onBeforeMount,\n    onUnmounted\n} from 'vue';\n\nexport default function(event: string, callback: Function): void {\n    onBeforeMount(() => eventEmitter.on(event, callback));\n    onUnmounted(() => eventEmitter.off(event, callback));\n}"
  },
  {
    "path": "packages/components/src/compositions/timer.ts",
    "content": "import {\n    onBeforeMount,\n    onUnmounted\n} from 'vue';\n\ntype Timer = 'interval' | 'timeout';\n\ninterface ITimerApplication {\n    set(handler: Function, timeout: number, ...args: any[]): number;\n    clear(handle: number): void;\n}\n\nconst TIMER = {\n    interval: {\n        set: (handler: Function, timeout: number, immediateInvoke: boolean = true) => {\n            if (immediateInvoke) {\n                handler();\n            }\n            \n            return window.setInterval(handler, timeout);\n        },\n        clear: window.clearInterval\n    },\n    timeout: {\n        set: window.setTimeout,\n        clear: window.clearTimeout\n    }\n} as Record<Timer, ITimerApplication>\n\nexport default function useTimer(handler: Function, timeout: number, timer: Timer = 'interval'): number {\n    let handle;\n\n    const timerApplication = TIMER[timer];\n\n    onBeforeMount(() => handle = timerApplication.set.call(window, handler, timeout));\n    onUnmounted(() => timerApplication.clear.call(window, handle));\n\n    return handle\n}"
  },
  {
    "path": "packages/components/src/constants/modals.ts",
    "content": "export default {\n    confirm: 'modals:confirm'\n} as const;"
  },
  {
    "path": "packages/components/src/controllers/components.ts",
    "content": "import MODALS from '../constants/modals';\n\nimport eventEmitter from '../event-emitter';\n\nimport type {\n    IConfirmModalPayload\n} from '../types';\n\nclass ComponentsController {\n\n    public async open<T = any>(id: string, payload?: any): Promise<T> {\n        return new Promise((resolve, reject) => {\n            eventEmitter.emit(`open:${id}`, payload, { resolve, reject });\n        });\n    }\n\n    public close(id: string, payload?: any): void {\n        eventEmitter.emit(`close:${id}`, payload);\n    }\n\n    public async confirm(payload: IConfirmModalPayload): Promise<void> {\n        return this.open(MODALS.confirm, payload);\n    }\n\n}\n\nexport default new ComponentsController();"
  },
  {
    "path": "packages/components/src/directives/focus.ts",
    "content": "import {\n    Directive\n} from 'vue';\n\nexport default {\n\n    mounted(el, binding) {\n        const {\n            value,\n            modifiers\n        } = binding;\n\n        const query = value || 'input, select, textarea';\n        const element = el.querySelector<HTMLInputElement>(query) || el;\n\n        if (!element) {\n            return;\n        }\n\n        element.focus();\n\n        if (modifiers.highlight) {\n            element.setSelectionRange(0, element.value.length);\n        }\n    }\n\n} as Directive<HTMLInputElement, string>;"
  },
  {
    "path": "packages/components/src/directives/index.ts",
    "content": "import focus from './focus';\nimport meta from './meta';\nimport tooltip from './tooltip';\nimport visible from './visible';\n\nexport default {\n    focus,\n    meta,\n    tooltip,\n    visible\n};"
  },
  {
    "path": "packages/components/src/directives/meta.ts",
    "content": "import {\n    Directive\n} from 'vue';\n\nimport {\n    domSetMeta\n} from '@ocula/utilities';\n\nexport default {\n\n    mounted(element, { arg, value }) {\n        domSetMeta(arg, value);\n    },\n\n    updated(element, { arg, value }) {\n        domSetMeta(arg, value);\n    },\n\n    unmounted(element, { arg }) {\n        domSetMeta(arg);\n    }\n\n} as Directive<HTMLInputElement, string>;"
  },
  {
    "path": "packages/components/src/directives/tooltip.ts",
    "content": "import {\n    Directive\n} from 'vue';\n\nfunction upsert(element: HTMLElement, binding) {\n    const {\n        value,\n        arg = 'top'\n    } = binding;\n    \n    element.setAttribute('data-tooltip', value);\n    element.setAttribute('data-tooltip-position', arg);\n}\n\nexport default {\n\n    mounted: upsert,\n    updated: upsert,\n\n    unmounted(element) {\n        element.removeAttribute('data-tooltip');\n        element.removeAttribute('data-tooltip-position');\n    }\n\n} as Directive<HTMLInputElement, string>;"
  },
  {
    "path": "packages/components/src/directives/visible.ts",
    "content": "import type {\n    Directive,\n    DirectiveBinding\n} from '@vue/runtime-core';\n\nconst VISIBILITY_MAP = {\n    0: 'hidden',\n    1: 'visible'\n};\n\nfunction updateVisibility(element: HTMLElement, binding: DirectiveBinding<boolean>) {\n    element.style.visibility = VISIBILITY_MAP[+!!binding.value];\n}\n\nexport default {\n    \n    mounted: updateVisibility,\n    updated: updateVisibility,\n\n    unmounted(element) {\n        element.style.visibility = null;\n    }\n\n} as Directive<HTMLElement, boolean>;"
  },
  {
    "path": "packages/components/src/event-emitter/index.ts",
    "content": "import {\n    EventEmitter\n} from '@ocula/event-emitter';\n\nexport default new EventEmitter();"
  },
  {
    "path": "packages/components/src/helpers/get-listeners.ts",
    "content": "import {\n    typeIsFunction\n} from '@ocula/utilities';\n\nexport default function getListeners(attrs: Record<string, any>): Record<string, Function> {\n    const listeners = {};\n\n    for (const key in attrs) {\n        const value = attrs[key];\n\n        if (key.startsWith('on') && typeIsFunction(value)) {\n            listeners[key.replace(/^on/, '').toLowerCase()] = value;\n        }\n    }\n\n    return listeners;\n}"
  },
  {
    "path": "packages/components/src/index.ts",
    "content": "import '@ocula/style/src/index.scss';\n\nimport directives from './directives';\nimport components from './components';\n\nimport type {\n    App\n} from 'vue';\n\ntype Registrar = 'directive' | 'component';\n\nfunction register(application: App, registrar: Registrar, dictionary: Record<string, any>): void {\n    Object.keys(dictionary).forEach(key => application[registrar].call(application, key, dictionary[key]));\n}\n\n// Compositions\nexport { default as useSubscriber } from './compositions/subscriber';\nexport { default as useTimer } from './compositions/timer';\nexport { default as componentsController } from './controllers/components';\n\n// Helpers\nexport { default as getListeners } from './helpers/get-listeners';\n\nexport default {\n\n    install(application: App) {\n        register(application, 'directive', directives);\n        register(application, 'component', components);\n    }\n\n};"
  },
  {
    "path": "packages/components/src/types/index.ts",
    "content": "export interface IPromisePayload {\n    resolve(value?: any): void;\n    reject(value?: any): void;\n};\n\nexport interface IConfirmModalPayload {\n    message: string;\n    confirmLabel?: string;\n    cancelLabel?: string;\n};"
  },
  {
    "path": "packages/event-emitter/package.json",
    "content": "{\n    \"name\": \"@ocula/event-emitter\",\n    \"version\": \"1.0.0\",\n    \"main\": \"./src/index.ts\"\n}\n"
  },
  {
    "path": "packages/event-emitter/src/index.ts",
    "content": "export interface IListener {\n    dispose(): void\n}\n\nexport class EventEmitter {\n\n    private listeners: {\n        [key: string]: Function[]\n    };\n\n    constructor() {\n        this.listeners = {};\n    }\n\n    on(event: string, handler: Function): IListener {\n        if (!this.listeners[event]) {\n            this.listeners[event] = [];\n        }\n\n        this.listeners[event].push(handler);\n\n        return {\n            dispose: () => this.off(event, handler)\n        };\n    }\n\n    off(event: string, handler: Function): void {\n        const listeners = this.listeners[event];\n\n        if (!listeners) {\n            return;\n        }\n\n        this.listeners[event] = listeners.filter(listener => listener !== handler);\n\n        if (this.listeners[event].length === 0) {\n            delete this.listeners[event];\n        }\n    }\n\n    once(event: string, handler: Function): IListener {\n        const callback = (...args) => {\n            handler(...args);\n            this.off(event, callback);\n        };\n\n        return this.on(event, callback);\n    }\n\n    emit(event: string, ...args) {\n        const handlers = this.listeners[event];\n\n        if (!handlers) {\n            return;\n        }\n\n        handlers.forEach(handler => handler(...args));\n    }\n\n}\n\nexport default new EventEmitter();"
  },
  {
    "path": "packages/router/package.json",
    "content": "{\n    \"name\": \"@ocula/router\",\n    \"version\": \"1.0.0\",\n    \"main\": \"./src/index.ts\",\n    \"dependencies\": {\n        \"vue-router\": \"4.0.1\"\n    },\n    \"peerDependencies\": {\n        \"vue\": \"3.0.5\"\n    }\n}\n"
  },
  {
    "path": "packages/router/src/index.ts",
    "content": "import {\n    createRouter,\n    createWebHistory\n} from 'vue-router';\n\nimport type {\n    App\n} from 'vue';\n\nimport type {\n    Router,\n    RouteRecordRaw\n} from 'vue-router';\n\nexport type {\n    RouteRecord,\n    RouteRecordRaw\n} from 'vue-router';\n\nexport let router: Router;\n\nexport default {\n\n    install(app: App, routes: RouteRecordRaw[]) {\n        router = createRouter({\n            history: createWebHistory(),\n            routes\n        });\n\n        app.use(router);\n    }\n\n};"
  },
  {
    "path": "packages/state/package.json",
    "content": "{\n    \"name\": \"@ocula/state\",\n    \"version\": \"1.0.0\",\n    \"main\": \"./src/index.ts\",\n    \"license\": \"MIT\",\n    \"peerDependencies\": {\n        \"vue\": \"3.0.5\"\n    },\n    \"dependencies\": {\n        \"@ocula/utilities\": \"1.0.0\",\n        \"@vue/devtools-api\": \"^6.0.0-beta.2\"\n    }\n}\n"
  },
  {
    "path": "packages/state/src/index.ts",
    "content": "import {\n    reactive,\n    readonly,\n    computed,\n    Plugin\n} from 'vue';\n\nimport {\n    CustomInspectorNode,\n    setupDevtoolsPlugin\n} from '@vue/devtools-api';\n\nimport {\n    typeIsPlainObject\n} from '@ocula/utilities';\n\nimport type {\n    Getter,\n    Mutation,\n    IStore\n} from './types';\n\ntype StoreAccessor = () => object;\n\nconst stores = new Map<string, StoreAccessor>();\n\nlet devtoolsEnabled = process.env.NODE_ENV === 'development';\n\nfunction logMutation(name: string, state: any, isError: boolean = false): void {\n    if (!devtoolsEnabled) {\n        return;\n    }\n\n    console[isError ? 'warn' : 'info'](name, 'mutation');\n}\n\nfunction mapStore(state: object): CustomInspectorNode[] {\n    return Object.keys(state).reduce((output, key) => {\n        const value = state[key];\n\n        if (!typeIsPlainObject(value)) {\n            return output;\n        }\n\n        return [].concat(output, {\n            id: key,\n            label: key,\n            children: mapStore(value)\n        });\n    }, [] as CustomInspectorNode[]);\n}\n\nexport const plugin = {\n\n    install(app) {\n        setupDevtoolsPlugin({\n            app,\n            id: 'state',\n            label: 'State'\n        }, api => {\n            api.addInspector({\n                id: 'state',\n                label: 'State'\n            });\n\n            api.on.getInspectorTree((payload, context) => {\n                if (payload.app !== app || payload.inspectorId !== 'state') {\n                    return;\n                }\n\n                const nodes: CustomInspectorNode[] = [];\n\n                stores.forEach((accessor, name) => {\n                    const state = accessor();\n                   \n                    nodes.push({\n                        id: name,\n                        label: name,\n                        children: mapStore(state)\n                    });\n                });\n\n                payload.rootNodes = nodes;\n            });\n        });\n    }\n\n} as Plugin;\n\nexport function enableDevtools(value: boolean = true): void {\n    devtoolsEnabled = value;\n}\n\nexport default function createStore<T extends object = any>(name: string, data: T): IStore<T> {\n    const write = reactive(data);\n    const state = readonly(write);\n\n    function getter<U>(getter: Getter<T, U>) {\n        return computed(() => getter(state as T));\n    }\n    \n    function mutate(name: string, mutation: Mutation<T>): void {\n        const mutationName = name || mutation.name || 'unknown';\n\n        try {\n            mutation(write as T);\n        } catch (error) {\n            logMutation(mutationName, write);\n        }\n\n        logMutation(mutationName, write);\n    }\n\n    function destroy() {\n        stores.delete(name);\n    }\n\n    stores.set(name, () => state);\n    \n    return {\n        state: state as T,\n        getter,\n        mutate,\n        destroy\n    };\n}"
  },
  {
    "path": "packages/state/src/types/index.ts",
    "content": "import type {\n    ComputedRef\n} from '@vue/reactivity';\n\nexport type Getter<T, U> = (state: T) => U;\nexport type Mutation<T> = (state: T) => void;\n\nexport interface IStore<T> {\n    state: T;\n    getter<U>(getter: Getter<T, U>): ComputedRef<U>;\n    mutate(name:string, mutation: Mutation<T>): void;\n    destroy(): void;\n};"
  },
  {
    "path": "packages/state/tsconfig.json",
    "content": "{\n    \"extends\": \"../../tsconfig.json\",\n    \"compilerOptions\": {\n        \"lib\": [\n            \"DOM\",\n            \"ESNext\"\n        ]\n    }\n}"
  },
  {
    "path": "packages/style/package.json",
    "content": "{\n    \"name\": \"@ocula/style\",\n    \"version\": \"1.0.0\",\n    \"main\": \"./src/index.scss\",\n    \"license\": \"MIT\",\n    \"dependencies\": {\n        \"flex-layout-attribute\": \"^1.0.3\"\n    }\n}\n"
  },
  {
    "path": "packages/style/src/_base.scss",
    "content": "html,\nbody {\n    width: 100%;\n    height: 100%;\n    margin: 0;\n    padding: 0;\n    overflow: auto;\n}\n\nhtml {\n    font-size: 16px;\n    box-sizing: border-box;\n    -moz-box-sizing: border-box;\n}\n\n*,\n*:before,\n*:after {\n    box-sizing: inherit;\n}\n\nbody {\n    font-family: var(--font__family);\n    font-size: var(--font__size);\n    font-weight: var(--font__weight);\n    line-height: 1.5;\n    color: var(--font__colour);\n    background-color: var(--background__colour);\n    -webkit-font-smoothing: antialiased;\n    text-rendering: optimizeLegibility;\n    -moz-osx-font-smoothing: grayscale;\n}\n\n@media (hover: hover) {\n\n    ::-webkit-scrollbar {\n        width: 16px;\n        height: 16px;\n    }\n    \n    ::-webkit-scrollbar-track {\n        background-color: var(--background__colour);\n    }\n    \n    ::-webkit-scrollbar-thumb {\n        width: 8px;\n        height: 8px;\n        min-height: 32px;\n        background-color: rgba(#CCCCCC, 0.75);\n        background-clip: padding-box;\n        border: 4px solid transparent;\n        border-radius: 8px;\n    \n        &:hover {\n            background-color: #CCCCCC;\n        }\n    }\n\n}"
  },
  {
    "path": "packages/style/src/_buttons.scss",
    "content": "button,\n.button {\n    display: inline-block;\n    padding: var(--spacing__x-small) var(--spacing__medium);\n    font: inherit;\n    font-weight: var(--font__weight--heavy);\n    border: none;\n    border-radius: var(--border__radius);\n    background-color: #EEEEEE;\n    cursor: pointer;\n    transition: background var(--transition__timing) var(--transition__easing--default);\n\n    &:hover,\n    &:focus {\n        background-color: darken(#EEEEEE, 20%);\n    }\n}\n\n.button--primary {\n    color: var(--font__colour--compliment);\n    background-color: var(--colour__primary);\n\n    &:hover,\n    &:focus {\n        background-color: var(--colour__primary--dark);\n    }\n}\n\n.button--ghost {\n    font-weight: var(--font__weight);\n    background: none;\n\n    &:hover,\n    &:focus {\n        background: none;\n    }\n\n    &:focus {\n        outline: none;\n    }\n}"
  },
  {
    "path": "packages/style/src/_dots.scss",
    "content": ".dot {\n    display: inline-block;\n    width: 0.5em;\n    height: 0.5em;\n    border-radius: 50%;\n    vertical-align: middle;\n    background-color: var(--border__colour);\n}\n\n.dot--large {\n    width: 1em;\n    height: 1em;    \n}"
  },
  {
    "path": "packages/style/src/_grid.scss",
    "content": "$min-size: 1;\n$max-size: 12;\n\n$alignments: (\n    auto,\n    start,\n    end,\n    center,\n    stretch\n);\n\n[grid] {\n    display: grid;\n    grid-gap: var(--spacing__small);\n}\n\n@for $size from $min-size to $max-size {\n\n    [grid^=\"#{$size}\"] {\n        grid-template-columns: repeat($size, 1fr);\n    }\n\n}\n\n@each $alignment in $alignments {\n\n    [grid*=\"#{$alignment}-\"] {\n        justify-items: $alignment;\n    }\n\n    [grid*=\"-#{$alignment}\"] {\n        align-items: $alignment;\n    }\n\n    [grid-self^=\"#{$alignment}\"] {\n        justify-self: $alignment;\n    }\n\n    [grid-self*=\"-#{$alignment}\"] {\n        align-self: $alignment;\n    }\n\n}\n\n[grid-gap=\"none\"] {\n    grid-gap: 0;\n}\n\n@each $spacing-name, $spacing-value in $spacings {\n\n    [grid-gap=\"#{$spacing-name}\"] {\n        grid-gap: var(--spacing__#{ $spacing-name });\n    }\n\n    [grid-gap|=\"#{$spacing-name}\"] {\n        grid-column-gap: var(--spacing__#{ $spacing-name });\n    }\n\n    [grid-gap$=\"-#{$spacing-name}\"] {\n        grid-row-gap: var(--spacing__#{ $spacing-name });\n    }\n\n}\n\n@each $breakpoint-name, $breakpoint-value in $breakpoints {\n\n    @include breakpoint($breakpoint-name) {\n\n        @for $size from $min-size to $max-size {\n\n            [grid*=\"#{$breakpoint-name}-#{$size}\"] {\n                grid-template-columns: repeat($size, 1fr);\n            }\n\n        }\n\n    }\n\n}"
  },
  {
    "path": "packages/style/src/_inputs.scss",
    "content": "input[type=\"text\"],\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime\"],\ninput[type=\"number\"],\ninput[type=\"password\"],\ninput[type=\"email\"],\ninput[type=\"search\"],\ntextarea,\nselect {\n    display: block;\n    width: 100%;\n    padding: var(--spacing__x-small) var(--spacing__small);\n    color: inherit;\n    font-size: var(--font__size);\n    background-color: var(--background__colour);\n    border: 1px solid var(--border__colour);\n    border-radius: var(--border__radius);\n    outline: none;\n}\n\ninput[type=\"range\"] {\n    margin: 0;\n    background-color: transparent;\n    -webkit-appearance: none;\n\n    @mixin track {\n        box-sizing: border-box;\n        width: 100%;\n        height: 0.5rem;\n        background: var(--background__colour--hover);\n        border: none;\n        border-radius: 0.25rem;\n        cursor: pointer;\n    }\n\n    @mixin thumb {\n        box-sizing: border-box;\n        width: 1rem;\n        height: 1rem;\n        margin-top: -0.25rem;\n        background: var(--colour__primary);\n        border: none;\n        border-radius: 50%;\n        transition: background var(--transition__timing) var(--transition__easing--default);\n        cursor: pointer;\n        -webkit-appearance: none;\n    }\n\n    &:focus,\n    &:active {\n        outline: none;\n\n        // &::-webkit-slider-runnable-track {\n        //     background: var(--colour__primary);\n        // }\n        \n        // &::-moz-range-track {\n        //     background: var(--colour__primary);\n        // }\n        \n        // &::-ms-track {\n        //     background: var(--colour__primary);\n        // }\n    }\n\n\n    /* Track */\n\n    &::-webkit-slider-runnable-track {\n        @include track;\n    }\n\n    &::-moz-range-track {\n        @include track;\n    }\n\n    &::-ms-track {\n        @include track;\n    }\n\n    /* Thumb */\n\n    &::-webkit-slider-thumb {\n        @include thumb;\n\n        &:hover {\n            background: var(--colour__primary--dark);\n        }\n    }\n    \n    &::-moz-range-thumb {\n        @include thumb;\n        \n        &:hover {\n            background: var(--colour__primary--dark);\n        }\n    }\n    \n    &::-ms-thumb {\n        @include thumb;\n        margin-top: 0;\n        \n        &:hover {\n            background: var(--colour__primary--dark);\n        }\n    }\n} "
  },
  {
    "path": "packages/style/src/_menus.scss",
    "content": ".menu {\n    display: block;\n}\n\n.menu-item {\n    padding: var(--spacing__small);\n    border-radius: var(--border__radius);\n    cursor: pointer;\n}\n\n.menu-item--active {\n    color: var(--colour__primary);\n}\n\n@media (hover: hover) {\n\n    .menu-item {\n\n        &:hover {\n            background-color: var(--background__colour--hover);\n        }\n    }\n\n}"
  },
  {
    "path": "packages/style/src/_mixins.scss",
    "content": "@import \"./_variables.scss\";\n\n@mixin text-truncate {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n}\n\n@mixin breakpoint($breakpoint) {\n\n    $size: map-get($breakpoints, $breakpoint);\n\n    @media screen and (min-width: #{ $size }) {\n        @content;\n    }\n\n}"
  },
  {
    "path": "packages/style/src/_spacing.scss",
    "content": "$directions: (\n    top: (top),\n    right: (right),\n    bottom: (bottom),\n    left: (left),\n    vertical: (top, bottom),\n    horizontal: (left, right),\n    all: (top, right, bottom, left)\n);\n\n$spacing-properties: (\n    margin,\n    padding\n);\n\n@each $property-name in $spacing-properties {\n\n    .#{ $property-name }__all--none {\n        #{ $property-name }-top: 0;\n        #{ $property-name }-right: 0;\n        #{ $property-name }-bottom: 0;\n        #{ $property-name }-left: 0;\n    }\n\n    @each $direction-name, $direction-value in $directions {\n\n        .#{ $property-name }__#{ $direction-name }--none {\n\n            @each $direction-property in $direction-value {\n                #{ $property-name }-#{ $direction-property }: 0;\n            }\n\n        }\n\n        @each $spacing-name, $spacing-value in $spacings {\n\n            .#{ $property-name }__#{ $direction-name }--#{ $spacing-name } {\n\n                @each $direction-property in $direction-value {\n                    #{ $property-name }-#{ $direction-property }: var(--spacing__#{ $spacing-name });\n                }\n\n            }\n\n        }\n\n    }\n}"
  },
  {
    "path": "packages/style/src/_tables.scss",
    "content": "table {\n    border-collapse: collapse;\n}\n\nth,\ntd {\n    padding: var(--spacing__x-small);\n}\n\n.table--fixed {\n    width: 100%;\n    max-width: 100%;\n    table-layout: fixed;\n}"
  },
  {
    "path": "packages/style/src/_tooltips.scss",
    "content": "@media (hover: hover) {\n\n    [data-tooltip] {\n        position: relative;\n    \n        &::after {\n            display: block;\n            position: absolute;\n            content: attr(data-tooltip);\n            width: auto;\n            padding: var(--spacing__xx-small) var(--spacing__x-small);\n            font-size: var(--font__size--small);\n            color: var(--tooltip__font-colour);\n            white-space: nowrap;\n            background: var(--tooltip__background);\n            border-radius: 3px;\n            overflow: hidden;\n            opacity: 0;\n            transition: opacity var(--transition__timing--long) var(--transition__easing--default) 500ms;\n        }\n        \n        &:hover {\n            \n            &::after {\n                opacity: 1;\n            }\n        }\n    }\n    \n    [data-tooltip-position=\"top\"],\n    [data-tooltip-position=\"bottom\"] {\n    \n        &::after {\n            left: 50%;\n        }\n    }\n    \n    [data-tooltip-position=\"left\"],\n    [data-tooltip-position=\"right\"] {\n    \n        &::after {\n            top: 50%;\n        }\n    }\n    \n    [data-tooltip-position=\"top\"] {\n    \n        &::after {\n            bottom: 100%;\n            transform: translate(-50%, -0.5rem);\n        }\n    }\n    \n    [data-tooltip-position=\"bottom\"] {\n    \n        &::after {\n            top: 100%;\n            transform: translate(-50%, 0.5rem);\n        }\n    }\n    \n    [data-tooltip-position=\"left\"] {\n    \n        &::after {\n            right: 100%;\n            transform: translate(-0.5rem, -50%);\n        }\n    }\n    \n    [data-tooltip-position=\"right\"] {\n    \n        &::after {\n            left: 100%;\n            transform: translate(0.5rem, -50%);\n        }\n    }\n\n}\n\n@keyframes tooltip {\n\n    from {\n        opacity: 0;\n    }\n\n    to {\n        opacity: 1;\n    }\n\n}"
  },
  {
    "path": "packages/style/src/_typography.scss",
    "content": "h1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n    margin: 0;\n    font-weight: var(--font__weight--heavy);\n}\n\na,\n.link {\n    text-decoration: none;\n}\n\na {\n    color: var(--colour__primary);\n    \n    &:hover {\n        color: var(--colour__primary--dark);\n    }\n}\n\n.link {\n    color: inherit;\n}\n\n.link--inherit {\n    color: inherit;\n\n    &:hover {\n        color: inherit;\n    }\n}\n\n.text--meta {\n    color: var(--font__colour--meta);\n}\n\n.text--medium {\n    font-weight: var(--font__weight--medium);\n}\n\nstrong,\n.text--heavy {\n    font-weight: var(--font__weight--heavy);\n}\n\nsmall,\n.text--small {\n    font-size: var(--font__size--small);\n}\n\n.text--x-small {\n    font-size: var(--font__size--x-small);\n}\n\n.text--left {\n    text-align: left;\n}\n\n.text--centre {\n    text-align: center;\n}\n\n.text--right {\n    text-align: right;\n}\n\n.text--tight {\n    line-height: 1;\n}\n\n.text--no-wrap {\n    overflow: hidden;\n    white-space: nowrap;\n}\n\n.text--truncate {\n    @include text-truncate;\n}\n\n@media (hover: hover) {\n\n    a {\n\n        &:hover {\n            color: var(--colour__primary--dark);\n        }\n    }\n\n}"
  },
  {
    "path": "packages/style/src/_variables.scss",
    "content": "$spacings: (\n    xx-small: 0.25rem,\n    x-small: 0.5rem,\n    small: 1rem,\n    medium: 1.5rem,\n    large: 2rem,\n    x-large: 3rem,\n    xx-large: 4rem\n);\n\n$breakpoints: (\n    lg: 64em,\n    md: 52em,\n    sm: 40em\n);\n\n$colour__primary: #5D9BE5;\n$colour__primary--light: lighten(#5D9BE5, 20%);\n$colour__primary--dark: darken(#5D9BE5, 20%);\n\n:root {\n    --font__size: 14px;\n    --font__size--small: 0.875em;\n    --font__size--x-small: 0.75em;\n    --font__size--large: 1.25em;\n    --font__size--x-large: 1.5em;\n\n    --font__weight: 400;\n    --font__weight--medium: 500;\n    --font__weight--heavy: 700;\n\n    --font__family: 'DM Sans', sans-serif;\n\n    --font__colour: #353539;\n    --font__colour--compliment: #F7F7F7;\n    --font__colour--meta: #9999AA;\n    \n    --transition__timing: 250ms;\n    --transition__timing--long: 400ms;\n    --transition__timing--fade: 1s;\n    --transition__easing--default: cubic-bezier(0.165, 0.84, 0.44, 1); // quartic-out\n    \n    --colour__primary: #5D9BE5;\n    --colour__primary--light: #{ lighten(#5D9BE5, 20%) };\n    --colour__primary--dark: #{ darken(#5D9BE5, 20%) };\n    \n    --background__colour: #FFFFFF;\n    --background__colour--hover: #EEEEEE;\n    \n    --border__colour: #EDEDED;\n    --border__radius: 5px;\n    --border__radius--large: 1rem;\n\n    --tooltip__background: #333333;\n    --tooltip__font-colour: #FFFFFF;\n        \n    @each $name, $value in $spacings {\n        --spacing__#{ $name }: #{ $value };\n    }\n}"
  },
  {
    "path": "packages/style/src/index.scss",
    "content": "@import \"~flex-layout-attribute/sass/flex-layout-attribute.scss\";\n\n@import \"./_variables.scss\";\n@import \"./_mixins.scss\";\n@import \"./_base.scss\";\n@import \"./_spacing.scss\";\n@import \"./_grid.scss\";\n@import \"./_typography.scss\";\n@import \"./_buttons.scss\";\n@import \"./_inputs.scss\";\n@import \"./_tables.scss\";\n@import \"./_menus.scss\";\n@import \"./_dots.scss\";\n@import \"./_tooltips.scss\";"
  },
  {
    "path": "packages/task-queue/package.json",
    "content": "{\n    \"name\": \"@ocula/task-queue\",\n    \"version\": \"1.0.0\",\n    \"main\": \"./src/index.ts\"\n}\n"
  },
  {
    "path": "packages/task-queue/src/index.ts",
    "content": "export class TaskQueue {\n\n    private _tasks: Set<Function>;\n    private _suppressErrors: boolean;\n\n    constructor(suppressErrors: boolean = false) {\n        this._tasks = new Set();\n        this._suppressErrors = suppressErrors;\n    }\n\n    get suppressErrors() {\n        return this._suppressErrors;\n    }\n\n    set suppressErrors(value) {\n        this._suppressErrors = !!value;\n    }\n\n    add(task: Function): void {\n        this._tasks.add(task);\n    }\n\n    remove(task: Function): void {\n        this._tasks.delete(task);\n    }\n\n    clear(): void {\n        this._tasks.clear();\n    }\n\n    run(...args: any[]): void {\n        this._tasks.forEach(task => {\n            try {\n                task(...args);\n            } catch (error) {\n                if (!this._suppressErrors) {\n                    throw error;\n                }\n            } finally {\n                this.remove(task);\n            }\n        });\n    }\n\n}\n\nexport default new TaskQueue();"
  },
  {
    "path": "packages/utilities/package.json",
    "content": "{\n    \"name\": \"@ocula/utilities\",\n    \"version\": \"1.0.0\",\n    \"main\": \"./src/index.ts\",\n    \"license\": \"MIT\",\n    \"dependencies\": {\n        \"date-fns\": \"^2.16.1\",\n        \"date-fns-tz\": \"^1.0.12\",\n        \"lodash\": \"^4.17.20\",\n        \"nanoid\": \"^3.1.20\"\n    },\n    \"devDependencies\": {\n        \"@types/lodash\": \"^4.14.165\"\n    }\n}\n"
  },
  {
    "path": "packages/utilities/src/array/join-by.ts",
    "content": "import getAccessor from '../value/get-accessor';\n\ntype Iteratee<T> = (value: T) => string;\n\nexport default function joinBy<T>(array: T[], iteratee: Iteratee<T> = value => String(value), separator: string = ','): string {\n    const accessor = getAccessor(iteratee);\n    const trimmer = new RegExp(`${separator}$`);\n\n    return array.reduce((output, value) => output + accessor(value) + separator, '')\n        .replace(trimmer, '');\n}"
  },
  {
    "path": "packages/utilities/src/array/order-by.ts",
    "content": "export { default } from 'lodash/orderBy';"
  },
  {
    "path": "packages/utilities/src/array/swap-by.ts",
    "content": "import isNumber from '../type/is-number';\nimport clamp from '../number/clamp';\n\n\ntype Predicate<T> = (value: T) => boolean;\n\nfunction getIndex<T>(array: T[], predicate: number | Predicate<T>): number {\n    const index = isNumber(predicate) ? predicate : array.findIndex(predicate);\n\n    return clamp(index, 0, array.length - 1);\n}\n\nexport default function swapBy<T>(array: T[], predicateA: number | Predicate<T>, predicateB: number | Predicate<T>): T[] {\n    const clone = array.slice();\n\n    const indexA = getIndex(array, predicateA);\n    const indexB = getIndex(array, predicateB);\n\n    clone[indexA] = array[indexB];\n    clone[indexB] = array[indexA];\n\n    return clone;\n}"
  },
  {
    "path": "packages/utilities/src/array/union-with.ts",
    "content": "export { default } from 'lodash/unionWith';"
  },
  {
    "path": "packages/utilities/src/array/unique-by.ts",
    "content": "export { default } from 'lodash/uniqBy';"
  },
  {
    "path": "packages/utilities/src/date/format-distance-to-now.ts",
    "content": "export { default } from 'date-fns/formatDistanceToNow';"
  },
  {
    "path": "packages/utilities/src/date/format-distance.ts",
    "content": "export { default } from 'date-fns/formatDistance';"
  },
  {
    "path": "packages/utilities/src/date/format.ts",
    "content": "export { default } from 'date-fns-tz/format';"
  },
  {
    "path": "packages/utilities/src/date/from-unix.ts",
    "content": "export { default } from 'date-fns/fromUnixTime';"
  },
  {
    "path": "packages/utilities/src/date/is-today.ts",
    "content": "export { default } from 'date-fns/isToday';"
  },
  {
    "path": "packages/utilities/src/date/to-unix.ts",
    "content": "export { default } from 'date-fns/getUnixTime';"
  },
  {
    "path": "packages/utilities/src/date/utc-to-zoned.ts",
    "content": "export { default } from 'date-fns-tz/utcToZonedTime';"
  },
  {
    "path": "packages/utilities/src/dom/set-meta.ts",
    "content": "export default function setMeta(key: string, value: string = ''): void {\n    let meta = document.querySelector(`meta[name=${key}]`);\n    \n    if (!meta) {\n        meta = document.createElement('meta');\n        meta.setAttribute('name', key);\n        document.head.appendChild(meta);\n    }\n    \n    meta.setAttribute('content', value);\n} "
  },
  {
    "path": "packages/utilities/src/env/_base/is-env.ts",
    "content": "type Environment = 'development' | 'production';\n\nexport default function isEnv(environment: Environment): boolean {\n    return process.env.NODE_ENV === environment;\n}"
  },
  {
    "path": "packages/utilities/src/env/is-development.ts",
    "content": "import isEnv from './_base/is-env';\n\nexport default isEnv('development');"
  },
  {
    "path": "packages/utilities/src/env/is-production.ts",
    "content": "import isEnv from './_base/is-env';\n\nexport default isEnv('production');"
  },
  {
    "path": "packages/utilities/src/function/debounce.ts",
    "content": "export { default } from 'lodash/debounce';"
  },
  {
    "path": "packages/utilities/src/function/identity.ts",
    "content": "export default function identity(value) {\n    return value;\n}"
  },
  {
    "path": "packages/utilities/src/function/noop.ts",
    "content": "export { default } from 'lodash/noop';"
  },
  {
    "path": "packages/utilities/src/index.ts",
    "content": "export { default as arrayJoinBy } from './array/join-by';\nexport { default as arrayOrderBy } from './array/order-by';\nexport { default as arraySwapBy } from './array/swap-by';\nexport { default as arrayUnionWith } from './array/union-with';\nexport { default as arrayUniqueBy } from './array/unique-by';\n\nexport { default as dateFormat } from './date/format';\nexport { default as dateFormatDistance } from './date/format-distance';\nexport { default as dateFormatDistanceToNow } from './date/format-distance-to-now';\nexport { default as dateFromUnix } from './date/from-unix';\nexport { default as dateIsToday } from './date/is-today';\nexport { default as dateToUnix } from './date/to-unix';\nexport { default as dateUtcToZoned } from './date/utc-to-zoned';\n\nexport { default as domSetMeta } from './dom/set-meta';\n\nexport { default as envIsDevelopment } from './env/is-development';\nexport { default as envIsProduction } from './env/is-production';\n\nexport { default as functionDebounce } from './function/debounce';\nexport { default as functionIdentity } from './function/identity';\n\nexport { default as numberClamp } from './number/clamp';\nexport { default as numberMinBy } from './number/min-by';\nexport { default as numberMaxBy } from './number/max-by';\nexport { default as numberPercentage } from './number/percentage';\nexport { default as numberRound } from './number/round';\n\nexport { default as objectCloneLazy } from './object/clone-lazy';\nexport { default as objectMerge } from './object/merge';\nexport { default as objectMergeWith } from './object/merge-with';\nexport { default as objectTransform } from './object/transform';\n\nexport { default as scaleContinuous } from './scale/continuous';\nexport { default as scaleDiscrete } from './scale/discrete';\n\nexport { default as stringCapitalize } from './string/capitalize';\nexport { default as stringUniqueId } from './string/unique-id';\n\nexport { default as typeIsArray } from './type/is-array';\nexport { default as typeIsDate } from './type/is-date';\nexport { default as typeIsFunction } from './type/is-function';\nexport { default as typeIsNil } from './type/is-nil';\nexport { default as typeIsNumber } from './type/is-number';\nexport { default as typeIsPlainObject } from './type/is-plain-object';\nexport { default as typeIsString } from './type/is-string';\n\nexport { default as valueGetAccessor } from './value/get-accessor';"
  },
  {
    "path": "packages/utilities/src/number/clamp.ts",
    "content": "export { default } from 'lodash/clamp';"
  },
  {
    "path": "packages/utilities/src/number/max-by.ts",
    "content": "export { default } from 'lodash/maxBy';"
  },
  {
    "path": "packages/utilities/src/number/min-by.ts",
    "content": "export { default } from 'lodash/minBy';"
  },
  {
    "path": "packages/utilities/src/number/percentage.ts",
    "content": "import round from './round';\n\nexport default function(value: number, precision: number = 0): string {\n    return `${round(value * 100, precision)}%`;\n}"
  },
  {
    "path": "packages/utilities/src/number/round.ts",
    "content": "export { default } from 'lodash/round';"
  },
  {
    "path": "packages/utilities/src/object/clone-lazy.ts",
    "content": "export default function cloneLazy<T extends Object>(value: T): T {\n    return JSON.parse(JSON.stringify(value));\n}"
  },
  {
    "path": "packages/utilities/src/object/merge-with.ts",
    "content": "import mergeWith from 'lodash/mergeWith';\n\nexport default function(...args) {\n    return mergeWith({}, ...args);\n}"
  },
  {
    "path": "packages/utilities/src/object/merge.ts",
    "content": "import merge from 'lodash/merge';\n\n// Convert merge to be immutable\nexport default function(...sources) {\n    return merge({}, ...sources); \n};"
  },
  {
    "path": "packages/utilities/src/object/transform.ts",
    "content": "import functionIdentity from '../function/identity';\nimport typeIsArray from '../type/is-array';\nimport typeIsFunction from '../type/is-function';\nimport typeIsNil from '../type/is-nil';\nimport typeIsPlainObject from '../type/is-plain-object';\n\ntype Transformer = <T>(value: any, key?: PropertyKey, input?: T) => any;\ntype SchemaValue = any | any[] | Transformer | Object;\n\ninterface Schema {\n    [key: string]: Transformer | Schema | Schema[]\n}\n\nfunction getTransformer(schemaValue: SchemaValue, baseTransformer: Transformer): Transformer {\n    switch (true) {\n        case typeIsFunction(schemaValue):\n            return schemaValue;\n        case typeIsArray(schemaValue):\n            return value => transformArray(value, schemaValue, baseTransformer);\n        case typeIsPlainObject(schemaValue):\n            return value => transformObject(value, schemaValue, baseTransformer);\n        default:\n            return baseTransformer;\n    }\n}\n\nfunction transformArray(input: any[], schemaValue: any[], baseTransformer: Transformer): any[] {\n    const transformer = getTransformer(schemaValue[0], baseTransformer);\n\n    return input.map(transformer);\n}\n\nfunction transformObject<T, U = any>(input: T, schema: Schema, baseTransformer: Transformer): U {\n    const output: U = {};\n\n    for (const key in input) {\n        const schemaValue = schema[key];\n        const transformer = getTransformer(schemaValue, baseTransformer);\n        const value = transformer(input[key], key, input);\n\n        if (!typeIsNil(value)) {\n            output[key] = value;\n        }\n    }\n\n    return output;\n}\n\nexport default function transform<T, U = any>(input: T, schema: Schema, baseTransformer: Transformer = functionIdentity): U {\n    return transformObject(input, schema, baseTransformer);\n}"
  },
  {
    "path": "packages/utilities/src/scale/_base/scale.ts",
    "content": "import numberClamp from '../../number/clamp';\n\ntype Calculation<T> = (value: T) => number;\n\nexport interface IScale<T = number> {\n    (value: T, clamp?: boolean): number,\n    domain: T[],\n    range: number[],\n}\n\nexport default function scale<T = number>(domain: T[], range: number[], calculation: Calculation<T>): IScale<T> {\n    const [\n        min,\n        max\n    ] = range;\n\n    const output: IScale<T> = (value: T, clamp: boolean) => {\n        let result = calculation(value);\n\n        if (clamp) {\n            result = numberClamp(result, min, max);\n        }\n\n        return result;\n    };\n\n    output.domain = domain;\n    output.range = range;\n    \n    return output;\n}"
  },
  {
    "path": "packages/utilities/src/scale/continuous.ts",
    "content": "import scale from './_base/scale';\n\nimport type {\n    IScale\n} from './_base/scale';\n\nexport default function continuous(\n    domain: number[],\n    range: number[],\n): IScale {\n    const [\n        domainMin,\n        domainMax\n    ] = domain;\n\n    const [\n        rangeMin,\n        rangeMax\n    ] = range;\n\n    const domainLength = domainMax - domainMin;\n    const rangeLength = rangeMax - rangeMin;\n\n    return scale(domain, range, value => {\n        return (value - domainMin) * rangeLength / domainLength + rangeMin;\n    });\n};"
  },
  {
    "path": "packages/utilities/src/scale/discrete.ts",
    "content": "import scale from './_base/scale';\n\nimport type {\n    IScale\n} from './_base/scale';\n\nexport default function discrete<T>(\n    domain: T[],\n    range: number[],\n): IScale<T> {\n    const [\n        rangeMin,\n        rangeMax\n    ] = range;\n\n    const rangeLength = rangeMax - rangeMin;\n    const domainLength = domain.length;\n    const step = rangeLength / domainLength;\n\n    return scale(domain, range, value => {\n        return rangeMin + (domain.indexOf(value) * step);\n    });\n};"
  },
  {
    "path": "packages/utilities/src/string/capitalize.ts",
    "content": "export { default } from 'lodash/capitalize';"
  },
  {
    "path": "packages/utilities/src/string/unique-id.ts",
    "content": "import { nanoid } from 'nanoid';\n\nexport default function uniqueId(length: number = 6): string {\n    return nanoid(length);\n}"
  },
  {
    "path": "packages/utilities/src/type/is-array.ts",
    "content": "export { default } from 'lodash/isArray';"
  },
  {
    "path": "packages/utilities/src/type/is-date.ts",
    "content": "export { default } from 'lodash/isDate';"
  },
  {
    "path": "packages/utilities/src/type/is-function.ts",
    "content": "export { default } from 'lodash/isFunction';"
  },
  {
    "path": "packages/utilities/src/type/is-nil.ts",
    "content": "export { default } from 'lodash/isNil';"
  },
  {
    "path": "packages/utilities/src/type/is-number.ts",
    "content": "export { default } from 'lodash/isNumber';"
  },
  {
    "path": "packages/utilities/src/type/is-plain-object.ts",
    "content": "export { default } from 'lodash/isPlainObject';"
  },
  {
    "path": "packages/utilities/src/type/is-string.ts",
    "content": "export { default } from 'lodash/isString';"
  },
  {
    "path": "packages/utilities/src/value/get-accessor.ts",
    "content": "import isNil from '../type/is-nil';\nimport isFunction from '../type/is-function';\nimport noop from '../function/noop';\n\ntype Product<T> = (...args: any[]) => T;\n\nexport default function getAccessor<T>(identity: T | Product<T>): Product<T> {\n    if (isNil(identity)) {\n        return noop as any;\n    }\n\n    return isFunction(identity) ? identity : () => identity;\n}"
  },
  {
    "path": "packages/utilities/tsconfig.json",
    "content": "{\n    \"extends\": \"../../tsconfig.json\"\n}"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"es2017\",\n        \"module\": \"esnext\",\n        \"moduleResolution\": \"node\",\n        \"jsx\": \"react\",\n        \"allowSyntheticDefaultImports\": true,\n        \"noImplicitAny\": false,\n        \"resolveJsonModule\": true\n    },\n    \"exclude\": [\n        \"public\",\n        \"dist\",\n        \"node_modules\"\n    ]\n}"
  },
  {
    "path": "vercel.json",
    "content": "{\n    \"version\": 2,\n    \"env\": {\n        \"OWM_API_KEY\": \"@owm-api-key\",\n        \"WORLDTIDES_API_KEY\": \"@worldtides-api-key\",\n        \"MAPBOX_API_KEY\": \"@mapbox-api-key\",\n        \"GA_TRACKING_ID\": \"@ga-tracking-id\",\n        \"SENTRY_DSN\": \"@sentry-dsn\"\n    },\n    \"build\": {\n        \"env\": {\n            \"OWM_API_KEY\": \"@owm-api-key\",\n            \"WORLDTIDES_API_KEY\": \"@worldtides-api-key\",\n            \"MAPBOX_API_KEY\": \"@mapbox-api-key\",\n            \"GA_TRACKING_ID\": \"@ga-tracking-id\",\n            \"SENTRY_DSN\": \"@sentry-dsn\"\n        }\n    },\n    \"routes\": [\n        {\n            \"handle\": \"filesystem\"\n        },\n        {\n            \"src\": \"/.*\",\n            \"dest\": \"/index.html\"\n        }\n    ]\n}"
  }
]