diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..8921a8121 Binary files /dev/null and b/.DS_Store differ diff --git a/.eslintrc.js b/.eslintrc.js index 0ec89fa61..492a94d8a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,9 +6,10 @@ module.exports = { }, extends: [ "eslint:recommended", - "plugin:vue/recommended", + "plugin:vue/vue3-recommended", "plugin:prettier/recommended", "prettier", + "plugin:storybook/recommended", ], rules: { "vue/require-default-prop": "off", diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 6580b1680..d03df648d 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -72,6 +72,11 @@ jobs: runs-on: ubuntu-latest steps: + - name: Set up Node + uses: actions/setup-node@v2 + with: + node-version: "16" + - name: Cache NPM modules uses: actions/cache@v2 with: diff --git a/.gitignore b/.gitignore index b0806936c..6c051daf4 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ ca-cert.srl server-key.pem ca-key.pem cmd/wip +fly.toml \ No newline at end of file diff --git a/.storybook/main.js b/.storybook/main.js index 08826898c..ddef68bcf 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,4 +1,23 @@ +const { loadConfigFromFile } = require("vite"); +const path = require("path"); + module.exports = { stories: ["../assets/js/**/*.stories.mdx", "../assets/js/**/*.stories.@(js|jsx|ts|tsx)"], addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-postcss"], + framework: "@storybook/vue3", + core: { + builder: "storybook-builder-vite", + }, + staticDirs: ["../assets"], + async viteFinal(config, b) { + const { config: userConfig } = await loadConfigFromFile( + path.resolve(__dirname, "../vite.config.ts") + ); + const vuePlugin = userConfig.plugins.find((p) => p.name === "vite:vue"); + + const vuePluginIndex = config.plugins.findIndex((p) => p.name === "vite:vue"); + config.plugins[vuePluginIndex] = vuePlugin; + + return config; + }, }; diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html deleted file mode 100644 index 7f0b8485c..000000000 --- a/.storybook/preview-head.html +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/.storybook/preview.js b/.storybook/preview.js index 5d00c0212..bfc6cb9f2 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,4 +1,26 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "bootstrap"; +import smoothscroll from "smoothscroll-polyfill"; +import { app } from "@storybook/vue3"; +import i18n from "../assets/js/i18n"; +import "../assets/css/app.css"; + +smoothscroll.polyfill(); +app.use(i18n); export const parameters = { actions: { argTypesRegex: "^on[A-Z].*" }, -} \ No newline at end of file + backgrounds: { + default: "dark", + values: [ + { + name: "light", + value: "#ffffff", + }, + { + name: "dark", + value: "#28293e", + }, + ], + }, +}; diff --git a/Dockerfile b/Dockerfile index 52799c35a..b06943cb8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN make install-ui # build ui COPY assets assets -COPY vue.config.js vue.config.js +COPY vite.config.js vite.config.js COPY .eslintrc.js .eslintrc.js RUN make clean ui diff --git a/README.md b/README.md index eded9c338..f8244ed11 100644 --- a/README.md +++ b/README.md @@ -41,14 +41,51 @@ Evcc is an extensible EV Charge Controller with PV integration implemented in [G You'll find everything you need in our [documentation](https://docs.evcc.io/) (German). -## Build +## Contribute -To build EVCC from source, [Go][2] 1.18 and [Node][3] 16 are required: +To build evcc from source, [Go][2] 1.18 and [Node][3] 16 are required. + +Build and run go backend. The UI becomes available at http://127.0.0.1:7070/ ```sh make +./evcc ``` +For frontend development start the Vue toolchain in dev-mode. Open http://127.0.0.1:7071/ to get to the livelreloading development server. It pulls its data from port 7070 (see above). + +```sh +npm install +npm run start +``` + +### Code formatting + +We use linters (golangci-lint, Prettier) to keep a coherent source code formatting. It's recommended to use the format-on-save feature of your editor. For VSCode use the [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [Veture](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extension. You can manually reformat your code by running: + +```sh +make lint +make lint-ui +``` + +### Changing UI code + +To ensure reproducability the build frontend artifacts are part of the source code repository. If you've made changes to frontend code, please make sure to rebuild the production assets before you commit. + +```sh +make ui +``` + +### Changing templates + +Evcc supports a massive amount of different devices. To keep our documentation and website in sync with the latest software the core project (this repo) generates meta-data that's pushed to the `docs` and `evcc.io` repository. Make sure to update this meta-data every time you make changes to a templates. + +```sh +make docs +``` + +If you miss one of the above steps Gitub Actions will likely trigger a **Porcelain** error. + ## Sponsorship Evcc believes in open source software. We're committed to provide best in class EV charging experience. diff --git a/assets/.DS_Store b/assets/.DS_Store new file mode 100644 index 000000000..6808ad310 Binary files /dev/null and b/assets/.DS_Store differ diff --git a/assets/css/app.css b/assets/css/app.css index 6d357dec6..5c6faa575 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -1,14 +1,67 @@ +@font-face { + font-family: "Montserrat"; + src: url("../font/Montserrat-Medium.woff2") format("woff2"); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: "Montserrat"; + src: url("../font/Montserrat-Bold.woff2") format("woff2"); + font-weight: bold; + font-style: normal; +} + :root { - --evcc-green: #66d85a; - --evcc-dark-green: #3aba2c; - --evcc-yellow: #ffe000; + --evcc-green: #baffcb; + --evcc-dark-green: #0fde41; + --evcc-yellow: #faf000; + --bs-gray-dark: #28293e; + --bs-gray-medium: #93949e; --evcc-grid: var(--bs-gray-dark); --evcc-self: var(--evcc-dark-green); --evcc-export: var(--evcc-yellow); + --evcc-transition-slow: 1000ms; + --evcc-transition-medium: 500ms; + --evcc-transition-fast: 250ms; + --evcc-transition-very-fast: 100ms; + --bs-primary: var(--evcc-dark-green); --bs-primary-rgb: 58, 186, 44; + + --bs-body-font-size: 14px; + --bs-font-sans-serif: Montserrat, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", + Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji"; +} + +html { + /* prevents content jump when scrollbar is activate */ + width: 100vw; + overflow-x: hidden; +} + +body { + background-color: var(--evcc-gray-dark); +} + +h1, +h2, +h3, +h4 { + font-weight: bold; +} + +h1, +h2 { + font-size: 1.25rem; + text-transform: uppercase; +} + +h3, +h4 { + font-size: 1.25rem; } .bg-primary { @@ -27,22 +80,33 @@ a { opacity: 0.25; } +.rounded { + border-radius: 10px !important; +} + .btn-primary, .btn-primary:focus { background-color: var(--bs-gray-dark); border-color: var(--bs-gray-dark); } + .btn-primary:hover, .btn-primary:active { background-color: var(--evcc-dark-green); border-color: var(--evcc-dark-green); } -.btn-outline-primary { +.btn-outline-primary, +.btn-outline-primary:focus { color: var(--bs-gray-dark); + background-color: transparent; + border-width: 2px; border-color: var(--bs-gray-dark); } -.btn-outline-primary:hover { - background-color: var(--evcc-dark-green); +.btn-outline-primary:hover, +.btn-outline-primary:active { + color: var(--evcc-dark-green); + background-color: transparent; + border-width: 2px; border-color: var(--evcc-dark-green); } .text-evcc { @@ -52,6 +116,18 @@ a { .text-grid { color: var(--evcc-grid); } +.text-dark { + color: var(--bs-gray-dark); +} +.text-gray { + color: var(--bs-gray-medium); +} +.text-gray-medium { + color: var(--bs-gray-medium); +} +.bg-dark { + background-color: var(--bs-gray-dark) !important; +} @media screen and (min-width: 400px) and (max-width: 574px) { .d-xs-none { @@ -62,6 +138,33 @@ a { } } +.modal-backdrop.show { + opacity: 0.8; +} + +.modal-header { + padding-bottom: 1.5rem; +} + +.modal-title { + font-weight: bold; + font-size: 22px; +} + +.modal-content { + border-radius: 1rem; + padding: 1rem; +} + +.modal-body { + padding-top: 1.5rem; + padding-bottom: 1.5rem; +} + +.modal-footer { + padding-top: 1.5rem; +} + .cursor-pointer { cursor: pointer; } @@ -69,3 +172,13 @@ a { .v-popper__inner { margin: 0 12px !important; } + +.small, +small { + font-size: var(--bs-body-font-size); + color: var(--bs-gray-medium) !important; +} + +.btn-close { + opacity: 1; +} diff --git a/assets/font/Montserrat-Bold.woff2 b/assets/font/Montserrat-Bold.woff2 new file mode 100644 index 000000000..a75e620ac Binary files /dev/null and b/assets/font/Montserrat-Bold.woff2 differ diff --git a/assets/font/Montserrat-Medium.woff2 b/assets/font/Montserrat-Medium.woff2 new file mode 100644 index 000000000..11098b7df Binary files /dev/null and b/assets/font/Montserrat-Medium.woff2 differ diff --git a/assets/ico/browserconfig.xml b/assets/ico/browserconfig.xml deleted file mode 100644 index bc5d0fe32..000000000 --- a/assets/ico/browserconfig.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - #18191a - - - diff --git a/assets/index.html b/assets/index.html index 7ba8be19b..1edbef4d2 100644 --- a/assets/index.html +++ b/assets/index.html @@ -3,22 +3,21 @@ - - + - - - - - - - - + - - - + + + + + + + + + + evcc @@ -32,5 +31,6 @@
+ diff --git a/assets/js/app.js b/assets/js/app.js index 91ed2e885..0dfada6a6 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1,22 +1,21 @@ import "bootstrap/dist/css/bootstrap.min.css"; import "bootstrap"; +import smoothscroll from "smoothscroll-polyfill"; import "../css/app.css"; -import Vue from "vue"; -import VueMeta from "vue-meta"; +import { createApp, h } from "vue"; +import { createMetaManager, plugin as metaPlugin } from "vue-meta"; import api from "./api"; -import App from "./views/App"; +import App from "./views/App.vue"; import router from "./router"; import i18n from "./i18n"; -import "./tooltip"; -import store from "./store"; +import featureflags from "./featureflags"; -Vue.use(VueMeta); +smoothscroll.polyfill(); -window.app = new Vue({ - el: "#app", - router, - i18n, - data: { store, notifications: [] }, +const app = createApp({ + data() { + return { notifications: [] }; + }, methods: { raise: function (msg) { console[msg.type](msg); @@ -48,11 +47,18 @@ window.app = new Vue({ this.raise(msg); }, }, - render: function (h) { - return h(App, { props: { notifications: this.notifications } }); + render: function () { + return h(App, { notifications: this.notifications }); }, }); +app.use(i18n); +app.use(router); +app.use(createMetaManager()); +app.use(metaPlugin); +app.use(featureflags); +window.app = app.mount("#app"); + window.setInterval(function () { api.get("health").catch(function () { window.app.error({ message: "Server unavailable" }); diff --git a/assets/js/components/Energyflow/BatteryIcon.vue b/assets/js/components/Energyflow/BatteryIcon.vue index ca7fa5c53..c3199d8c4 100644 --- a/assets/js/components/Energyflow/BatteryIcon.vue +++ b/assets/js/components/Energyflow/BatteryIcon.vue @@ -1,58 +1,27 @@ - diff --git a/assets/js/components/Energyflow/Energyflow.spec.js b/assets/js/components/Energyflow/Energyflow.spec.js deleted file mode 100644 index 7e1ee03dd..000000000 --- a/assets/js/components/Energyflow/Energyflow.spec.js +++ /dev/null @@ -1,228 +0,0 @@ -/* globals describe, it, expect */ -import { shallowMount } from "@vue/test-utils"; -import Energyflow from "./Energyflow.vue"; - -const mocks = { - $t: (x) => x, - $tc: (x, n) => `${n} ${x}`, - $n: function (value, options) { - const n = new Intl.NumberFormat("en-EN", options); - return n.format(value); - }, -}; - -describe("Energyflow.vue", () => { - const defaultProps = { - gridConfigured: true, - gridPower: 0, - pvConfigured: true, - pvPower: 0, - homePower: 0, - batteryConfigured: false, - batteryPower: 0, - batterySoC: 0, - }; - - it("using pv and grid power", async () => { - const wrapper = shallowMount(Energyflow, { - mocks, - propsData: { - ...defaultProps, - gridPower: 1000, - pvPower: 4000, - homePower: 1300, - loadpointsPower: 3700, - }, - }); - await wrapper.find(".energyflow").trigger("click"); - - expect(wrapper.find("[data-test-grid-import]").text()).toMatch("1.0 kW"); - expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("4.0 kW"); - expect(wrapper.find("[data-test-pv-export]").text()).toMatch("0.0 kW"); - - expect(wrapper.find("[data-test-home-power]").text()).toMatch("1.3 kW"); - expect(wrapper.find("[data-test-loadpoints]").text()).toMatch("3.7 kW"); - expect(wrapper.find("[data-test-pv-production]").text()).toMatch("4.0 kW"); - expect(wrapper.find("[data-test-battery]").exists()).toBe(false); - }); - - it("exporting all pv power, no usage", async () => { - const wrapper = shallowMount(Energyflow, { - mocks, - propsData: { ...defaultProps, gridPower: -4000, pvPower: 5000, loadpointsPower: 1000 }, - }); - - await wrapper.find(".energyflow").trigger("click"); - - expect(wrapper.find("[data-test-grid-import]").text()).toMatch("0.0 kW"); - expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("1.0 kW"); - expect(wrapper.find("[data-test-pv-export]").text()).toMatch("4.0 kW"); - - expect(wrapper.find("[data-test-home-power]").text()).toMatch("0.0 kW"); - expect(wrapper.find("[data-test-loadpoints]").text()).toMatch("1.0 kW"); - expect(wrapper.find("[data-test-pv-production]").text()).toMatch("5.0 kW"); - expect(wrapper.find("[data-test-battery]").exists()).toBe(false); - }); - - it("more grid export than pv, grid value wins (invalid state)", async () => { - const wrapper = shallowMount(Energyflow, { - mocks, - propsData: { ...defaultProps, gridPower: -4000, pvPower: 3000 }, - }); - - await wrapper.find(".energyflow").trigger("click"); - - expect(wrapper.find("[data-test-grid-import]").text()).toMatch("0.0 kW"); - expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("0.0 kW"); - expect(wrapper.find("[data-test-pv-export]").text()).toMatch("4.0 kW"); - - expect(wrapper.find("[data-test-home-power]").text()).toMatch("0.0 kW"); - expect(wrapper.find("[data-test-loadpoints]").text()).toMatch("0.0 kW"); - expect(wrapper.find("[data-test-pv-production]").text()).toMatch("3.0 kW"); - expect(wrapper.find("[data-test-battery]").exists()).toBe(false); - }); - - it("more loadpoints power than grid import, loadpoints unchanged, house consumption min 0 (invalid state)", async () => { - const wrapper = shallowMount(Energyflow, { - mocks, - propsData: { ...defaultProps, gridPower: 6000, pvPower: 0, loadpointsPower: 7000 }, - }); - - await wrapper.find(".energyflow").trigger("click"); - - expect(wrapper.find("[data-test-grid-import]").text()).toMatch("6.0 kW"); - expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("0.0 kW"); - expect(wrapper.find("[data-test-pv-export]").text()).toMatch("0.0 kW"); - - expect(wrapper.find("[data-test-home-power]").text()).toMatch("0.0 kW"); - expect(wrapper.find("[data-test-loadpoints]").text()).toMatch("7.0 kW"); - expect(wrapper.find("[data-test-pv-production]").text()).toMatch("0.0 kW"); - expect(wrapper.find("[data-test-battery]").exists()).toBe(false); - }); - - it("only grid usage, no pv, idleBattery", async () => { - const wrapper = shallowMount(Energyflow, { - mocks, - propsData: { - ...defaultProps, - gridPower: 360, - pvPower: 0, - batteryConfigured: true, - batteryPower: 0, - homePower: 360, - }, - }); - - await wrapper.find(".energyflow").trigger("click"); - - expect(wrapper.find("[data-test-grid-import]").text()).toMatch("360 W"); - expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("0 W"); - expect(wrapper.find("[data-test-pv-export]").text()).toMatch("0 W"); - - expect(wrapper.find("[data-test-home-power]").text()).toMatch("360 W"); - expect(wrapper.find("[data-test-pv-production]").text()).toMatch("0 W"); - expect(wrapper.find("[data-test-battery]").text()).toMatch("main.energyflow.battery"); - }); - - it("grid and battery usage, no pv", async () => { - const wrapper = shallowMount(Energyflow, { - mocks, - propsData: { - ...defaultProps, - gridPower: 300, - batteryConfigured: true, - batteryPower: 234, - batterySoC: 77, - pvPower: 0, - homePower: 534, - }, - }); - - await wrapper.find(".energyflow").trigger("click"); - - expect(wrapper.find("[data-test-grid-import]").text()).toMatch("300 W"); - expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("234 W"); - expect(wrapper.find("[data-test-pv-export]").text()).toMatch("0 W"); - - expect(wrapper.find("[data-test-home-power]").text()).toMatch("534 W"); - expect(wrapper.find("[data-test-pv-production]").text()).toMatch("0 W"); - expect(wrapper.find("[data-test-battery]").text()).toMatch("234 W"); - expect(wrapper.find("[data-test-battery]").text()).toMatch("77%"); - expect(wrapper.find("[data-test-battery]").text()).toMatch("main.energyflow.batteryDischarge"); - }); - - it("battery charge, pv export", async () => { - const wrapper = shallowMount(Energyflow, { - mocks, - propsData: { - ...defaultProps, - gridPower: -2500, - batteryConfigured: true, - batteryPower: -1700, - pvPower: 9000, - homePower: 4800, - }, - }); - - await wrapper.find(".energyflow").trigger("click"); - - expect(wrapper.find("[data-test-grid-import]").text()).toMatch("0.0 kW"); - expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("6.5 kW"); - expect(wrapper.find("[data-test-pv-export]").text()).toMatch("2.5 kW"); - - expect(wrapper.find("[data-test-home-power]").text()).toMatch("4.8 kW"); - expect(wrapper.find("[data-test-pv-production]").text()).toMatch("9.0 kW"); - expect(wrapper.find("[data-test-battery]").text()).toMatch("1.7 kW"); - expect(wrapper.find("[data-test-battery]").text()).toMatch("main.energyflow.batteryCharge"); - }); - - it("thresholds", async () => { - const wrapper = shallowMount(Energyflow, { - mocks, - propsData: { - ...defaultProps, - gridPower: 5555, - batteryConfigured: true, - batteryPower: 1234, - pvPower: 378, - homePower: 7200, - }, - }); - - await wrapper.find(".energyflow").trigger("click"); - - expect(wrapper.find("[data-test-grid-import]").text()).toMatch("5.6 kW"); - expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("1.6 kW"); - expect(wrapper.find("[data-test-pv-export]").text()).toMatch("0.0 kW"); - - expect(wrapper.find("[data-test-home-power]").text()).toMatch("7.2 kW"); - expect(wrapper.find("[data-test-pv-production]").text()).toMatch("0.4 kW"); - expect(wrapper.find("[data-test-battery]").text()).toMatch("1.2 kW"); - expect(wrapper.find("[data-test-battery]").text()).toMatch("main.energyflow.batteryDischarge"); - }); - - it("battery charge, grid import", async () => { - const wrapper = shallowMount(Energyflow, { - mocks, - propsData: { - ...defaultProps, - gridPower: 1500, - batteryConfigured: true, - batteryPower: -1000, - homePower: 500, - pvPower: 0, - }, - }); - - await wrapper.find(".energyflow").trigger("click"); - - expect(wrapper.find("[data-test-grid-import]").text()).toMatch("1.5 kW"); - expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("0.0 kW"); - expect(wrapper.find("[data-test-pv-export]").text()).toMatch("0.0 kW"); - - expect(wrapper.find("[data-test-home-power]").text()).toMatch("0.5 kW"); - expect(wrapper.find("[data-test-pv-production]").text()).toMatch("0.0 kW"); - expect(wrapper.find("[data-test-battery]").text()).toMatch("1.0 kW"); - expect(wrapper.find("[data-test-battery]").text()).toMatch("main.energyflow.batteryCharge"); - }); -}); diff --git a/assets/js/components/Energyflow/Energyflow.stories.js b/assets/js/components/Energyflow/Energyflow.stories.js index 84b977b64..4bc6bcdd2 100644 --- a/assets/js/components/Energyflow/Energyflow.stories.js +++ b/assets/js/components/Energyflow/Energyflow.stories.js @@ -1,5 +1,4 @@ import Energyflow from "./"; -import i18n from "../../i18n"; export default { title: "Main/Energyflow", @@ -13,13 +12,19 @@ export default { batteryPower: { control: { type: "range", min: -4000, max: 4000, step: 100 } }, batterySoC: { control: { type: "range", min: 0, max: 100, step: 1 } }, }, + parameters: { + backgrounds: { + default: "light", + }, + }, }; -const Template = (args, { argTypes }) => ({ - i18n, - props: Object.keys(argTypes), +const Template = (args) => ({ + setup() { + return { args }; + }, components: { Energyflow }, - template: '', + template: '', }); export const GridAndPV = Template.bind({}); diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue index 38c208722..abe8db599 100644 --- a/assets/js/components/Energyflow/Energyflow.vue +++ b/assets/js/components/Energyflow/Energyflow.vue @@ -1,12 +1,12 @@ diff --git a/assets/js/components/Energyflow/EnergyflowEntry.vue b/assets/js/components/Energyflow/EnergyflowEntry.vue new file mode 100644 index 000000000..70d429d38 --- /dev/null +++ b/assets/js/components/Energyflow/EnergyflowEntry.vue @@ -0,0 +1,55 @@ + + + + diff --git a/assets/js/components/Energyflow/GridIcon.vue b/assets/js/components/Energyflow/GridIcon.vue deleted file mode 100644 index def24db58..000000000 --- a/assets/js/components/Energyflow/GridIcon.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - - diff --git a/assets/js/components/Energyflow/LabelBar.vue b/assets/js/components/Energyflow/LabelBar.vue index 0a9665183..7d7241834 100644 --- a/assets/js/components/Energyflow/LabelBar.vue +++ b/assets/js/components/Energyflow/LabelBar.vue @@ -35,8 +35,7 @@ export default { .label-bar { width: 0; margin: 0; - height: 1.7rem; - padding: 0.6rem 0; + padding: 10px 0; opacity: 1; overflow: hidden; } @@ -44,51 +43,50 @@ export default { opacity: 0; } .label-bar-scale { - border: 1px solid var(--bs-gray); - height: 0.5rem; + border: 1px solid var(--bs-gray-medium); + height: 14px; background: none; display: flex; justify-content: center; align-items: center; white-space: nowrap; border-radius: 0; - transition: border-radius 500ms linear; + transition: border-radius var(--evcc-transition-medium) linear; } .label-bar--top .label-bar-scale { + border-top-left-radius: 10px; + border-top-right-radius: 10px; border-bottom: none; } .label-bar--bottom .label-bar-scale { + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; border-top: none; } -.label-bar--top.label-bar--first .label-bar-scale { - border-top-left-radius: 4px; -} -.label-bar--top.label-bar--last .label-bar-scale { - border-top-right-radius: 4px; -} -.label-bar--bottom.label-bar--first .label-bar-scale { - border-bottom-left-radius: 4px; -} -.label-bar--bottom.label-bar--last .label-bar-scale { - border-bottom-right-radius: 4px; -} -.label-bar:not(.label-bar--first) .label-bar-scale { - border-left: none; -} .label-bar-icon { background-color: white; - color: var(--bs-gray); - padding: 0 0.3rem; - opacity: 1; + transform: scale(1); + color: var(--bs-gray-dark); + border-radius: 0; + border: 0.25rem solid white; + transition-property: background-color, transform, border-radius, border; + /* will be overwritten by parent component to avoid initial transition */ + transition-duration: 0s; + transition-delay: 0s; + transition-timing-function: linear; } .label-bar--top .label-bar-icon { - margin-top: -6px; + margin-top: -12px; } .label-bar--bottom .label-bar-icon { - margin-top: 6px; + margin-top: 12px; } .label-bar--hide-icon .label-bar-icon { - opacity: 0; + background-color: var(--bs-gray-dark); + transform: scale(0.1666666); + border-radius: 100%; + border-width: 1.5rem; + transition-delay: 400ms, 0s; } .label-bar--hidden { opacity: 0; diff --git a/assets/js/components/Energyflow/Visualization.vue b/assets/js/components/Energyflow/Visualization.vue index c35b9b369..d84e0bad8 100644 --- a/assets/js/components/Energyflow/Visualization.vue +++ b/assets/js/components/Energyflow/Visualization.vue @@ -1,17 +1,18 @@ diff --git a/assets/js/components/Footer.stories.js b/assets/js/components/Footer.stories.js index f3b72efef..fc4598a89 100644 --- a/assets/js/components/Footer.stories.js +++ b/assets/js/components/Footer.stories.js @@ -1,5 +1,4 @@ import Footer from "./Footer.vue"; -import i18n from "../i18n"; export default { title: "Main/Footer", @@ -7,11 +6,12 @@ export default { argTypes: {}, }; -const Template = (args, { argTypes }) => ({ - i18n, - props: Object.keys(argTypes), +const Template = (args) => ({ + setup() { + return { args }; + }, components: { Footer }, - template: '', + template: '', }); export const KeinUpdate = Template.bind({}); @@ -84,18 +84,3 @@ Upgrade.args = { selfConsumptionPercent: 0, }, }; - -export const Savings = Template.bind({}); -Savings.args = { - version: { - installed: "0.36", - available: "0.40", - hasUpdater: true, - }, - savings: { - since: 82800, - totalCharged: 15231, - selfConsumptionCharged: 12231, - selfConsumptionPercent: 80.3, - }, -}; diff --git a/assets/js/components/Footer.vue b/assets/js/components/Footer.vue index ad51104c8..d6127955f 100644 --- a/assets/js/components/Footer.vue +++ b/assets/js/components/Footer.vue @@ -1,8 +1,16 @@ + + diff --git a/assets/js/components/LabelAndValue.vue b/assets/js/components/LabelAndValue.vue new file mode 100644 index 000000000..1e577c4f9 --- /dev/null +++ b/assets/js/components/LabelAndValue.vue @@ -0,0 +1,41 @@ + + + + diff --git a/assets/js/components/Loadpoint.stories.js b/assets/js/components/Loadpoint.stories.js index 1158bb6dc..1874c71b4 100644 --- a/assets/js/components/Loadpoint.stories.js +++ b/assets/js/components/Loadpoint.stories.js @@ -1,5 +1,4 @@ import Loadpoint from "./Loadpoint.vue"; -import i18n from "../i18n"; export default { title: "Main/Loadpoint", @@ -11,11 +10,12 @@ export default { }, }; -const Template = (args, { argTypes }) => ({ - i18n, - props: Object.keys(argTypes), +const Template = (args) => ({ + setup() { + return { args }; + }, components: { Loadpoint }, - template: '', + template: '', }); export const Base = Template.bind({}); @@ -29,9 +29,29 @@ Base.args = { vehicleTitle: "Mein Auto", enabled: true, connected: true, + mode: "pv", charging: true, vehicleSoC: 66, targetSoC: 90, + chargeCurrent: 7, + minCurrent: 6, + maxCurrent: 16, + activePhases: 2, +}; + +export const Idle = Template.bind({}); +Idle.args = { + id: 0, + chargePower: 0, + vehiclePresent: false, + enabled: false, + connected: false, + mode: "off", + charging: false, + chargeCurrent: 0, + minCurrent: 6, + maxCurrent: 16, + activePhases: 0, }; export const Disabled = Template.bind({}); @@ -42,9 +62,16 @@ Disabled.args = { remoteDisabledSource: "Sunny Home Manager", vehiclePresent: true, vehicleTitle: "Mein Auto", + chargedEnergy: 31211, enabled: true, + mode: "now", connected: true, charging: false, + chargePower: 8112, vehicleSoC: 66, targetSoC: 100, + chargeCurrent: 7, + minCurrent: 6, + maxCurrent: 16, + activePhases: 3, }; diff --git a/assets/js/components/Loadpoint.vue b/assets/js/components/Loadpoint.vue index 995c745d6..a623d77c3 100644 --- a/assets/js/components/Loadpoint.vue +++ b/assets/js/components/Loadpoint.vue @@ -1,40 +1,92 @@ + + diff --git a/assets/js/components/LoadpointDetails.stories.js b/assets/js/components/LoadpointDetails.stories_.js similarity index 99% rename from assets/js/components/LoadpointDetails.stories.js rename to assets/js/components/LoadpointDetails.stories_.js index a919a80df..e4f19666e 100644 --- a/assets/js/components/LoadpointDetails.stories.js +++ b/assets/js/components/LoadpointDetails.stories_.js @@ -1,6 +1,5 @@ import LoadpointDetails from "./LoadpointDetails.vue"; import i18n from "../i18n"; -import "../tooltip"; export default { title: "Main/LoadpointDetails", diff --git a/assets/js/components/LoadpointDetails.vue b/assets/js/components/LoadpointDetails.vue deleted file mode 100644 index ce09c4747..000000000 --- a/assets/js/components/LoadpointDetails.vue +++ /dev/null @@ -1,201 +0,0 @@ - - - diff --git a/assets/js/components/Loadpoints.vue b/assets/js/components/Loadpoints.vue new file mode 100644 index 000000000..a0dfff1c8 --- /dev/null +++ b/assets/js/components/Loadpoints.vue @@ -0,0 +1,172 @@ + + + + diff --git a/assets/js/components/Mode.stories.js b/assets/js/components/Mode.stories.js index 96a080890..f6de771e9 100644 --- a/assets/js/components/Mode.stories.js +++ b/assets/js/components/Mode.stories.js @@ -1,27 +1,20 @@ import Mode from "./Mode.vue"; -import i18n from "../i18n"; export default { title: "Main/Mode", component: Mode, argTypes: { - mode: { control: { type: "inline-radio", options: ["off", "now", "minpv", "pv"] } }, + mode: { control: { type: "inline-radio" }, options: ["off", "now", "minpv", "pv"] }, }, }; -const Template = (args, { argTypes }) => ({ - i18n, - props: Object.keys(argTypes), +const Template = (args) => ({ + setup() { + return { args }; + }, components: { Mode }, - template: '', + template: '', }); export const Base = Template.bind({}); -Base.args = {}; - -export const CaptionAndPV = Template.bind({}); -CaptionAndPV.args = { - caption: true, - pvConfigured: true, - mode: "pv", -}; +Base.args = { mode: "pv" }; diff --git a/assets/js/components/Mode.vue b/assets/js/components/Mode.vue index 21108a8e0..4960f2141 100644 --- a/assets/js/components/Mode.vue +++ b/assets/js/components/Mode.vue @@ -1,42 +1,28 @@ @@ -46,18 +32,49 @@ export default { props: { mode: String, }, + emits: ["updated"], + data() { + return { + modes: ["off", "now", "minpv", "pv"], + }; + }, methods: { + isActive: function (mode) { + return this.mode === this.mapToOldModes(mode); + }, setTargetMode: function (mode) { - this.$emit("updated", mode); + this.$emit("updated", this.mapToOldModes(mode)); + }, + mapToOldModes: function (mode) { + const mapping = { fast: "now", cheap: "pv" }; + return mapping[mode] || mode; }, }, }; + + diff --git a/assets/js/components/Notifications.stories.js b/assets/js/components/Notifications.stories.js index d4a3d3556..a3d33337f 100644 --- a/assets/js/components/Notifications.stories.js +++ b/assets/js/components/Notifications.stories.js @@ -1,5 +1,4 @@ import Notifications from "./Notifications.vue"; -import i18n from "../i18n"; export default { title: "Main/Notifications", @@ -7,11 +6,12 @@ export default { argTypes: {}, }; -const Template = (args, { argTypes }) => ({ - i18n, - props: Object.keys(argTypes), +const Template = (args) => ({ + setup() { + return { args }; + }, components: { Notifications }, - template: '', + template: '', }); function timeAgo(hours = 0, minutes = 0, seconds = 0) { diff --git a/assets/js/components/Notifications.vue b/assets/js/components/Notifications.vue index d28834e81..7d77ff595 100644 --- a/assets/js/components/Notifications.vue +++ b/assets/js/components/Notifications.vue @@ -5,17 +5,20 @@ href="#" data-bs-toggle="modal" data-bs-target="#notificationModal" - class="btn btn-link text-decoration-none link-light text-nowrap" + class="btn btn-sm btn-link text-decoration-none link-light text-nowrap" > - +