UI: Add Light- and Dark-Mode (#3720)

This commit is contained in:
Michael Geers 2022-06-27 16:18:58 +02:00 • committed by GitHub
parent 89c908b414
commit 06e688c28b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 14384 additions and 9080 deletions

View file

@ -1,23 +1,16 @@
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"],
stories: ["../assets/js/**/*.stories.js"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
],
framework: "@storybook/vue3",
core: {
builder: "storybook-builder-vite",
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;
features: {
storyStoreV7: true,
},
};

View file

@ -1,9 +0,0 @@
const express = require("express");
const expressMiddleWare = (router) => {
router.post("/update", function (req, res) {
res.status(200).send("ok");
});
};
module.exports = expressMiddleWare;

View file

@ -0,0 +1,3 @@
<script>
window.global = window;
</script>

View file

@ -10,16 +10,22 @@ app.use(i18n);
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
backgrounds: {
default: "dark",
default: "background",
values: [
{
name: "light",
value: "#ffffff",
name: "background",
value: "var(--evcc-background)",
},
{
name: "dark",
value: "#28293e",
name: "box",
value: "var(--evcc-box)",
},
],
},

View file

@ -14,6 +14,8 @@ RUN make install-ui
COPY assets assets
COPY vite.config.js vite.config.js
COPY .eslintrc.js .eslintrc.js
COPY postcss.config.js postcss.config.js
RUN make clean ui

View file

@ -11,24 +11,47 @@
font-style: normal;
}
/* Bootstrap Breakpoints as Custom Media Queries */
@custom-media --sm-and-up (min-width: 576px);
@custom-media --md-and-up (min-width: 768px);
@custom-media --lg-and-up (min-width: 992px);
@custom-media --xl-and-up (min-width: 1200px);
@custom-media --xxl-and-up (min-width: 1400px);
@custom-media --light-mode (prefers-color-scheme: light);
@custom-media --dark-mode (prefers-color-scheme: dark);
:root {
--evcc-green: #baffcb;
--evcc-dark-green: #0fde41;
--evcc-darker-green: #0ba631;
--evcc-darkest-green: #076f20;
--evcc-yellow: #faf000;
--evcc-dark-yellow: #bbb400;
--bs-gray-deep: #010322;
--bs-gray-dark: #28293e;
--bs-gray-medium: #93949e;
--bs-gray-light: #b5b6c3;
--bs-gray-bright: #f3f3f7;
--evcc-grid: var(--bs-gray-dark);
--evcc-self: var(--evcc-dark-green);
--evcc-export: var(--evcc-yellow);
--evcc-background: var(--bs-gray-bright);
--evcc-box: var(--bs-white);
--evcc-default-text: var(--bs-gray-dark);
--evcc-gray: var(--bs-gray-medium);
--evcc-accent1: var(--evcc-dark-yellow);
--evcc-accent2: var(--evcc-darker-green);
--evcc-accent3: var(--evcc-darkest-green);
--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: var(--evcc-darker-green);
--bs-primary-rgb: 58, 186, 44;
--bs-body-font-size: 14px;
@ -37,6 +60,20 @@
"Segoe UI Symbol", "Noto Color Emoji";
}
@media (--dark-mode) {
:root {
--evcc-grid: var(--bs-gray-medium);
--evcc-background: var(--bs-gray-deep);
--evcc-box: var(--bs-gray-dark);
--evcc-default-text: var(--bs-white);
--evcc-gray: var(--bs-gray-light);
--evcc-accent1: var(--evcc-yellow);
--evcc-accent2: var(--evcc-dark-green);
--evcc-accent3: var(--evcc-darker-green);
--bs-primary: var(--evcc-dark-green);
}
}
html {
/* prevents content jump when scrollbar is activate */
width: 100vw;
@ -44,7 +81,8 @@ html {
}
body {
background-color: var(--evcc-gray-dark);
background-color: var(--evcc-background);
color: var(--evcc-default-text);
}
h1,
@ -70,7 +108,11 @@ h4 {
}
a {
color: var(--evcc-dark-green);
color: var(--bs-primary);
}
a:hover {
color: var(--evcc-accent3);
}
/* reverse loading animation */
@ -87,33 +129,50 @@ a {
.btn-primary,
.btn-primary:focus {
background-color: var(--bs-gray-dark);
border-color: var(--bs-gray-dark);
background-color: var(--bs-primary);
border-color: var(--bs-primary);
color: var(--evcc-background);
}
.btn-primary:hover,
.btn-primary:active {
background-color: var(--evcc-dark-green);
border-color: var(--evcc-dark-green);
background-color: var(--evcc-accent3);
border-color: var(--evcc-accent3);
}
.btn-outline-primary,
.btn-outline-primary:focus {
color: var(--bs-gray-dark);
color: var(--bs-primary);
background-color: transparent;
border-width: 2px;
border-color: var(--bs-gray-dark);
border-color: var(--bs-primary);
}
.btn-outline-primary:hover,
.btn-outline-primary:active {
color: var(--evcc-dark-green);
color: var(--evcc-accent3);
background-color: transparent;
border-width: 2px;
border-color: var(--evcc-dark-green);
border-color: var(--evcc-accent3);
}
.text-evcc {
color: var(--evcc-dark-green);
}
.text-accent1 {
color: var(--evcc-accent1);
}
.text-accent2 {
color: var(--evcc-accent2);
}
.text-accent3 {
color: var(--evcc-accent3);
}
.evcc-default-text {
color: var(--evcc-default-text) !important;
}
.evcc-gray {
color: var(--evcc-gray);
}
.text-grid {
color: var(--evcc-grid);
}
@ -147,26 +206,39 @@ a {
}
.modal-header {
padding-bottom: 1.5rem;
padding: 0 0 1rem 0;
border: none;
}
.modal-title {
font-weight: bold;
font-size: 22px;
font-size: 1.25rem;
}
.modal-content {
border-radius: 1rem;
padding: 1rem;
padding: 1.25rem;
background-color: var(--evcc-box);
color: var(--evcc-default-text);
}
@media (--sm-and-up) {
.modal-content {
padding: 2rem;
}
}
.modal-body {
padding-top: 1.5rem;
padding-bottom: 1.5rem;
padding: 1rem 0 0;
}
.modal-footer {
padding-top: 1.5rem;
padding: 1rem 0 0;
border: none;
}
.modal-footer > * {
margin: 0;
}
.cursor-pointer {
@ -186,3 +258,25 @@ small {
.btn-close {
opacity: 1;
}
@media (--dark-mode) {
.btn-close {
filter: invert(1);
}
}
.dropdown-menu {
border: none;
border-radius: 10px;
box-shadow: 0 0 8px var(--bs-gray-light);
background-color: var(--evcc-box);
}
.dropdown-item {
color: var(--evcc-default-text);
}
@media (--dark-mode) {
.dropdown-menu {
box-shadow: 0 0 8px var(--evcc-box);
}
}

View file

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="evcc - Sonne tanken ☀️🚘" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="evcc" />
<meta name="application-name" content="evcc" />
@ -17,7 +17,7 @@
<link rel="shortcut icon" href="meta/favicon.ico?[[.Version]]" />
<meta name="msapplication-TileColor" content="#28293e" />
<meta name="msapplication-config" content="meta/browserconfig.xml?[[.Version]]" />
<meta name="theme-color" content="#28293e" />
<meta name="theme-color" content="#020318" />
<title>evcc</title>
</head>

View file

@ -73,3 +73,11 @@ window.app = app.mount("#app");
window.setInterval(function () {
api.get("health").then(window.app.setOnline).catch(window.app.setOffline);
}, 5000);
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)");
isDarkMode.addEventListener("change", updateThemeColor);
function updateThemeColor() {
const $el = document.querySelector("meta[name=theme-color]");
$el.setAttribute("content", isDarkMode.matches ? "#020318" : "#f3f3f7");
}
updateThemeColor();

View file

@ -12,11 +12,6 @@ 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) => ({

View file

@ -19,9 +19,6 @@
:valuesInKw="valuesInKw"
/>
</div>
<div class="indicator position-absolute bottom-0 start-50">
<shopicon-regular-arrowdown></shopicon-regular-arrowdown>
</div>
<div class="details" :style="{ height: detailsHeight }">
<div ref="detailsInner" class="details-inner row">
<div class="col-12 d-flex justify-content-between pt-2 mb-4">
@ -121,7 +118,6 @@
<script>
import "@h2d2/shopicons/es/filled/square";
import "@h2d2/shopicons/es/regular/arrowdown";
import Visualization from "./Visualization.vue";
import EnergyflowEntry from "./EnergyflowEntry.vue";
import formatter from "../../mixins/formatter";
@ -204,30 +200,19 @@ export default {
};
</script>
<style scoped>
.energyflow {
background: var(--bs-white);
}
.indicator {
opacity: 0;
transform: translateX(-50%) scaleY(1);
transition: opacity, transform;
transition-duration: var(--evcc-transition-slow);
}
.energyflow--open .indicator {
transform: translateX(-50%) scaleY(-1);
}
@media (hover: hover) and (pointer: fine) {
.energyflow:hover .indicator {
opacity: 0.25;
}
}
.details {
height: 0;
opacity: 0;
transform: scale(0.98);
overflow: visible;
transition: height;
transition: height, opacity, transform;
transition-duration: var(--evcc-transition-medium);
transition-timing-function: cubic-bezier(0.5, 0.5, 0.5, 1.15);
}
.energyflow--open .details {
opacity: 1;
transform: scale(1);
}
.color-grid {
color: var(--evcc-grid);
}

View file

@ -1,5 +1,5 @@
<template>
<div class="d-flex justify-content-between mb-2 entry" :class="{ 'text-gray-medium': !active }">
<div class="d-flex justify-content-between mb-2 entry" :class="{ 'evcc-gray': !active }">
<span class="d-flex flex-nowrap">
<BatteryIcon v-if="isBattery" :soc="soc" />
<component :is="`shopicon-regular-${icon}`" v-else></component>

View file

@ -43,7 +43,7 @@ export default {
opacity: 0;
}
.label-bar-scale {
border: 1px solid var(--bs-gray-medium);
border: 1px solid var(--evcc-gray);
height: 14px;
background: none;
display: flex;
@ -64,11 +64,11 @@ export default {
border-top: none;
}
.label-bar-icon {
background-color: white;
background-color: var(--evcc-background);
transform: scale(1);
color: var(--bs-gray-dark);
color: var(--evcc-default-text);
border-radius: 0;
border: 0.25rem solid white;
border: 0.25rem solid var(--evcc-background);
transition-property: background-color, transform, border-radius, border;
/* will be overwritten by parent component to avoid initial transition */
transition-duration: 0s;
@ -82,7 +82,7 @@ export default {
margin-top: 12px;
}
.label-bar--hide-icon .label-bar-icon {
background-color: var(--bs-gray-dark);
background-color: var(--evcc-default-text);
transform: scale(0.1666666);
border-radius: 100%;
border-width: 1.5rem;

View file

@ -193,7 +193,7 @@ export default {
margin-right: 1.2rem;
}
.label-scale-name {
color: var(--bs-gray-medium);
color: var(--evcc-gray);
flex-basis: 1.2rem;
flex-grow: 0;
flex-shrink: 0;
@ -218,6 +218,11 @@ export default {
background-color: var(--evcc-grid);
color: var(--bs-white);
}
@media (--dark-mode) {
.grid-import {
color: var(--bs-dark);
}
}
.self-consumption {
background-color: var(--evcc-self);
color: var(--bs-dark);

View file

@ -46,8 +46,4 @@ export default {
};
</script>
<style scoped>
.footer {
color: var(--bs-white);
}
</style>
<style scoped></style>

View file

@ -1,5 +1,5 @@
<template>
<div class="root" :class="{ 'root--dark': onDark }">
<div class="root">
<div class="mb-2 label">{{ label }}</div>
<slot>
<h3 class="value m-0">
@ -19,7 +19,6 @@ export default {
label: String,
value: String,
extraValue: String,
onDark: Boolean,
},
};
</script>
@ -29,19 +28,14 @@ export default {
}
.label {
text-transform: uppercase;
color: var(--bs-gray-medium);
color: var(--evcc-gray);
font-size: 14px;
}
.value {
font-size: 18px;
}
.extraValue {
color: var(--bs-gray-medium);
color: var(--evcc-gray);
font-size: 14px;
}
.root--dark .extraValue,
.root--dark .label {
color: var(--bs-gray-light);
}
</style>

View file

@ -1,11 +1,11 @@
<template>
<div class="loadpoint bg-white p-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="loadpoint pt-4 pb-2 px-3 px-sm-4 mx-2 mx-sm-0">
<div class="d-block d-sm-flex justify-content-between align-items-center mb-3">
<h3 class="mb-3 me-2 text-truncate">
{{ title || $t("main.loadpoint.fallbackName") }}
</h3>
<div class="mb-3 d-flex align-items-center">
<Mode :mode="mode" @updated="setTargetMode" />
<Mode class="w-100 w-sm-auto" :mode="mode" @updated="setTargetMode" />
<button v-if="$hiddenFeatures" class="btn btn-link text-gray p-0 flex-shrink-0">
<shopicon-filled-options size="s"></shopicon-filled-options>
</button>
@ -64,6 +64,7 @@
${fmtShortDurationUnit(chargeDurationInterpolated)}`"
/>
</div>
<hr class="divider" />
<Vehicle
v-bind="vehicle"
@target-soc-updated="setTargetSoC"
@ -214,7 +215,8 @@ export default {
<style scoped>
.loadpoint {
border-radius: 2rem;
color: var(--bs-gray-dark);
color: var(--evcc-default-text);
background: var(--evcc-box);
}
.details > div {
@ -230,4 +232,18 @@ export default {
.opacity-transiton {
transition: opacity var(--evcc-transition-slow) ease-in;
}
.divider {
border: none;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: var(--evcc-gray);
background: none;
opacity: 0.5;
margin: 0 -1rem;
}
@media (--sm-and-up) {
.divider {
margin: 0 -1.5rem;
}
}
</style>

View file

@ -21,7 +21,7 @@
<button
v-for="(loadpoint, index) in loadpoints"
:key="index"
class="btn btn-sm btn-link p-0 mx-1 indicator d-flex justify-content-center align-items-center text-white"
class="btn btn-sm btn-link p-0 mx-1 indicator d-flex justify-content-center align-items-center evcc-default-text"
:class="{ 'indicator--selected': selected(index) }"
@click="scrollTo(index)"
>

View file

@ -10,6 +10,7 @@
<path
d="M13.082 29.071a12.384 12.384 0 01-9-3.42 12.192 12.192 0 01-3.54-9.12v-.64a15.394 15.394 0 011.47-6.83 10.825 10.825 0 014.17-4.64 11.64 11.64 0 016.15-1.63 10.45 10.45 0 018.21 3.26c2 2.194 3 5.297 3 9.31v2.76H7.382a6.348 6.348 0 002 4 5.997 5.997 0 004.16 1.49 7.305 7.305 0 006.1-2.84l3.31 3.73a10 10 0 01-4.13 3.39 13.309 13.309 0 01-5.74 1.18zm-.77-20.84a4.216 4.216 0 00-3.26 1.37 7.141 7.141 0 00-1.6 3.91h9.39v-.55a5.005 5.005 0 00-1.22-3.49 4.304 4.304 0 00-3.31-1.24zM36.452 20.331l4.7-17.09h7l-8.48 25.36h-6.44l-8.52-25.36h7l4.74 17.09zM85.542 23.611a4.444 4.444 0 003-1 3.638 3.638 0 001.22-2.75h6.32a8.668 8.668 0 01-1.4 4.73 9.145 9.145 0 01-3.79 3.3 11.736 11.736 0 01-5.29 1.19 10.912 10.912 0 01-8.54-3.46c-2.087-2.3-3.13-5.483-3.13-9.55v-.45c0-3.9 1.033-7.016 3.1-9.35a10.868 10.868 0 018.51-3.5c2.791-.134 5.524.84 7.6 2.71a9.626 9.626 0 012.9 7.21h-6.3a4.663 4.663 0 00-1.2-3.22 4.005 4.005 0 00-3.08-1.24 4.068 4.068 0 00-3.56 1.73c-.8 1.15-1.2 3-1.2 5.6v.7c0 2.61.39 4.49 1.19 5.63a4.092 4.092 0 003.65 1.72zM110.422 23.611a4.454 4.454 0 003-1 3.63 3.63 0 001.21-2.75h6.33a8.668 8.668 0 01-1.4 4.73 9.143 9.143 0 01-3.73 3.3 11.76 11.76 0 01-5.29 1.18 10.912 10.912 0 01-8.54-3.46c-2.087-2.3-3.13-5.483-3.13-9.55v-.45c0-3.9 1.033-7.016 3.1-9.35a10.85 10.85 0 018.57-3.49 10.575 10.575 0 017.6 2.71 9.598 9.598 0 012.91 7.21h-6.33a4.651 4.651 0 00-1.21-3.22 4.492 4.492 0 00-6.64.49c-.8 1.15-1.21 3-1.21 5.6v.7c0 2.607.4 4.484 1.2 5.63a4.09 4.09 0 003.56 1.72z"
fill="#fff"
class="letter"
fill-rule="nonzero"
/>
<path
@ -26,3 +27,13 @@ export default {
name: "Logo",
};
</script>
<style scoped>
.letter {
fill: #18191a;
}
@media (--dark-mode) {
.letter {
fill: var(--bs-white);
}
}
</style>

View file

@ -6,6 +6,7 @@ export default {
argTypes: {
mode: { control: { type: "inline-radio" }, options: ["off", "now", "minpv", "pv"] },
},
parameters: { backgrounds: { default: "box" } },
};
const Template = (args) => ({

View file

@ -1,27 +1,14 @@
<template>
<div v-if="$hiddenFeatures" class="mode-group border d-inline-flex" role="group">
<button
v-for="m in ['fast', 'cheap']"
:key="m"
type="button"
class="btn"
:class="{ active: isActive(m) }"
@click="setTargetMode(m)"
>
{{ $t(`main.mode.${m}`) }}
</button>
</div>
<div v-else class="mode-group border d-inline-flex" role="group">
<div class="mode-group border d-inline-flex" role="group">
<button
v-for="m in modes"
:key="m"
type="button"
class="btn"
class="btn flex-grow-1 flex-shrink-1"
:class="{ active: isActive(m) }"
@click="setTargetMode(m)"
>
<span class="d-inline d-sm-none"> {{ $t(`main.mode.${m}Short`) }} </span>
<span class="d-none d-sm-inline"> {{ $t(`main.mode.${m}Long`) }} </span>
{{ $t(`main.mode.${m}`) }}
</button>
</div>
</template>
@ -35,30 +22,23 @@ export default {
emits: ["updated"],
data() {
return {
modes: ["off", "now", "minpv", "pv"],
modes: ["off", "minpv", "pv", "now"],
};
},
methods: {
isActive: function (mode) {
return this.mode === this.mapToOldModes(mode);
return this.mode === mode;
},
setTargetMode: function (mode) {
this.$emit("updated", this.mapToOldModes(mode));
},
mapToOldModes: function (mode) {
const mapping = { fast: "now", cheap: "pv" };
return mapping[mode] || mode;
this.$emit("updated", mode);
},
},
};
</script>
<style scoped></style>
<style scoped>
.mode-group {
border: 2px solid var(--bs-gray-medium);
background-color: var(--bs-white);
border: 2px solid var(--evcc-default-text);
border-radius: 20px;
padding: 4px;
}
@ -69,10 +49,14 @@ export default {
white-space: nowrap;
border-radius: 18px;
padding: 0.1em 0.8em;
color: var(--evcc-default-text);
}
.btn:hover {
color: var(--evcc-gray);
}
.btn.active {
background: var(--bs-gray-dark);
color: var(--bs-white);
color: var(--evcc-background);
background: var(--evcc-default-text);
}
.btn-group {
border-radius: 16px;

View file

@ -4,11 +4,7 @@ export default {
title: "Main/Phases",
component: Phases,
argTypes: {},
parameters: {
backgrounds: {
default: "light",
},
},
parameters: { backgrounds: { default: "box" } },
};
const Template = (args, { argTypes }) => ({

View file

@ -46,13 +46,18 @@ export default {
width: 73px;
}
.phase {
background-color: var(--bs-gray-200);
background-color: var(--bs-gray-bright);
height: 4px;
flex-grow: 1;
position: relative;
border-radius: 1px;
overflow: hidden;
}
@media (--dark-mode) {
.phase {
background-color: var(--bs-gray-bright);
}
}
.phase.inactive {
display: none;
}

View file

@ -1,7 +1,7 @@
<template>
<div>
<button
class="btn btn-link pe-0 text-decoration-none text-white text-nowrap d-flex align-items-end"
class="btn btn-link pe-0 text-decoration-none evcc-default-text text-nowrap d-flex align-items-end"
data-bs-toggle="modal"
data-bs-target="#savingsModal"
>
@ -31,18 +31,7 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<span class="text-nowrap">
{{
$t("footer.savings.modalTitlePartOne", {
total: fmtKw(totalCharged * 1000, true, false),
})
}}
·
</span>
{{ " " }}
<span class="text-nowrap">
{{ $t("footer.savings.modalTitlePartTwo", { percent }) }}
</span>
{{ $t("footer.savings.modalTitle") }}
</h5>
<button
type="button"
@ -51,108 +40,66 @@
aria-label="Close"
></button>
</div>
<div class="modal-body py-4">
<div class="chart-container mb-3">
<div
class="chart-legend d-flex flex-wrap justify-content-between mb-1"
>
<div
class="text-nowrap"
:class="{ 'text-gray-medium': noData }"
>
<shopicon-filled-square
class="text-evcc d-inline-block"
:class="{ 'text-gray-medium': noData }"
></shopicon-filled-square>
{{
$t("footer.savings.modalChartSelf", {
self: fmtKw(
selfConsumptionCharged * 1000,
true,
false
),
})
}}
</div>
<div
class="text-nowrap"
:class="{ 'text-gray-medium': noData }"
>
{{
$t("footer.savings.modalChartGrid", {
grid: fmtKw(gridCharged * 1000, true, false),
})
}}
<shopicon-filled-square
class="text-grid d-inline-block"
:class="{ 'text-gray-medium': noData }"
></shopicon-filled-square>
</div>
</div>
<div
class="chart d-flex justify-content-stretch mb-1 rounded overflow-hidden"
>
<div
v-if="!noData"
class="chart-item chart-item--self d-flex justify-content-center align-items-center text-white flex-shrink-1"
:style="{ width: `${percent}%` }"
>
<span class="text-truncate"> {{ percent }}% </span>
</div>
<div
v-if="!noData"
class="chart-item chart-item--grid d-flex justify-content-center align-items-center text-white flex-shrink-1"
:style="{ width: `${100 - percent}%` }"
>
<span class="text-truncate"> {{ 100 - percent }}% </span>
</div>
<div
v-if="noData"
class="chart-item chart-item--no-data d-flex justify-content-center align-items-center text-white w-100"
>
<span>{{ $t("footer.savings.modalNoData") }}</span>
</div>
</div>
</div>
<p class="mb-3">
{{ $t("footer.savings.modalSavingsPrice") }}:
<strong>{{ fmtPricePerKWh(effectivePrice, currency) }}</strong>
<br />
{{ $t("footer.savings.modalSavingsTotal") }}:
<strong>{{ fmtMoney(amount, currency) }}</strong>
</p>
<p class="small text-muted mb-3">
<a
href="https://docs.evcc.io/docs/guides/setup/#ersparnisberechnung"
target="_blank"
class="small"
>
{{ $t("footer.savings.modalExplaination") }}</a
>:
<span class="text-nowrap">
{{
$t("footer.savings.modalExplainationGrid", {
gridPrice: fmtPricePerKWh(gridPrice, currency),
})
}}</span
>,
<span class="text-nowrap">
{{
$t("footer.savings.modalExplainationFeedIn", {
feedInPrice: fmtPricePerKWh(feedInPrice, currency),
})
}}
</span>
<br />
<div class="modal-body">
<p>
{{
$t("footer.savings.modalServerStart", {
$t("footer.savings.sinceServerStart", {
since: fmtTimeAgo(secondsSinceStart()),
})
}}
</p>
<hr class="mb-4" />
<div class="d-block d-lg-flex mb-4">
<SavingsTile
class="text-accent1"
icon="sun"
:title="$t('footer.savings.percentTitle')"
:value="percent"
unit="%"
:sub1="
$t('footer.savings.percentSelf', {
self: fmtKw(selfConsumptionCharged * 1000, true, false),
})
"
:sub2="
$t('footer.savings.percentGrid', {
grid: fmtKw(gridCharged * 1000, true, false),
})
"
/>
<SavingsTile
class="text-accent2"
icon="receivepayment"
:title="$t('footer.savings.priceTitle')"
:value="effectivePriceFormatted.value"
:unit="effectivePriceFormatted.unit"
:sub1="
$t('footer.savings.priceFeedIn', {
feedInPrice: fmtPricePerKWh(feedInPrice, currency),
})
"
:sub2="
$t('footer.savings.priceGrid', {
gridPrice: fmtPricePerKWh(gridPrice, currency),
})
"
/>
<SavingsTile
class="text-accent3"
icon="coinjar"
:title="$t('footer.savings.savingsTitle')"
:value="fmtMoney(amount, currency)"
:unit="fmtCurrencySymbol(currency)"
:sub1="$t('footer.savings.savingsComparedToGrid')"
:sub2="
$t('footer.savings.savingsTotalEnergy', {
total: fmtKw(totalCharged * 1000, true, false),
})
"
/>
</div>
<Sponsor :sponsor="sponsor" />
</div>
@ -166,12 +113,11 @@
<script>
import formatter from "../mixins/formatter";
import Sponsor from "./Sponsor.vue";
import "@h2d2/shopicons/es/filled/square";
import "@h2d2/shopicons/es/regular/sun";
import SavingsTile from "./SavingsTile.vue";
export default {
name: "Savings",
components: { Sponsor },
components: { Sponsor, SavingsTile },
mixins: [formatter],
props: {
selfConsumptionPercent: Number,
@ -190,8 +136,11 @@ export default {
percent() {
return Math.round(this.selfConsumptionPercent) || 0;
},
noData() {
return this.totalCharged === 0;
effectivePriceFormatted() {
const [value, unit] = this.fmtPricePerKWh(this.effectivePrice, this.currency).split(
" "
);
return { value, unit };
},
},
methods: {
@ -202,28 +151,6 @@ export default {
};
</script>
<style scoped>
/* make modal a bottom drawer on small screens */
@media (max-width: 575px) {
.modal-dialog.modal-dialog-centered {
align-items: flex-end;
margin-bottom: 0;
}
.modal.fade .modal-dialog {
transition: transform var(--evcc-transition-medium) ease;
transform: translate(0, 150px);
}
.modal.show .modal-dialog {
transform: none;
}
.modal-dialog-scrollable {
height: calc(100% - 0.5rem);
}
.modal-content {
border-end-end-radius: 0;
border-end-start-radius: 0;
}
}
.chart {
height: 2.5rem;
}
@ -243,4 +170,7 @@ export default {
transition-duration: var(--evcc-transition-medium);
transition-timing-function: linear;
}
.tile-icon {
width: 70px;
}
</style>

View file

@ -0,0 +1,60 @@
<template>
<div class="flex-grow-1 d-flex mb-4 mb-lg-0 align-items-center align-items-lg-start">
<shopicon-regular-sun v-if="icon === 'sun'" class="tile-icon"></shopicon-regular-sun>
<shopicon-regular-coinjar
v-if="icon === 'coinjar'"
class="tile-icon"
></shopicon-regular-coinjar>
<shopicon-regular-receivepayment
v-if="icon === 'receivepayment'"
class="tile-icon"
></shopicon-regular-receivepayment>
<div class="ms-3 d-flex flex-grow-1 d-lg-block align-items-center justify-content-between">
<div>
<p class="my-0 fw-bold">{{ title }}</p>
<strong class="d-flex align-items-baseline lh-sm">
<span class="fs-1">{{ value }}</span>
<span class="ms-1 unit">{{ unit }}</span>
</strong>
</div>
<small
class="d-block mt-0 ms-3 ms-lg-0 text-end text-lg-start flex-grow-1 flex-shrink-1"
>
{{ sub1 }} <br />
{{ sub2 }}
</small>
</div>
</div>
</template>
<script>
import "@h2d2/shopicons/es/regular/coinjar";
import "@h2d2/shopicons/es/regular/sun";
import "@h2d2/shopicons/es/regular/receivepayment";
export default {
name: "SavingsTile",
props: {
title: String,
icon: String,
value: String,
unit: String,
sub1: String,
sub2: String,
},
};
</script>
<style scoped>
.tile-icon {
width: 40px;
flex: 0 0 auto;
}
@media (--lg-and-up) {
.tile-icon {
width: 70px;
}
}
.unit {
font-size: var(--bs-body-font-size);
}
</style>

View file

@ -14,7 +14,7 @@
</div>
<Energyflow v-bind="energyflow" />
</div>
<div class="d-flex flex-column justify-content-between content-area pt-4">
<div class="d-flex flex-column justify-content-between content-area">
<Loadpoints class="mt-1 mt-sm-2 flex-grow-1" :loadpoints="loadpoints" />
<Vehicles v-if="$hiddenFeatures" />
<Footer v-bind="footer"></Footer>
@ -137,8 +137,6 @@ export default {
min-height: 100vh;
}
.content-area {
background-color: var(--bs-gray-dark);
color: var(--bs-white);
flex-grow: 1;
z-index: 1;
}

View file

@ -3,16 +3,18 @@
<p class="mb-3">
{{ $t("footer.sponsor.thanks", { sponsor }) }}
</p>
<div class="d-flex justify-content-center align-items-center flex-column">
<div
class="d-flex justify-content-center align-items-center flex-column flex-lg-row align-items-lg-baseline justify-content-lg-start"
>
<button
ref="confetti"
class="btn btn btn-outline-primary mb-2 confetti-button bg-evcc w-75 rounded"
class="btn btn btn-outline-primary mb-2 confetti-button bg-evcc rounded"
@click="surprise"
>
<shopicon-regular-heart class="me-1 d-inline-block"></shopicon-regular-heart>
<shopicon-regular-stars class="me-1 d-inline-block"></shopicon-regular-stars>
{{ $t("footer.sponsor.confetti") }}
</button>
<a href="https://evcc.io/sticker" target="_blank" class="small text-muted">
<a href="https://evcc.io/sticker" target="_blank" class="small text-muted ms-lg-3">
{{ $t("footer.sponsor.sticker") }}
</a>
</div>
@ -21,16 +23,18 @@
<p class="mb-3">
{{ $t("footer.sponsor.supportUs") }}
</p>
<div class="d-flex justify-content-center align-items-center flex-column">
<div
class="d-flex justify-content-center align-items-center flex-column flex-lg-row align-items-lg-baseline justify-content-lg-start"
>
<a
target="_blank"
href="https://github.com/sponsors/andig"
class="btn btn-outline-primary mb-2 w-75"
class="btn btn-outline-primary mb-3 become-sponsor"
>
<shopicon-regular-heart class="me-1 d-inline-block"></shopicon-regular-heart>
{{ $t("footer.sponsor.becomeSponsor") }}
</a>
<div class="small text-muted text-center">
<div class="small text-muted text-center ms-lg-3">
{{ $t("footer.sponsor.confettiPromise") }}
</div>
</div>
@ -40,7 +44,7 @@
<script>
import confetti from "canvas-confetti";
import "@h2d2/shopicons/es/regular/heart";
import "@h2d2/shopicons/es/filled/heart";
import "@h2d2/shopicons/es/regular/stars";
export default {
name: "Sponsor",
@ -90,4 +94,20 @@ export default {
touch-action: none;
user-select: none;
}
.confetti-button,
.become-sponsor {
width: 100%;
}
@media (--sm-and-up) {
.confetti-button,
.become-sponsor {
width: 75%;
}
}
@media (--lg-and-up) {
.confetti-button,
.become-sponsor {
width: 40%;
}
}
</style>

View file

@ -1,11 +1,16 @@
<template>
<div>
<LabelAndValue class="flex-grow-1" :label="$t('main.targetCharge.title')" on-dark>
<LabelAndValue
class="root flex-grow-1"
:label="$t('main.targetCharge.title')"
:class="disabled ? 'opacity-0' : 'opacity-1'"
>
<button
class="btn btn-link p-0 value text-end"
:class="buttonIsEnabled ? 'text-white' : 'text-gray-light'"
class="btn btn-link p-0 value text-center"
:class="targetChargeEnabled ? 'evcc-default-text' : 'text-gray-light'"
data-bs-toggle="modal"
:data-bs-target="`#${modalId}`"
:disabled="disabled"
>
<strong v-if="targetChargeEnabled">{{ targetTimeLabel() }}</strong>
<span v-else>{{ $t("main.targetCharge.setTargetTime") }}</span>
@ -142,9 +147,6 @@ export default {
targetChargeEnabled: function () {
return this.targetTime;
},
buttonIsEnabled: function () {
return this.targetChargeEnabled && !this.disabled;
},
selectedTargetTimeValid: function () {
const now = new Date();
return now < this.selectedTargetTime;
@ -253,7 +255,9 @@ export default {
font-size: 18px;
line-height: 1.2;
border: none;
transition: color var(--evcc-transition-medium) linear;
}
.root {
transition: opacity var(--evcc-transition-medium) linear;
}
.value:hover {
color: var(--bs-color-white);

View file

@ -1,5 +1,5 @@
<template>
<LabelAndValue class="flex-grow-1" :label="$t('main.vehicle.targetSoC')" :on-dark="true">
<LabelAndValue class="flex-grow-1" :label="$t('main.vehicle.targetSoC')">
<h3 class="value m-0">
<label class="d-inline-block position-relative">
<select :value="targetSoc" class="custom-select" @change="change">
@ -48,7 +48,7 @@ export default {
},
methods: {
change: function (e) {
return this.$emit("target-soc-updated", e.target.value);
return this.$emit("target-soc-updated", parseInt(e.target.value, 10));
},
estimatedRange: function (soc) {
if (this.rangePerSoc) {

View file

@ -7,7 +7,7 @@
aria-controls="navbarNavAltMarkup"
aria-expanded="false"
aria-label="Toggle navigation"
class="btn btn-sm btn-outline-secondary position-relative"
class="btn btn-sm btn-outline-secondary position-relative border-0 menu-button"
>
<span
v-if="logoutCount > 0"
@ -102,3 +102,8 @@ export default {
},
};
</script>
<style scoped>
.menu-button {
margin-right: -0.7rem;
}
</style>

View file

@ -3,6 +3,7 @@ import Vehicle from "./Vehicle.vue";
export default {
title: "Main/Vehicle",
component: Vehicle,
parameters: { backgrounds: { default: "box" } },
};
const Template = (args) => ({
@ -11,6 +12,7 @@ const Template = (args) => ({
},
components: { Vehicle },
template: '<Vehicle v-bind="args"></Vehicle>',
parameters: { backgrounds: { default: "box" } },
});
export const Base = Template.bind({});

View file

@ -1,5 +1,5 @@
<template>
<div class="vehicle p-4 pb-3">
<div class="vehicle pt-4">
<VehicleTitle v-bind="vehicleTitleProps" />
<VehicleStatus v-if="!parked" v-bind="vehicleStatus" class="mb-2" />
<VehicleSoc
@ -13,23 +13,22 @@
<LabelAndValue
class="flex-grow-1 text-start"
:label="$t('main.vehicle.vehicleSoC')"
:value="`${vehicleSoC || '--'} %`"
:value="vehicleSoC ? `${vehicleSoC}%` : '--'"
:extraValue="vehicleRange ? `${vehicleRange} km` : null"
on-dark
/>
<TargetSoCSelect
class="flex-grow-1 text-center"
:target-soc="displayTargetSoC"
:range-per-soc="rangePerSoC"
@target-soc-updated="targetSocUpdated"
/>
<TargetCharge
class="flex-grow-1 text-end target-charge"
class="flex-grow-1 text-center target-charge"
v-bind="targetCharge"
:disabled="targetChargeDisabled"
@target-time-updated="setTargetTime"
@target-time-removed="removeTargetTime"
/>
<TargetSoCSelect
class="flex-grow-1 text-end"
:target-soc="displayTargetSoC"
:range-per-soc="rangePerSoC"
@target-soc-updated="targetSocUpdated"
/>
</div>
<div v-if="$hiddenFeatures" class="d-flex justify-content-start">
<small>vor 5 Stunden</small>
@ -132,11 +131,6 @@ export default {
</script>
<style scoped>
.vehicle {
background-color: var(--bs-gray-dark);
border-radius: 1rem;
color: var(--bs-white);
}
.car-icon {
width: 1.75rem;
}

View file

@ -101,21 +101,23 @@ export default {
e.stopPropagation();
},
changeTargetSoCEnd: function (e) {
const value = parseInt(e.target.value, 10);
// value changed
if (e.target.value !== this.targetSoC) {
this.$emit("target-soc-updated", e.target.value);
if (value !== this.targetSoC) {
this.$emit("target-soc-updated", value);
}
},
movedTargetSoC: function (e) {
let value = parseInt(e.target.value, 10);
e.stopPropagation();
const minTargetSoC = 20;
if (e.target.value < minTargetSoC) {
if (value < minTargetSoC) {
e.target.value = minTargetSoC;
this.selectedTargetSoC = e.target.value;
this.selectedTargetSoC = value;
e.preventDefault();
return false;
}
this.selectedTargetSoC = e.target.value;
this.selectedTargetSoC = value;
this.$emit("target-soc-drag", this.selectedTargetSoC);
return true;
@ -135,9 +137,10 @@ export default {
.progress {
height: 100%;
font-size: 1rem;
background: var(--evcc-background);
}
.progress-bar.bg-muted {
color: var(--white);
opacity: 0.5;
}
.bg-light {
color: var(--bs-gray-dark);
@ -179,7 +182,7 @@ export default {
border: none;
opacity: 1;
border-radius: var(--thumb-overlap);
box-shadow: 0 0 6px var(--bs-gray-dark);
box-shadow: 0 0 6px var(--evcc-background);
pointer-events: auto;
}
.target-slider::-moz-range-thumb {
@ -191,7 +194,7 @@ export default {
border: none;
opacity: 1;
border-radius: var(--thumb-overlap);
box-shadow: 0 0 6px var(--bs-gray-dark);
box-shadow: 0 0 6px var(--evcc-background);
pointer-events: auto;
}
</style>

View file

@ -1,5 +1,5 @@
<template>
<div class="d-block text-gray-light">{{ message }}&nbsp;</div>
<div class="d-block evcc-gray">{{ message }}&nbsp;</div>
</template>
<script>

View file

@ -11,7 +11,7 @@
></shopicon-regular-cablecharge>
<span class="flex-grow-1 text-truncate"> {{ name }} </span>
</h4>
<button v-if="$hiddenFeatures" class="btn btn-link text-white p-0 flex-shrink-0">
<button v-if="$hiddenFeatures" class="btn btn-link evcc-default-text p-0 flex-shrink-0">
<shopicon-filled-options size="s"></shopicon-filled-options>
</button>
</div>

View file

@ -3,7 +3,7 @@
<div class="container px-4 mb-3 mb-sm-4 d-flex justify-content-start align-items-center">
<h2 class="m-0">{{ $t("main.vehicles") }}</h2>
<button
class="btn btn-link d-flex text-white p-2 ms-1 refresh"
class="btn btn-link d-flex evcc-default-text p-2 ms-1 refresh"
:class="{
'refresh--in-progress': refreshing,
}"

View file

@ -4,7 +4,7 @@
v-if="commit"
:href="githubHashUrl"
target="_blank"
class="btn btn-link ps-0 text-decoration-none text-white text-nowrap d-flex align-items-end"
class="btn btn-link ps-0 text-decoration-none evcc-default-text text-nowrap d-flex align-items-end"
>
<Logo class="logo me-2" />
v{{ installed }}
@ -16,7 +16,7 @@
href="#"
data-bs-toggle="modal"
data-bs-target="#updateModal"
class="btn btn-link ps-0 text-decoration-none text-white text-nowrap d-flex align-items-end"
class="btn btn-link ps-0 text-decoration-none evcc-default-text text-nowrap d-flex align-items-end"
>
<shopicon-regular-gift class="me-2 text-evcc"></shopicon-regular-gift>
v{{ installed }}
@ -31,7 +31,7 @@
v-else
:href="releaseNotesUrl(installed)"
target="_blank"
class="btn btn-link text-white ps-0 text-decoration-none text-nowrap d-flex align-items-end"
class="btn btn-link evcc-default-text ps-0 text-decoration-none text-nowrap d-flex align-items-end"
>
<Logo class="logo me-2" />
v{{ installed }}

View file

@ -1,34 +0,0 @@
import WaitingDots from "./WaitingDots.vue";
export default {
title: "Main/WaitingDots",
component: WaitingDots,
argTypes: {},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { WaitingDots },
template: '<WaitingDots v-bind="$props"></WaitingDots>',
});
export const HorizontalUp = Template.bind({});
HorizontalUp.args = {
orientation: "horizontal",
direction: "up",
};
export const HorizontalDown = Template.bind({});
HorizontalDown.args = {
orientation: "horizontal",
direction: "down",
};
export const VerticalUp = Template.bind({});
VerticalUp.args = {
orientation: "vertical",
direction: "up",
};
export const VerticalDown = Template.bind({});
VerticalDown.args = {
orientation: "vertical",
direction: "down",
};

View file

@ -1,56 +0,0 @@
<template>
<div class="root position-relative" :class="`direction-${direction}`">
<shopicon-filled-options class="dots dots-back"></shopicon-filled-options
><shopicon-filled-options
class="dots dots-front position-absolute start-0 top-0"
></shopicon-filled-options>
</div>
</template>
<script>
import "@h2d2/shopicons/es/filled/options";
export default {
name: "WaitingDots",
props: {
direction: { type: String, default: "up" },
},
};
</script>
<style scoped>
.dots {
height: 1em;
width: 1em;
}
.dots-front,
.dots-back {
display: block;
}
.dots-back {
opacity: 0.25;
}
.dots-front {
opacity: 1;
}
.direction-up .dots-front {
animation: dots-vertical 2s infinite normal steps(4, jump-none);
}
.direction-down .dots-front {
animation: dots-vertical 2s infinite reverse steps(4, jump-none);
}
@keyframes dots-vertical {
25% {
clip-path: inset(100% 0 0 0);
}
75% {
clip-path: inset(0 0 0 0);
}
}
.root ::v-deep(svg) {
display: block;
}
</style>

View file

@ -25,26 +25,26 @@ export default {
savings: {
footerShort: "{percent}% Sonne",
footerLong: "{percent}% Sonnenenergie",
modalTitlePartOne: "{total} kWh geladen",
modalTitlePartTwo: "{percent}% Sonne",
modalChartGrid: "Netz {grid} kWh",
modalChartSelf: "Sonne {self} kWh",
modalSavingsPrice: "Effektiver Energiepreis",
modalSavingsTotal: "Ersparnis gegenüber Netzbezug",
modalExplaination: "Berechnung",
modalExplainationGrid: "Netz {gridPrice}",
modalExplainationFeedIn: "Einspeisung {feedInPrice}",
modalServerStart: "Seit Serverstart {since}.",
modalNoData: "noch nicht geladen",
modalTitle: "Auswertung Ladeenergie",
sinceServerStart: "Seit Serverstart {since}.",
percentTitle: "Sonnenenergie",
percentSelf: "{self} kWh Sonne",
percentGrid: "{grid} kWh Netz",
priceTitle: "Energiepreis",
priceFeedIn: "{feedInPrice} Einpeisung",
priceGrid: "{gridPrice} Netz",
savingsTitle: "Ersparnis",
savingsComparedToGrid: "gegenüber Netzbezug",
savingsTotalEnergy: "{total} kWh geladen",
},
sponsor: {
thanks: "Danke für deine Unterstützung, {sponsor}! Das hilft uns bei der Weiterentwicklung.",
confetti: "Lust auf etwas Sponsor-Konfetti?",
confetti: "Lust auf Konfetti?",
supportUs:
"Wir möchten effizientes Zuhause-Laden zum Standard für möglichst viele Menschen machen. Helfe uns indem du die Weiterentwicklung und Pflege des Projekts unterstützt.",
"Unsere Mission: Sonne tanken zum Standard machen. Hilf uns und unterstütze evcc finanziell.",
sticker: "...oder evcc Sticker?",
confettiPromise: "Es gibt auch Sticker und digitales Konfetti ;)",
becomeSponsor: "Werde jetzt GitHub Sponsor",
becomeSponsor: "Sponsor werden",
},
},
notifications: {
@ -65,16 +65,10 @@ export default {
pvExport: "Einspeisung",
},
mode: {
offShort: "Aus",
offLong: "Aus",
nowShort: "Sofort",
nowLong: "Sofort",
minpvShort: "Min",
minpvLong: "Min + PV",
pvShort: "PV",
pvLong: "PV",
fast: "Schnell",
cheap: "Günstig",
off: "Aus",
minpv: "Min+PV",
pv: "PV",
now: "Schnell",
},
loadpoint: {
fallbackName: "Ladepunkt",

View file

@ -25,26 +25,26 @@ export default {
savings: {
footerShort: "{percent}% solar",
footerLong: "{percent}% solar energy",
modalTitlePartOne: "{total} kWh charged",
modalTitlePartTwo: "{percent}% solar",
modalChartGrid: "Grid {grid} kWh",
modalChartSelf: "Solar {self} kWh",
modalSavingsPrice: "Effective energy price",
modalSavingsTotal: "Savings compared to grid",
modalExplaination: "Calculation",
modalExplainationGrid: "grid tariff {gridPrice}",
modalExplainationFeedIn: "feed-in rate {feedInPrice}",
modalServerStart: "since server start {since}.",
modalNoData: "nothing charged yet",
modalTitle: "Charge Energy Overview",
sinceServerStart: "Since server start {since}.",
percentTitle: "Solar Energy",
percentSelf: "{self} kWh solar",
percentGrid: "{grid} kWh grid",
priceTitle: "Energy Price",
priceFeedIn: "{feedInPrice} feed-in",
priceGrid: "{gridPrice} grid",
savingsTitle: "Savings",
savingsComparedToGrid: "compared to grid",
savingsTotalEnergy: "{total} kWh charged",
},
sponsor: {
thanks: "Thanks for your support, {sponsor}! It helps us with the further development.",
confetti: "Ready for some sponsor confetti?",
confetti: "Ready for confetti?",
supportUs:
"We want to make efficient home charging the standard for as many people as possible. Help us by supporting the further development and maintenance of the project.",
"Our mission: Make solar charging the standard. Help us and support evcc financially.",
sticker: "...or evcc stickers?",
confettiPromise: "There will be stickers and digital confetti ;)",
becomeSponsor: "Become a GitHub Sponsor",
becomeSponsor: "Become a Sponsor",
},
},
notifications: {
@ -65,16 +65,10 @@ export default {
pvExport: "Grid export",
},
mode: {
offShort: "Stop",
offLong: "Stop",
nowShort: "Now",
nowLong: "Now",
minpvShort: "Min",
minpvLong: "Min + PV",
pvShort: "PV",
pvLong: "PV",
fast: "Fast",
cheap: "Cheap",
off: "Off",
minpv: "Min+PV",
pv: "PV",
now: "Fast",
},
loadpoint: {
fallbackName: "Loadpoint",

View file

@ -46,13 +46,10 @@ export default {
pvExport: "Grid export",
},
mode: {
title: "Modalità",
stop: "Stop",
minpv: "Min+FV",
pv: "FV",
now: "Ora",
minpvShort: "Min",
minpvLong: "Min + FV",
pvShort: "FV",
pvLong: "Solo FV",
},
loadpoint: {
fallbackName: "Punto di carica",

View file

@ -24,26 +24,26 @@ export default {
savings: {
footerShort: "{percent}% saulės",
footerLong: "{percent}% saulės energija",
modalTitlePartOne: "{total} kWh įkrauta",
modalTitlePartTwo: "{percent}% saulės",
modalChartGrid: "Energija iš tinklo {grid} kWh",
modalChartSelf: "Saulės energija {self} kWh",
modalSavingsPrice: "Faktinė energijos kaina",
modalSavingsTotal: "Sutaupyta, palyginus su tinklu",
modalExplaination: "Skaičiavimas",
modalExplainationGrid: "kaina iš tinklo {gridPrice}",
modalExplainationFeedIn: "kompensavimas už patiektą į tinklą energiją {feedInPrice}",
modalServerStart: "nuo EVCC starto {since}.",
modalNoData: "įkrovimo duomenų dar nėra",
modalTitle: "Įkrovimo energijos apžvalga",
sinceServerStart: "nuo evcc starto {since}.",
percentTitle: "Saulės energija",
percentSelf: "{self} kWh saulės",
percentGrid: "{grid} kWh tinklas",
priceTitle: "Energijos kaina",
priceFeedIn: "{feedInPrice} eksporto",
priceGrid: "{gridPrice} tinklo",
savingsTitle: "Sutaupyta",
savingsComparedToGrid: "palyginus su tinklu",
savingsTotalEnergy: "{total} kWh įkrauta",
},
sponsor: {
thanks: "Ačiū, kad mus remiate {sponsor}! Taip prisidedate prie projekto vystymo.",
confetti: "Ar pasiruošę priimti rėmėjo konfeti?",
confetti: "Norite konfeti?",
supportUs:
"Norime, kad efektyvesnis elektromobilių įkrovimas saulės energija taptų pasiekiamas kuo daugiau žmonių. Remdami prisidedate prie nuolatinio projekto vystymo ir techninio palaikymo.",
sticker: "... ar EVCC lipdukų?",
"Mūsų misija: Siekiame, kad įkrovimas saulės energija taptų standartu. Padėkite mums ir paremkite evcc finansiškai.",
sticker: "... ar evcc lipdukų?",
confettiPromise: "Gausite lipdukų ir skaitmeninių konfeti ;)",
becomeSponsor: "Pradėkite GitHub rėmimą!",
becomeSponsor: "Tapkite rėmėju",
},
},
notifications: {
@ -64,16 +64,10 @@ export default {
pvExport: "Tinklo eksportas",
},
mode: {
offShort: "Stop",
offLong: "Stop",
nowShort: "Dabar",
nowLong: "Dabar",
minpvShort: "Min",
minpvLong: "Min + PV",
pvShort: "PV",
pvLong: "PV",
fast: "Greitai",
cheap: "Pigiai",
off: "Stop",
minpv: "Min+PV",
pv: "PV",
now: "Greitas",
},
loadpoint: {
fallbackName: "Įkroviklis",

View file

@ -128,7 +128,15 @@ export default {
return new Intl.NumberFormat(this.$i18n.locale, {
style: "currency",
currency,
}).format(amout);
currencyDisplay: "code",
})
.format(amout)
.replace(currency, "")
.trim();
},
fmtCurrencySymbol: function (currency = "EUR") {
const symbols = { EUR: "€", USD: "$" };
return symbols[currency] || currency;
},
fmtPricePerKWh: function (amout = 0, currency = "EUR") {
let unit = currency;

View file

@ -86,6 +86,5 @@ export default {
<style scoped>
.app {
min-height: 100vh;
background-color: white;
}
</style>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

93
dist/assets/index.f2e7fb95.js vendored Normal file

File diff suppressed because one or more lines are too long

8
dist/index.html vendored
View file

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="evcc - Sonne tanken ☀️🚘" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="evcc" />
<meta name="application-name" content="evcc" />
@ -17,11 +17,11 @@
<link rel="shortcut icon" href="meta/favicon.ico?[[.Version]]" />
<meta name="msapplication-TileColor" content="#28293e" />
<meta name="msapplication-config" content="meta/browserconfig.xml?[[.Version]]" />
<meta name="theme-color" content="#28293e" />
<meta name="theme-color" content="#020318" />
<title>evcc</title>
<script type="module" crossorigin src="./assets/index.969c8832.js"></script>
<link rel="stylesheet" href="./assets/index.dca955c6.css">
<script type="module" crossorigin src="./assets/index.f2e7fb95.js"></script>
<link rel="stylesheet" href="./assets/index.49c54bb1.css">
</head>
<body>
<script>

22259
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,9 +6,9 @@
"build": "vite build",
"test": "",
"lint": "prettier assets './**/*.{yml,yaml}' --write && eslint assets/**/*.js assets/**/*.vue --fix",
"build-storybook": "build-storybook",
"dev": "vite",
"storybook": "start-storybook -p 6006"
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
"main": "index.js",
"dependencies": {
@ -26,10 +26,13 @@
},
"devDependencies": {
"@babel/core": "^7.17.2",
"@storybook/addon-actions": "^6.5.0-alpha.38",
"@storybook/addon-essentials": "^6.5.0-alpha.38",
"@storybook/addon-links": "^6.5.0-alpha.38",
"@storybook/vue3": "^6.5.0-alpha.38",
"@storybook/addon-actions": "^6.5.9",
"@storybook/addon-essentials": "^6.5.9",
"@storybook/addon-interactions": "^6.5.9",
"@storybook/addon-links": "^6.5.9",
"@storybook/builder-vite": "^0.1.37",
"@storybook/testing-library": "^0.0.13",
"@storybook/vue3": "^6.5.9",
"@vue/compiler-sfc": "^3.2.30",
"@vue/eslint-config-standard": "^6.1.0",
"babel-loader": "^8.2.3",
@ -42,9 +45,11 @@
"eslint-plugin-standard": "^5.0.0",
"eslint-plugin-storybook": "^0.5.6",
"eslint-plugin-vue": "^7.20.0",
"html-webpack-plugin": "^5.5.0",
"postcss-custom-media": "^8.0.2",
"prettier": "^2.5.1",
"storybook-builder-vite": "^0.1.15",
"vite": "^2.7.13"
"vite": "^2.7.13",
"vue-loader": "^16.8.3"
},
"engines": {
"npm": ">=8.0.0",

5
postcss.config.js Normal file
View file

@ -0,0 +1,5 @@
const postcssCustomMedia = require("postcss-custom-media");
module.exports = {
plugins: [postcssCustomMedia()],
};