chore: Storybook migration (#19780)

This commit is contained in:
Michael Geers 2025-03-17 20:46:36 +01:00 • committed by GitHub
parent 97b4b9aeda
commit ec9af3782f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 3384 additions and 2933 deletions

2
.gitignore vendored
View file

@ -43,3 +43,5 @@ asset-stats.html
/playwright/.cache/
.gitpod.yml
/evcc.db
*storybook.log

14
.storybook/main.js Normal file
View file

@ -0,0 +1,14 @@
/** @type { import('@storybook/vue3-vite').StorybookConfig } */
const config = {
stories: ["../assets/js/**/*.stories.@(js|ts)"],
addons: [
"@storybook/addon-essentials",
"@chromatic-com/storybook",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/vue3-vite",
options: {},
},
};
export default config;

42
.storybook/preview.js Normal file
View file

@ -0,0 +1,42 @@
import { setup } from "@storybook/vue3";
import "bootstrap/dist/css/bootstrap.min.css";
import smoothscroll from "smoothscroll-polyfill";
import setupI18n from "../assets/js/i18n";
import "../assets/css/app.css";
import { watchThemeChanges } from "../assets/js/theme";
smoothscroll.polyfill();
watchThemeChanges();
// Setup global parameters
/** @type { import('@storybook/vue3').Preview } */
const preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
backgrounds: {
default: "default",
values: [
{
name: "default",
value: "var(--evcc-background)",
},
{
name: "box",
value: "var(--evcc-box)",
},
],
},
},
};
setup((app) => {
app.config.globalProperties.$hiddenFeatures = () => true;
app.use(setupI18n());
});
export default preview;

View file

@ -89,6 +89,14 @@ npm install
npm run dev
```
#### Storybook
We're using storybook to develop and visualize UI components in different states. Running the command below will open your browser at http://127.0.0.1:6006/.
```sh
npm run storybook
```
#### Integration testing
We use Playwright for end-to-end integration tests. They start a local evcc instance with different configuration yamls and prefilled databases. To run them, you have to do a local build first.

View file

@ -1,14 +0,0 @@
import { defineSetupVue3 } from "@histoire/plugin-vue";
import "bootstrap/dist/css/bootstrap.min.css";
import smoothscroll from "smoothscroll-polyfill";
import setupI18n from "./js/i18n";
import "./css/app.css";
import { watchThemeChanges } from "./js/theme";
smoothscroll.polyfill();
watchThemeChanges();
export const setupVue3 = defineSetupVue3(({ app }) => {
app.config.globalProperties.$hiddenFeatures = () => true;
app.use(setupI18n());
});

View file

@ -0,0 +1,55 @@
import ChargingPlan from "./ChargingPlan.vue";
const hoursFromNow = (hours) => {
const now = new Date();
now.setHours(now.getHours() + hours);
return now.toISOString();
};
export default {
title: "ChargingPlans/ChargingPlan",
component: ChargingPlan,
parameters: {
layout: "centered",
},
argTypes: {
id: { control: "text" },
effectivePlanTime: { control: "text" },
effectivePlanSoc: { control: "number" },
planEnergy: { control: "number" },
socBasedCharging: { control: "boolean" },
socBasedPlanning: { control: "boolean" },
vehicleSoc: { control: "number" },
},
};
const Template = (args) => ({
components: { ChargingPlan },
setup() {
return { args };
},
template: '<ChargingPlan v-bind="args" />',
});
export const None = Template.bind({});
None.args = {
id: "1",
};
export const SocBasedCharging = Template.bind({});
SocBasedCharging.args = {
id: "1",
effectivePlanTime: hoursFromNow(4),
effectivePlanSoc: 77,
socBasedCharging: true,
socBasedPlanning: true,
};
export const EnergyBasedCharging = Template.bind({});
EnergyBasedCharging.args = {
id: "1",
effectivePlanTime: hoursFromNow(12),
planEnergy: 77,
socBasedCharging: false,
socBasedPlanning: false,
};

View file

@ -1,22 +0,0 @@
<script setup>
import ChargingPlan from "./ChargingPlan.vue";
const hoursFromNow = function (hours) {
const now = new Date();
now.setHours(now.getHours() + hours);
return now.toISOString();
};
</script>
<template>
<Story>
<Variant title="none">
<ChargingPlan id="1" />
</Variant>
<Variant title="target charge">
<ChargingPlan id="1" :targetTime="hoursFromNow(14)" />
</Variant>
<Variant title="min soc">
<ChargingPlan id="1" :minSoc="30" :vehicleSoc="25" />
</Variant>
</Story>
</template>

View file

@ -0,0 +1,114 @@
import Preview from "./Preview.vue";
const now = new Date();
function createDate(hoursFromNow) {
const result = new Date(now.getTime());
result.setHours(result.getHours() + hoursFromNow);
return result;
}
function createRate(price, hoursFromNow, durationHours = 1) {
const start = new Date(now.getTime());
start.setHours(start.getHours() + hoursFromNow);
start.setMinutes(0);
start.setSeconds(0);
start.setMilliseconds(0);
const end = new Date(start.getTime());
end.setHours(start.getHours() + durationHours);
end.setMinutes(0);
end.setSeconds(0);
end.setMilliseconds(0);
return { start: start.toISOString(), end: end.toISOString(), price };
}
// Scenario data
const co2Data = {
rates: [
545, 518, 545, 518, 0, 545, 527, 527, 536, 518, 400, 336, 336, 339, 344, 336, 336, 336, 372,
400, 555, 555, 545, 555, 564, 545, 555, 545, 536, 545, 527, 536, 518, 545, 509, 336, 336, 336,
].map((price, i) => createRate(price, i)),
duration: 8695,
plan: [createRate(213, 4), createRate(336, 11), createRate(336, 12)],
smartCostType: "co2",
targetTime: createDate(14),
};
const fixedData = {
rates: [createRate(0.442, 0, 50)],
duration: 8695,
plan: [createRate(0.442, 12, 3)],
smartCostType: "price",
currency: "EUR",
targetTime: createDate(14),
};
const zonedData = {
rates: [
createRate(3.72, 0, 4),
createRate(2.39, 4, 12),
createRate(3.72, 16, 12),
createRate(2.39, 28, 12),
createRate(3.72, 40, 12),
],
duration: 8695,
plan: [createRate(2.39, 13, 3)],
smartCostType: "price",
currency: "DKK",
targetTime: createDate(17),
};
const unknownData = {
rates: co2Data.rates.slice(0, 16),
duration: 8695,
plan: [createRate(213, 4), createRate(336, 11), createRate(336, 12)],
smartCostType: "co2",
targetTime: createDate(14),
};
const dynamicData = {
rates: [0.12, 0.15, 0, -0.05, -0.11, -0.24, -0.08, 0.12, 0.25, 0.29, 0.22, 0.31, 0.31, 0.33].map(
(price, i) => createRate(price, i)
),
duration: 8695,
plan: [createRate(0.23, 2, 5)],
smartCostType: "price",
currency: "EUR",
targetTime: createDate(13),
};
export default {
title: "ChargingPlans/Preview",
component: Preview,
argTypes: {
rates: { control: "object" },
duration: { control: "number" },
plan: { control: "object" },
smartCostType: { control: "select", options: ["co2", "price"] },
currency: { control: "text" },
targetTime: { control: "text" },
},
};
const Template = (args) => ({
components: { Preview },
setup() {
return { args };
},
template: '<Preview v-bind="args" />',
});
export const Co2 = Template.bind({});
Co2.args = co2Data;
export const Fixed = Template.bind({});
Fixed.args = fixedData;
export const Zoned = Template.bind({});
Zoned.args = zonedData;
export const Unknown = Template.bind({});
Unknown.args = unknownData;
export const Dynamic = Template.bind({});
Dynamic.args = dynamicData;

View file

@ -1,100 +0,0 @@
<script setup>
import Preview from "./Preview.vue";
const now = new Date();
function createDate(hoursFromNow) {
const result = new Date(now.getTime());
result.setHours(result.getHours() + hoursFromNow);
return result;
}
function createRate(price, hoursFromNow, durationHours = 1) {
const start = new Date(now.getTime());
start.setHours(start.getHours() + hoursFromNow);
start.setMinutes(0);
start.setSeconds(0);
start.setMilliseconds(0);
const end = new Date(start.getTime());
end.setHours(start.getHours() + durationHours);
end.setMinutes(0);
end.setSeconds(0);
end.setMilliseconds(0);
return { start: start.toISOString(), end: end.toISOString(), price };
}
const co2 = {
rates: [
545, 518, 545, 518, 0, 545, 527, 527, 536, 518, 400, 336, 336, 339, 344, 336, 336, 336, 372,
400, 555, 555, 545, 555, 564, 545, 555, 545, 536, 545, 527, 536, 518, 545, 509, 336, 336,
336,
].map((price, i) => createRate(price, i)),
duration: 8695,
plan: [createRate(213, 4), createRate(336, 11), createRate(336, 12)],
smartCostType: "co2",
targetTime: createDate(14),
};
const fixed = {
rates: [createRate(0.442, 0, 50)],
duration: 8695,
plan: [createRate(0.442, 12, 3)],
smartCostType: "price",
currency: "EUR",
targetTime: createDate(14),
};
const zoned = {
rates: [
createRate(3.72, 0, 4),
createRate(2.39, 4, 12),
createRate(3.72, 16, 12),
createRate(2.39, 28, 12),
createRate(3.72, 40, 12),
],
duration: 8695,
plan: [createRate(2.39, 13, 3)],
smartCostType: "price",
currency: "DKK",
targetTime: createDate(17),
};
const unknown = {
rates: co2.rates.slice(0, 16),
duration: 8695,
plan: [createRate(213, 4), createRate(336, 11), createRate(336, 12)],
smartCostType: "co2",
targetTime: createDate(14),
};
const dynamic = {
rates: [
0.12, 0.15, 0, -0.05, -0.11, -0.24, -0.08, 0.12, 0.25, 0.29, 0.22, 0.31, 0.31, 0.33,
].map((price, i) => createRate(price, i)),
duration: 8695,
plan: [createRate(0.23, 2, 5)],
smartCostType: "price",
currency: "EUR",
targetTime: createDate(13),
};
</script>
<template>
<Story title="ChargingPlanPreview">
<Variant title="co2">
<Preview v-bind="co2" />
</Variant>
<Variant title="fixed">
<Preview v-bind="fixed" />
</Variant>
<Variant title="zoned">
<Preview v-bind="zoned" />
</Variant>
<Variant title="unknown">
<Preview v-bind="unknown" />
</Variant>
<Variant title="dynamic">
<Preview v-bind="dynamic" />
</Variant>
</Story>
</template>

View file

@ -0,0 +1,34 @@
import BatteryIcon from "./BatteryIcon.vue";
export default {
title: "Energyflow/BatteryIcon",
component: BatteryIcon,
parameters: {
layout: "centered",
},
};
const createStory = (args) => {
const story = () => ({
components: { BatteryIcon },
setup() {
return { args };
},
template: '<BatteryIcon v-bind="args" />',
});
story.args = args;
return story;
};
export const Empty = createStory({ soc: 0 });
export const Soc10 = createStory({ soc: 10 });
export const Soc20 = createStory({ soc: 20 });
export const Soc30 = createStory({ soc: 30 });
export const Soc40 = createStory({ soc: 40 });
export const Soc50 = createStory({ soc: 50 });
export const Soc60 = createStory({ soc: 60 });
export const Soc70 = createStory({ soc: 70 });
export const Soc80 = createStory({ soc: 80 });
export const Soc90 = createStory({ soc: 90 });
export const Hold = createStory({ hold: true });
export const GridCharge = createStory({ gridCharge: true });

View file

@ -1,20 +0,0 @@
<script setup>
import BatteryIcon from "./BatteryIcon.vue";
</script>
<template>
<Story :layout="{ type: 'grid', width: 150 }">
<Variant title="0%"> <BatteryIcon :soc="0" /> </Variant>
<Variant title="10%"> <BatteryIcon :soc="10" /> </Variant>
<Variant title="20%"> <BatteryIcon :soc="20" /> </Variant>
<Variant title="30%"> <BatteryIcon :soc="30" /> </Variant>
<Variant title="40%"> <BatteryIcon :soc="40" /> </Variant>
<Variant title="50%"> <BatteryIcon :soc="50" /> </Variant>
<Variant title="60%"> <BatteryIcon :soc="60" /> </Variant>
<Variant title="70%"> <BatteryIcon :soc="70" /> </Variant>
<Variant title="80%"> <BatteryIcon :soc="80" /> </Variant>
<Variant title="90%"> <BatteryIcon :soc="90" /> </Variant>
<Variant title="hold"> <BatteryIcon hold /> </Variant>
<Variant title="grid charge"> <BatteryIcon grid-charge /> </Variant>
</Story>
</template>

View file

@ -0,0 +1,215 @@
import Energyflow from "./Energyflow.vue";
export default {
title: "Energyflow/Energyflow",
component: Energyflow,
};
const Template = (args) => ({
components: { Energyflow },
setup() {
return { args };
},
template: '<Energyflow v-bind="args" />',
});
export const GridAndPV = Template.bind({});
GridAndPV.args = {
gridConfigured: true,
pvConfigured: true,
pvPower: 7300,
gridPower: -2300,
homePower: 800,
loadpointsCompact: [
{ power: 1000, icon: "car", charging: true },
{ power: 1000, icon: "bike", charging: true },
{ power: 2200, icon: "car", charging: true },
],
tariffGrid: 0.25,
tariffFeedIn: 0.08,
tariffEffectivePrice: 0.08,
smartCostType: "price",
smartCostAvailable: true,
currency: "EUR",
siteTitle: "Home",
pv: [{ power: 5000 }, { power: 2300 }],
};
export const BatteryAndGrid = Template.bind({});
BatteryAndGrid.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 0,
gridPower: 1200,
homePower: 2000,
batteryPower: 800,
batterySoc: 77,
tariffGrid: 0.25,
tariffFeedIn: 0.08,
tariffEffectivePrice: 0.08,
currency: "EUR",
siteTitle: "Home",
battery: [
{ soc: 44.999, capacity: 13.3, power: 350 },
{ soc: 82.3331, capacity: 21, power: 450 },
],
};
export const BatteryCharging = Template.bind({});
BatteryCharging.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 5000,
gridPower: -1300,
homePower: 800,
loadpointsCompact: [{ power: 1400, icon: "car", charging: true }],
batteryPower: -1500,
batterySoc: 75,
siteTitle: "Home",
};
export const GridPVAndBattery = Template.bind({});
GridPVAndBattery.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 1000,
gridPower: 700,
homePower: 3300,
batteryPower: 1500,
batterySoc: 30,
siteTitle: "Home",
};
export const BatteryThresholds = Template.bind({});
BatteryThresholds.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 8700,
gridPower: -500,
loadpointsCompact: [
{ power: 5000, icon: "car", charging: true },
{ power: 2500, icon: "bus", charging: true },
],
batteryPower: -700,
batterySoc: 95,
siteTitle: "Home",
};
export const PVThresholds = Template.bind({});
PVThresholds.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 300,
gridPower: 6500,
homePower: 1000,
loadpointsCompact: [
{ power: 5000, icon: "car", charging: true },
{ power: 1600, icon: "car", charging: true },
],
batteryPower: 800,
batterySoc: 76,
siteTitle: "Home",
};
export const GridOnly = Template.bind({});
GridOnly.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 0,
gridPower: 6500,
homePower: 1000,
loadpointsCompact: [
{ power: 5500, icon: "car", charging: true },
{ power: 0, icon: "car", charging: false },
{ power: 0, icon: "car", charging: false },
{ power: 0, icon: "car", charging: false },
],
batteryPower: 0,
batterySoc: 0,
siteTitle: "Home",
};
export const LowPower = Template.bind({});
LowPower.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 700,
gridPower: -300,
homePower: 300,
batteryPower: -100,
batterySoc: 55,
tariffGrid: 0.25,
tariffFeedIn: 0.08,
tariffEffectivePrice: 0.08,
currency: "EUR",
siteTitle: "Home",
};
export const CO2 = Template.bind({});
CO2.args = {
gridConfigured: true,
pvConfigured: true,
pvPower: 7300,
gridPower: -2300,
homePower: 800,
loadpointsCompact: [
{ power: 1000, icon: "car", charging: true },
{ power: 1000, icon: "car", charging: true },
{ power: 2200, icon: "car", charging: true },
],
tariffGrid: 0.25,
tariffFeedIn: 0.08,
tariffEffectivePrice: 0.08,
tariffCo2: 723,
tariffEffectiveCo2: 0,
smartCostType: "co2",
smartCostAvailable: true,
currency: "EUR",
siteTitle: "Home",
pv: [{ power: 5000 }, { power: 2300 }],
};
export const UnknownInput = Template.bind({});
UnknownInput.args = {
gridConfigured: true,
pvConfigured: true,
pvPower: 2000,
gridPower: -2000,
loadpointsCompact: [{ power: 1000, icon: "car", charging: true }],
};
export const UnknownInputFill = Template.bind({});
UnknownInputFill.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 500,
gridPower: 0,
batteryPower: -1000,
loadpointsCompact: [],
};
export const UnknownOutput = Template.bind({});
UnknownOutput.args = {
gridConfigured: true,
pvConfigured: true,
pvPower: 3000,
gridPower: -1000,
loadpointsCompact: [{ power: 1700, icon: "car", charging: true }],
};
export const UnknownOutputLessThan10Percent = Template.bind({});
UnknownOutputLessThan10Percent.args = {
gridConfigured: true,
pvConfigured: true,
pvPower: 3000,
gridPower: -1000,
loadpointsCompact: [{ power: 1800, icon: "car", charging: true }],
};

View file

@ -1,209 +0,0 @@
<script setup>
import Energyflow from "./Energyflow.vue";
</script>
<template>
<Story>
<Variant title="grid and pv">
<Energyflow
gridConfigured
pvConfigured
:pvPower="7300"
:gridPower="-2300"
:homePower="800"
:loadpointsCompact="[
{ power: 1000, icon: 'car', charging: true },
{ power: 1000, icon: 'bike', charging: true },
{ power: 2200, icon: 'car', charging: true },
]"
:tariffGrid="0.25"
:tariffFeedIn="0.08"
:tariffEffectivePrice="0.08"
smartCostType="price"
smartCostAvailable
currency="EUR"
siteTitle="Home"
:pv="[{ power: 5000 }, { power: 2300 }]"
/>
</Variant>
<Variant title="battery and grid">
<Energyflow
gridConfigured
pvConfigured
batteryConfigured
:pvPower="0"
:gridPower="1200"
:homePower="2000"
:batteryPower="800"
:batterySoc="77"
:tariffGrid="0.25"
:tariffFeedIn="0.08"
:tariffEffectivePrice="0.08"
currency="EUR"
siteTitle="Home"
:battery="[
{ soc: 44.999, capacity: 13.3, power: 350 },
{ soc: 82.3331, capacity: 21, power: 450 },
]"
/>
</Variant>
<Variant title="battery charging">
<Energyflow
gridConfigured
pvConfigured
batteryConfigured
:pvPower="5000"
:gridPower="-1300"
:homePower="800"
:loadpointsCompact="[{ power: 1400, icon: 'car', charging: true }]"
:batteryPower="-1500"
:batterySoc="75"
siteTitle="Home"
/>
</Variant>
<Variant title="grid, pv and battery">
<Energyflow
gridConfigured
pvConfigured
batteryConfigured
:pvPower="1000"
:gridPower="700"
:homePower="3300"
:batteryPower="1500"
:batterySoc="30"
siteTitle="Home"
/>
</Variant>
<Variant title="battery thresholds">
<Energyflow
gridConfigured
pvConfigured
batteryConfigured
:pvPower="8700"
:gridPower="-500"
:loadpointsCompact="[
{ power: 5000, icon: 'car', charging: true },
{ power: 2500, icon: 'bus', charging: true },
]"
:batteryPower="-700"
:batterySoc="95"
siteTitle="Home"
/>
</Variant>
<Variant title="pv thresholds">
<Energyflow
gridConfigured
pvConfigured
batteryConfigured
:pvPower="300"
:gridPower="6500"
:homePower="1000"
:loadpointsCompact="[
{ power: 5000, icon: 'car', charging: true },
{ power: 1600, icon: 'car', charging: true },
]"
:batteryPower="800"
:batterySoc="76"
siteTitle="Home"
/>
</Variant>
<Variant title="grid only">
<Energyflow
gridConfigured
pvConfigured
batteryConfigured
:pvPower="0"
:gridPower="6500"
:homePower="1000"
:loadpointsCompact="[
{ power: 5500, icon: 'car', charging: true },
{ power: 0, icon: 'car', charging: false },
{ power: 0, icon: 'car', charging: false },
{ power: 0, icon: 'car', charging: false },
]"
:batteryPower="0"
:batterySoc="0"
siteTitle="Home"
/>
</Variant>
<Variant title="low power">
<Energyflow
gridConfigured
pvConfigured
batteryConfigured
:pvPower="700"
:gridPower="-300"
:homePower="300"
:batteryPower="-100"
:batterySoc="55"
:tariffGrid="0.25"
:tariffFeedIn="0.08"
:tariffEffectivePrice="0.08"
currency="EUR"
siteTitle="Home"
/>
</Variant>
<Variant title="co2">
<Energyflow
gridConfigured
pvConfigured
:pvPower="7300"
:gridPower="-2300"
:homePower="800"
:loadpointsCompact="[
{ power: 1000, icon: 'car', charging: true },
{ power: 1000, icon: 'car', charging: true },
{ power: 2200, icon: 'car', charging: true },
]"
:tariffGrid="0.25"
:tariffFeedIn="0.08"
:tariffEffectivePrice="0.08"
:tariffCo2="723"
:tariffEffectiveCo2="0"
smartCostType="co2"
smartCostAvailable
currency="EUR"
siteTitle="Home"
:pv="[{ power: 5000 }, { power: 2300 }]"
/>
</Variant>
<Variant title="unknown input">
<Energyflow
gridConfigured
pvConfigured
:pvPower="2000"
:gridPower="-2000"
:loadpointsCompact="[{ power: 1000, icon: 'car', charging: true }]"
/>
</Variant>
<Variant title="unknown input fill">
<Energyflow
gridConfigured
pvConfigured
batteryConfigured
:pvPower="500"
:gridPower="0"
:batteryPower="-1000"
:loadpointsCompact="[]"
/>
</Variant>
<Variant title="unknown output">
<Energyflow
gridConfigured
pvConfigured
:pvPower="3000"
:gridPower="-1000"
:loadpointsCompact="[{ power: 1700, icon: 'car', charging: true }]"
/>
</Variant>
<Variant title="unknown output (&lt; 10%)">
<Energyflow
gridConfigured
pvConfigured
:pvPower="3000"
:gridPower="-1000"
:loadpointsCompact="[{ power: 1800, icon: 'car', charging: true }]"
/>
</Variant>
</Story>
</template>

View file

@ -0,0 +1,58 @@
import Footer from "./Footer.vue";
export default {
title: "Footer/Footer",
component: Footer,
argTypes: {
version: { control: "object" },
savings: { control: "object" },
},
};
const Template = (args) => ({
components: { Footer },
setup() {
return { args };
},
template: '<Footer v-bind="args" />',
});
export const Default = Template.bind({});
Default.args = {
version: {
installed: "0.401",
available: "0.401",
},
savings: {
statistics: {
"30d": { solarPercentage: 50 },
},
},
};
export const UpdateNightly = Template.bind({});
UpdateNightly.args = {
version: {
installed: "0.400",
available: "0.400",
commit: "5ce7be4",
},
savings: {
statistics: {
"30d": { solarPercentage: 22 },
},
},
};
export const UpdateAvailable = Template.bind({});
UpdateAvailable.args = {
version: {
installed: "0.400",
available: "0.500",
},
savings: {
statistics: {
"30d": { solarPercentage: 100 },
},
},
};

View file

@ -1,31 +0,0 @@
<script setup>
import { reactive } from "vue";
import Footer from "./Footer.vue";
const state = reactive({
version: {
installed: "0.401",
available: "0.401",
},
savings: {
selfConsumptionPercent: 88,
},
});
</script>
<template>
<Story>
<Variant title="default">
<Footer v-bind="state" />
</Variant>
<Variant title="update nightly">
<Footer
v-bind="state"
:version="{ installed: '0.400', available: '0.400', commit: '5ce7be4' }"
/>
</Variant>
<Variant title="update availabe">
<Footer v-bind="state" :version="{ installed: '0.400', available: '0.500' }" />
</Variant>
</Story>
</template>

View file

@ -0,0 +1,32 @@
import OfflineIndicator from "./OfflineIndicator.vue";
export default {
title: "Footer/OfflineIndicator",
component: OfflineIndicator,
argTypes: {
offline: { control: "boolean" },
fatal: { control: "object" },
},
};
const Template = (args) => ({
components: { OfflineIndicator },
setup() {
return { args };
},
template: '<OfflineIndicator v-bind="args" />',
});
export const Offline = Template.bind({});
Offline.args = {
offline: true,
};
export const Fatal = Template.bind({});
Fatal.args = {
fatal: {
error: `cannot create charger 'wallbox_cphl': cannot create charger type 'template': cannot create
charger type 'hardybarth-ecbl: Post http://192.168.2.219/api/v1/chargecontrols/1/mode: context deadline exceeded (Client.Timeout
exceeded while awaiting headers)`,
},
};

View file

@ -1,20 +0,0 @@
<script setup>
import OfflineIndicator from "./OfflineIndicator.vue";
</script>
<template>
<Story>
<Variant title="offline">
<OfflineIndicator offline />
</Variant>
<Variant title="fatal">
<OfflineIndicator
:fatal="{
error: `cannot create charger 'wallbox_cphl': cannot create charger type 'template': cannot create
charger type 'hardybarth-ecbl: Post http://192.168.2.219/api/v1/chargecontrols/1/mode: context deadline exceeded (Client.Timeout
exceeded while awaiting headers)`,
}"
/>
</Variant>
</Story>
</template>

View file

@ -0,0 +1,61 @@
import Version from "./Version.vue";
const releaseNotes =
'<h1>0.141</h1><p>Bug fixes:</p><ul><li>Withdraw Kia and Hyundai apis (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="817450535" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/699" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/699/hovercard" href="https://github.com/evcc-io/evcc/pull/699">#699</a>)</li><li>Simplify Tesla integration and fix upstream changes (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="817525274" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/700" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/700/hovercard" href="https://github.com/evcc-io/evcc/pull/700">#700</a>)</li><li>SHM: Check connected status (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="811567860" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/673" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/673/hovercard" href="https://github.com/evcc-io/evcc/pull/673">#673</a>)</li></ul><p>Enhancements:</p><ul><li>Add Seat api (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="812885392" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/681" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/681/hovercard" href="https://github.com/evcc-io/evcc/pull/681">#681</a>)</li><li>Add Skoda api (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="812883796" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/680" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/680/hovercard" href="https://github.com/evcc-io/evcc/pull/680">#680</a>)</li><li>Add Peugeot/Opel/Citroen api (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="815612446" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/692" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/692/hovercard" href="https://github.com/evcc-io/evcc/pull/692">#692</a>)</li><li>KEBA: Support mA current resolution adjustments (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="801358501" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/646" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/646/hovercard" href="https://github.com/evcc-io/evcc/pull/646">#646</a>)</li></ul>';
export default {
title: "Footer/Version",
component: Version,
parameters: {
layout: "centered",
},
argTypes: {
installed: { control: "text" },
available: { control: "text" },
commit: { control: "text" },
releaseNotes: { control: "text" },
hasUpdater: { control: "boolean" },
},
};
const Template = (args) => ({
components: { Version },
setup() {
return { args };
},
template: '<Version v-bind="args" />',
});
export const Latest = Template.bind({});
Latest.args = {
installed: "0.141",
available: "0.141",
};
export const Nightly = Template.bind({});
Nightly.args = {
installed: "0.141",
available: "0.141",
commit: "5ce7be4",
};
export const UpdateAvailable = Template.bind({});
UpdateAvailable.args = {
installed: "0.141",
available: "0.142",
releaseNotes,
};
export const AutoUpdater = Template.bind({});
AutoUpdater.args = {
installed: "0.141",
available: "0.142",
releaseNotes,
hasUpdater: true,
};
export const NoReleaseNotes = Template.bind({});
NoReleaseNotes.args = {
installed: "0.141",
available: "0.142",
};

View file

@ -1,32 +0,0 @@
<script setup>
import { reactive } from "vue";
import Version from "./Version.vue";
const state = reactive({
installed: "0.141",
available: "0.141",
});
const releaseNotes =
'<h1>0.141</h1><p>Bug fixes:</p><ul><li>Withdraw Kia and Hyundai apis (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="817450535" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/699" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/699/hovercard" href="https://github.com/evcc-io/evcc/pull/699">#699</a>)</li><li>Simplify Tesla integration and fix upstream changes (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="817525274" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/700" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/700/hovercard" href="https://github.com/evcc-io/evcc/pull/700">#700</a>)</li><li>SHM: Check connected status (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="811567860" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/673" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/673/hovercard" href="https://github.com/evcc-io/evcc/pull/673">#673</a>)</li></ul><p>Enhancements:</p><ul><li>Add Seat api (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="812885392" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/681" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/681/hovercard" href="https://github.com/evcc-io/evcc/pull/681">#681</a>)</li><li>Add Skoda api (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="812883796" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/680" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/680/hovercard" href="https://github.com/evcc-io/evcc/pull/680">#680</a>)</li><li>Add Peugeot/Opel/Citroen api (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="815612446" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/692" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/692/hovercard" href="https://github.com/evcc-io/evcc/pull/692">#692</a>)</li><li>KEBA: Support mA current resolution adjustments (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="801358501" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/646" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/646/hovercard" href="https://github.com/evcc-io/evcc/pull/646">#646</a>)</li></ul>';
</script>
<template>
<Story :layout="{ type: 'grid', width: 600 }">
<Variant title="latest">
<Version v-bind="state" />
</Variant>
<Variant title="nightly">
<Version v-bind="state" commit="5ce7be4" />
</Variant>
<Variant title="update available">
<Version v-bind="state" available="0.142" :releaseNotes="releaseNotes" />
</Variant>
<Variant title="auto updater">
<Version v-bind="state" available="0.142" :releaseNotes="releaseNotes" hasUpdater />
</Variant>
<Variant title="no release notes">
<Version v-bind="state" available="0.142" />
</Variant>
</Story>
</template>

View file

@ -0,0 +1,74 @@
import Loadpoint from "./Loadpoint.vue";
export default {
title: "Loadpoints/Loadpoint",
component: Loadpoint,
parameters: {
layout: "centered",
},
};
const baseState = {
id: 0,
pvConfigured: true,
chargePower: 2800,
chargedEnergy: 11e3,
chargeDuration: 95 * 60,
vehicleTitle: "Mein Auto",
vehicleName: "meinauto",
enabled: true,
connected: true,
mode: "pv",
charging: true,
vehicleSoc: 66,
limitSoc: 90,
chargeCurrent: 7,
minCurrent: 6,
maxCurrent: 16,
activePhases: 2,
};
const createStory = (props) => {
const story = (args) => ({
components: { Loadpoint },
setup() {
return { args };
},
template: '<Loadpoint v-bind="args" />',
});
story.args = { ...baseState, ...props };
return story;
};
export const Standard = createStory({});
export const WithoutSoc = createStory({
vehicleTitle: "",
vehicleName: "",
});
export const Idle = createStory({
enabled: false,
connected: false,
vehicleName: "",
mode: "off",
charging: false,
chargeCurrent: 0,
});
export const DisabledLongTitle = createStory({
title: "Charging point with a very very very long title!!!1!",
remoteDisabled: "soft",
remoteDisabledSource: "Sunny Home Manager",
mode: "now",
enabled: false,
charging: false,
chargePower: 0,
});
export const ChargerIconNoVehicle = createStory({
chargerIcon: "heater",
title: "Heating device with long name",
mode: "now",
chargerFeatureIntegratedDevice: true,
});

View file

@ -1,67 +0,0 @@
<script setup>
import Loadpoint from "./Loadpoint.vue";
import { reactive } from "vue";
const state = reactive({
id: 0,
pvConfigured: true,
chargePower: 2800,
chargedEnergy: 11e3,
chargeDuration: 95 * 60,
vehicleTitle: "Mein Auto",
vehicleName: "meinauto",
enabled: true,
connected: true,
mode: "pv",
charging: true,
vehicleSoc: 66,
limitSoc: 90,
chargeCurrent: 7,
minCurrent: 6,
maxCurrent: 16,
activePhases: 2,
});
</script>
<template>
<Story>
<Variant title="standard">
<Loadpoint v-bind="state" />
</Variant>
<Variant title="without soc">
<Loadpoint v-bind="state" vehicleTitle="" vehicleName="" />
</Variant>
<Variant title="idle">
<Loadpoint
v-bind="state"
:enabled="false"
:connected="false"
vehicleName=""
mode="off"
:charging="false"
:chargeCurrent="0"
/>
</Variant>
<Variant title="disabled, long title">
<Loadpoint
v-bind="state"
title="Charging point with a very very very long title!!!1!"
remoteDisabled="soft"
remoteDisabledSource="Sunny Home Manager"
mode="now"
:enabled="false"
:charging="false"
:chargePower="0"
/>
</Variant>
<Variant title="charger icon, no vehicle">
<Loadpoint
v-bind="state"
chargerIcon="heater"
title="Heating device with long name"
mode="now"
chargerFeatureIntegratedDevice
/>
</Variant>
</Story>
</template>

View file

@ -0,0 +1,53 @@
import Loadpoints from "./Loadpoints.vue";
function loadpoint(opts = {}) {
const base = {
id: 0,
pvConfigured: true,
chargePower: 2800,
chargedEnergy: 11e3,
chargeDuration: 95 * 60,
vehicleName: "tesla",
vehicleTitle: "Tesla Model 3",
enabled: true,
connected: true,
mode: "pv",
charging: true,
vehicleSoc: 66,
limitSoc: 90,
chargeCurrent: 7,
minCurrent: 6,
maxCurrent: 16,
activePhases: 2,
};
return { ...base, ...opts };
}
export default {
title: "Loadpoints/Loadpoints",
component: Loadpoints,
parameters: {
layout: "centered",
},
};
const Template = (args) => ({
components: { Loadpoints },
setup() {
return { args };
},
template: '<Loadpoints v-bind="args" />',
});
export const Standard = Template.bind({});
Standard.args = {
vehicles: [],
loadpoints: [
loadpoint({ title: "Carport" }),
loadpoint({
title: "Water Heater",
chargerFeatureIntegratedDevice: true,
chargerIcon: "waterheater",
}),
],
};

View file

@ -1,47 +0,0 @@
<script setup>
import Loadpoints from "./Loadpoints.vue";
import { reactive } from "vue";
function loadpoint(opts) {
const base = {
id: 0,
pvConfigured: true,
chargePower: 2800,
chargedEnergy: 11e3,
chargeDuration: 95 * 60,
vehicleName: "tesla",
vehicleTitle: "Tesla Model 3",
enabled: true,
connected: true,
mode: "pv",
charging: true,
vehicleSoc: 66,
limitSoc: 90,
chargeCurrent: 7,
minCurrent: 6,
maxCurrent: 16,
activePhases: 2,
};
return { ...base, ...opts };
}
const state = reactive({
vehicles: [],
loadpoints: [
loadpoint({ title: "Carport" }),
loadpoint({
title: "Water Heater",
chargerFeatureIntegratedDevice: true,
chargerIcon: "waterheater",
}),
],
});
</script>
<template>
<Story>
<Variant title="standard">
<Loadpoints v-bind="state" />
</Variant>
</Story>
</template>

View file

@ -0,0 +1,44 @@
import Mode from "./Mode.vue";
export default {
title: "Loadpoints/Mode",
component: Mode,
argTypes: {
mode: {
control: "select",
options: ["off", "now", "minpv", "pv"],
description: "Charging mode",
},
pvPossible: { control: "boolean", description: "Whether PV is possible" },
hasSmartCost: { control: "boolean", description: "Whether smart cost is available" },
},
parameters: {
layout: "centered",
},
};
const createStory = (props) => {
const story = (args) => ({
components: { Mode },
setup() {
return { args };
},
template: '<Mode v-bind="args" />',
});
story.args = props;
return story;
};
export const Minimal = createStory({ mode: "now" });
export const Full = createStory({
mode: "pv",
pvPossible: true,
hasSmartCost: true,
});
export const SmartGridOnly = createStory({
mode: "pv",
pvPossible: false,
hasSmartCost: true,
});

View file

@ -1,28 +0,0 @@
<script setup>
import { reactive } from "vue";
import Mode from "./Mode.vue";
const state = reactive({
mode: "pv",
});
</script>
<template>
<Story auto-props-disabled>
<Variant title="standard">
<Mode :mode="state.mode" />
<template #controls>
<HstSelect
v-model="state.mode"
title="mode"
:options="{
off: 'off',
now: 'now',
minpv: 'minpv',
pv: 'pv',
}"
/>
</template>
</Variant>
</Story>
</template>

View file

@ -0,0 +1,94 @@
import Phases from "./Phases.vue";
export default {
title: "Loadpoints/Phases",
component: Phases,
parameters: {
layout: "centered",
},
argTypes: {
phasesActive: { control: { type: "number" } },
minCurrent: { control: { type: "number" } },
maxCurrent: { control: { type: "number" } },
chargeCurrent: { control: { type: "number" } },
chargeCurrents: { control: { type: "object" } },
},
};
const Template = (args) => ({
components: { Phases },
setup() {
return { args };
},
template: '<Phases v-bind="args" />',
});
export const OnePhase = Template.bind({});
OnePhase.args = {
phasesActive: 1,
minCurrent: 6,
maxCurrent: 16,
chargeCurrent: 8,
chargeCurrents: null,
};
export const TwoPhase = Template.bind({});
TwoPhase.args = {
...OnePhase.args,
phasesActive: 2,
};
export const ThreePhase = Template.bind({});
ThreePhase.args = {
...OnePhase.args,
phasesActive: 3,
};
export const RealCurrents = Template.bind({});
RealCurrents.args = {
...OnePhase.args,
phasesActive: 3,
chargeCurrent: 13,
chargeCurrents: [11, 9, 12],
};
export const OnePhaseMoreAvailable = Template.bind({});
OnePhaseMoreAvailable.args = {
...OnePhase.args,
chargeCurrent: 12,
chargeCurrents: [6, 0.2, 0],
};
export const TwoPhasesActive = Template.bind({});
TwoPhasesActive.args = {
...OnePhase.args,
phasesActive: 2,
chargeCurrent: 16,
chargeCurrents: [16, 16, 0.3],
};
export const AsymetricPhases = Template.bind({});
AsymetricPhases.args = {
...OnePhase.args,
phasesActive: 2,
chargeCurrent: 16,
chargeCurrents: [8, 0.9, 14],
};
export const OnlySecondPhase = Template.bind({});
OnlySecondPhase.args = {
...OnePhase.args,
phasesActive: 1,
chargeCurrent: 13,
chargeCurrents: [0, 13, 0],
};
export const MainlyThirdPhase = Template.bind({});
MainlyThirdPhase.args = {
...OnePhase.args,
phasesActive: 1,
chargeCurrent: 10,
chargeCurrents: [0.007, 0.009, 5.945],
minCurrent: 6,
maxCurrent: 20,
};

View file

@ -1,71 +0,0 @@
<script setup>
import Phases from "./Phases.vue";
import { reactive } from "vue";
const state = reactive({
phasesActive: 1,
minCurrent: 6,
maxCurrent: 16,
chargeCurrent: 8,
chargeCurrents: null,
});
</script>
<template>
<Story :layout="{ type: 'grid', width: 300 }">
<Variant title="one phase">
<Phases v-bind="state" />
</Variant>
<Variant title="two phase">
<Phases v-bind="state" :phasesActive="2" />
</Variant>
<Variant title="three phase">
<Phases v-bind="state" :phasesActive="3" />
</Variant>
<Variant title="real currents">
<Phases
v-bind="state"
:phasesActive="3"
:chargeCurrent="13"
:chargeCurrents="[11, 9, 12]"
/>
</Variant>
<Variant title="one phase, more available">
<Phases v-bind="state" :chargeCurrent="12" :chargeCurrents="[6, 0.2, 0]" />
</Variant>
<Variant title="two phases active">
<Phases
v-bind="state"
:phasesActive="2"
:chargeCurrent="16"
:chargeCurrents="[16, 16, 0.3]"
/>
</Variant>
<Variant title="asymetric phases">
<Phases
v-bind="state"
:phasesActive="2"
:chargeCurrent="16"
:chargeCurrents="[8, 0.9, 14]"
/>
</Variant>
<Variant title="only second phase">
<Phases
v-bind="state"
:phasesActive="1"
:chargeCurrent="13"
:chargeCurrents="[0, 13, 0]"
/>
</Variant>
<Variant title="mainly third phase">
<Phases
v-bind="state"
:phasesActive="1"
:chargeCurrent="10"
:chargeCurrents="[0.007, 0.009, 5.945]"
:minCurrent="6"
:maxCurrent="20"
/>
</Variant>
</Story>
</template>

View file

@ -0,0 +1,44 @@
import MultiIcon from "./MultiIcon.vue";
export default {
title: "MultiIcon/MultiIcon",
component: MultiIcon,
argTypes: {
count: {
control: { type: "number", min: 1, max: 10 },
description: "Number of icons to display",
},
size: {
control: { type: "select", options: ["sm", "md", "lg", "xl"] },
description: "Size of the icons",
},
},
};
export const SingleIcon = (args) => ({
components: { MultiIcon },
setup() {
return { args };
},
template: '<MultiIcon v-bind="args" />',
});
export const AllCounts = (args) => ({
components: { MultiIcon },
setup() {
const counts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
return { counts, args };
},
template: `
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 30px;">
<div v-for="count in counts" :key="count" style="display: flex; flex-direction: column; align-items: center; gap: 10px;">
<MultiIcon :count="count" :size="args.size" />
<small>{{ count }}</small>
</div>
</div>
`,
});
AllCounts.args = {
size: "xl",
};

View file

@ -1,15 +0,0 @@
<script setup>
import MultiIcon from "./MultiIcon.vue";
</script>
<template>
<Story :layout="{ type: 'grid', width: 150 }">
<Variant
v-for="count in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
:key="`${count}`"
:title="`${count}`"
>
<MultiIcon :count="count" size="xl" />
</Variant>
</Story>
</template>

View file

@ -0,0 +1,16 @@
import LiveCommunity from "./LiveCommunity.vue";
export default {
title: "Savings/LiveCommunity",
component: LiveCommunity,
parameters: {
layout: "centered",
},
};
const Template = () => ({
components: { LiveCommunity },
template: "<LiveCommunity />",
});
export const Default = Template.bind({});

View file

@ -1,9 +0,0 @@
<script setup>
import LiveCommunity from "./LiveCommunity.vue";
</script>
<template>
<Story>
<LiveCommunity />
</Story>
</template>

View file

@ -1,31 +0,0 @@
<script setup>
import { reactive } from "vue";
import Savings from "./Savings.vue";
const state = reactive({
totalCharged: 408.88948736396026,
selfConsumptionPercent: 80.3,
since: 1666776003,
amount: 11.924127889686181,
effectivePrice: 0.22583777338821223,
gridCharged: 313.4964642464708,
selfConsumptionCharged: 95.39302311748945,
gridPrice: 0.34,
feedInPrice: 0.13,
currency: "EUR",
});
</script>
<template>
<Story>
<Variant title="no sponsor">
<Savings v-bind="state" />
</Variant>
<Variant title="sponsor">
<Savings v-bind="state" sponsor="naltatis" />
</Variant>
<Variant title="no data">
<Savings />
</Variant>
</Story>
</template>

View file

@ -0,0 +1,59 @@
import Sponsor from "./Sponsor.vue";
export default {
title: "Savings/Sponsor",
component: Sponsor,
parameters: {
layout: "centered",
},
argTypes: {
name: {
control: "text",
description: "Sponsor name (use 'trial' or 'victron' for special states)",
},
expiresAt: {
control: "text",
description: "When the sponsorship expires (ISO date string)",
},
expiresSoon: {
control: "boolean",
description: "Whether the sponsorship is expiring soon",
},
},
};
// Template for rendering the component
const Template = (args) => ({
components: { Sponsor },
setup() {
return { args };
},
template: '<Sponsor v-bind="args" />',
});
// Create stories for each variant
export const NoSponsor = Template.bind({});
NoSponsor.args = {};
export const Trial = Template.bind({});
Trial.args = {
name: "trial",
};
export const IndividualSponsor = Template.bind({});
IndividualSponsor.args = {
name: "naltatis",
};
export const VictronDevice = Template.bind({});
VictronDevice.args = {
name: "victron",
};
// Add an extra story showing the expiring state
export const ExpiringSponsor = Template.bind({});
ExpiringSponsor.args = {
name: "naltatis",
expiresSoon: true,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days from now
};

View file

@ -1,20 +0,0 @@
<script setup>
import Sponsor from "./Sponsor.vue";
</script>
<template>
<Story :layout="{ type: 'grid', width: 600 }">
<Variant title="no sponsor">
<Sponsor />
</Variant>
<Variant title="trial">
<Sponsor sponsor="trial" />
</Variant>
<Variant title="sponsor">
<Sponsor sponsor="naltatis" />
</Variant>
<Variant title="victron device">
<Sponsor sponsor="victron" />
</Variant>
</Story>
</template>

View file

@ -0,0 +1,33 @@
import SponsorTokenExpires from "./SponsorTokenExpires.vue";
export default {
title: "Savings/SponsorTokenExpires",
component: SponsorTokenExpires,
parameters: {
layout: "centered",
},
argTypes: {
expiresSoon: { control: "boolean", description: "Whether sponsorship is expiring soon" },
expiresAt: { control: "text", description: "Date when the sponsor token expires" },
name: { control: "text", description: "Name of the sponsor token" },
},
};
// Template for rendering the component
const Template = (args) => ({
components: { SponsorTokenExpires },
setup() {
return { args };
},
template: '<SponsorTokenExpires v-bind="args" />',
});
// Create stories for each variant
export const SomeDay = Template.bind({});
SomeDay.args = {
expiresSoon: true,
expiresAt: new Date(Date.now() + 22 * 20 * 60 * 1000).toISOString(),
};
export const Empty = Template.bind({});
Empty.args = {};

View file

@ -1,14 +0,0 @@
<script setup>
import SponsorTokenExpires from "./SponsorTokenExpires.vue";
</script>
<template>
<Story title="Sponsor Token Expires">
<Variant title="some day">
<SponsorTokenExpires :sponsorSoon="true" expiresAt="2055-01-01T00:00:00Z" />
</Variant>
<Variant title="empty">
<SponsorTokenExpires />
</Variant>
</Story>
</template>

View file

@ -0,0 +1,37 @@
import Tile from "./Tile.vue";
export default {
title: "Savings/Tile",
component: Tile,
parameters: {
layout: "centered",
},
argTypes: {
icon: { control: "text", description: "Icon name to display" },
title: { control: "text", description: "Title of the tile" },
value: { control: "text", description: "Primary value to display" },
unit: { control: "text", description: "Unit for the value" },
sub1: { control: "text", description: "First subtitle" },
sub2: { control: "text", description: "Second subtitle" },
},
};
// Template for rendering the component
const Template = (args) => ({
components: { Tile },
setup() {
return { args };
},
template: '<Tile v-bind="args" />',
});
// Create default story with the example props
export const Default = Template.bind({});
Default.args = {
icon: "coinjar",
title: "Ersparnis",
value: "14,2",
unit: "€",
sub1: "gegenüber Netzbezug",
sub2: "seit Dezember 2022",
};

View file

@ -1,16 +0,0 @@
<script setup>
import Tile from "./Tile.vue";
</script>
<template>
<Story>
<Tile
icon="coinjar"
title="Ersparnis"
value="14,2"
unit="€"
sub1="gegenüber Netzbezug"
sub2="seit Dezember 2022"
/>
</Story>
</template>

View file

@ -0,0 +1,57 @@
import Navigation from "./Navigation.vue";
export default {
title: "Top/Navigation",
component: Navigation,
parameters: {
layout: "centered",
},
argTypes: {
vehicleLogins: { control: "object" },
sponsor: { control: "object" },
},
};
const Template = (args) => ({
components: { Navigation },
setup() {
return { args };
},
template: '<Navigation v-bind="args" />',
});
export const Standard = Template.bind({});
Standard.args = {};
export const VehicleLogins = Template.bind({});
VehicleLogins.args = {
vehicleLogins: {
"Mercedes EQS": {
authenticated: true,
uri: "https://login-provider-a.test/",
},
"Nissan Leaf Pro": {
authenticated: true,
uri: "https://login-provider-b.test/",
},
},
};
export const PendingVehicleLogins = Template.bind({});
PendingVehicleLogins.args = {
vehicleLogins: {
"Mercedes EQS": {
authenticated: true,
uri: "https://login-provider-a.test/",
},
"Nissan Leaf Pro": {
authenticated: false,
uri: "https://login-provider-b.test/",
},
},
};
export const TokenExpires = Template.bind({});
TokenExpires.args = {
sponsor: { expiresSoon: true },
};

View file

@ -1,42 +0,0 @@
<script setup>
import Navigation from "./Navigation.vue";
</script>
<template>
<Story title="Top Navigation">
<Variant title="standard">
<Navigation />
</Variant>
<Variant title="vehicle logins">
<Navigation
:vehicleLogins="{
['Mercedes EQS']: {
authenticated: true,
uri: 'https://login-provider-a.test/',
},
['Nissan Leaf Pro']: {
authenticated: true,
uri: 'https://login-provider-b.test/',
},
}"
/>
</Variant>
<Variant title="pending vehicle logins">
<Navigation
:vehicleLogins="{
['Mercedes EQS']: {
authenticated: true,
uri: 'https://login-provider-a.test/',
},
['Nissan Leaf Pro']: {
authenticated: false,
uri: 'https://login-provider-b.test/',
},
}"
/>
</Variant>
<Variant title="token expires">
<Navigation :sponsor="{ expiresSoon: true }" />
</Variant>
</Story>
</template>

View file

@ -0,0 +1,80 @@
import Notifications from "./Notifications.vue";
const timeAgo = (hours = 0, minutes = 0, seconds = 0) => {
const date = new Date();
date.setHours(date.getHours() - hours);
date.setMinutes(date.getMinutes() - minutes);
date.setSeconds(date.getSeconds() - seconds);
return date;
};
const notificationsData = [
{
message: "Server unavailable",
level: "error",
time: timeAgo(),
count: 1,
},
{
message: "charger out of sync: expected disabled, got enabled",
level: "warn",
count: 4,
time: timeAgo(0, 0, 42),
},
{
message: "Sponsortoken: x509: certificate has expired",
level: "error",
count: 1,
time: timeAgo(1, 12, 44),
},
{
message:
"Block device full: can not write to /tmp/evcc/foobarloremawefhwuiehfwuiehfwiauhefjkajowaeigjwalvmoweivwail",
level: "error",
count: 1,
time: timeAgo(1, 12, 44),
},
{
message: "charger out of sync: expected disabled, got enabled",
level: "warn",
count: 4,
time: timeAgo(1, 22, 0),
},
{
message: "vehicle remote charge start: invalid character '<' looking for beginning of value",
level: "warn",
count: 3,
time: timeAgo(4, 2, 0),
},
{
message:
"Amet irure quis incididunt voluptate esse. Commodo ea sunt est ipsum tempor nisi laboris voluptate labore elit laborum. Ex irure commodo reprehenderit consequat consequat do ad tempor aliquip deserunt eu. Laboris minim nostrud quis nisi. Dolor occaecat reprehenderit velit dolore exercitation cupidatat et voluptate. Nulla pariatur deserunt esse minim nisi nisi nulla. Sit eiusmod do incididunt sint minim pariatur aute.",
level: "warn",
count: 1,
time: timeAgo(5, 2, 44),
},
];
export default {
title: "Top/Notifications",
component: Notifications,
parameters: {
layout: "centered",
},
argTypes: {
notifications: { control: "object" },
},
};
const Template = (args) => ({
components: { Notifications },
setup() {
return { args };
},
template: '<Notifications v-bind="args" />',
});
export const Default = Template.bind({});
Default.args = {
notifications: notificationsData,
};

View file

@ -1,70 +0,0 @@
<script setup>
import { reactive } from "vue";
import Notifications from "./Notifications.vue";
const timeAgo = (hours = 0, minutes = 0, seconds = 0) => {
const date = new Date();
date.setHours(date.getHours() - hours);
date.setMinutes(date.getMinutes() - minutes);
date.setSeconds(date.getSeconds() - seconds);
return date;
};
const state = reactive({
notifications: [
{
message: "Server unavailable",
level: "error",
time: timeAgo(),
count: 1,
},
{
message: "charger out of sync: expected disabled, got enabled",
level: "warn",
count: 4,
time: timeAgo(0, 0, 42),
},
{
message: "Sponsortoken: x509: certificate has expired",
level: "error",
count: 1,
time: timeAgo(1, 12, 44),
},
{
message:
"Block device full: can not write to /tmp/evcc/foobarloremawefhwuiehfwuiehfwiauhefjkajowaeigjwalvmoweivwail",
level: "error",
count: 1,
time: timeAgo(1, 12, 44),
},
{
message: "charger out of sync: expected disabled, got enabled",
level: "warn",
count: 4,
time: timeAgo(1, 22, 0),
},
{
message:
"vehicle remote charge start: invalid character '<' looking for beginning of value",
level: "warn",
count: 3,
time: timeAgo(4, 2, 0),
},
{
message:
"Amet irure quis incididunt voluptate esse. Commodo ea sunt est ipsum tempor nisi laboris voluptate labore elit laborum. Ex irure commodo reprehenderit consequat consequat do ad tempor aliquip deserunt eu. Laboris minim nostrud quis nisi. Dolor occaecat reprehenderit velit dolore exercitation cupidatat et voluptate. Nulla pariatur deserunt esse minim nisi nisi nulla. Sit eiusmod do incididunt sint minim pariatur aute.",
level: "warn",
count: 1,
time: timeAgo(5, 2, 44),
},
],
});
</script>
<template>
<Story>
<Variant title="default">
<Notifications v-bind="state" />
</Variant>
</Story>
</template>

View file

@ -0,0 +1,103 @@
import VehicleIcon from "./VehicleIcon.vue";
const icons = [
"car",
"bike",
"bus",
"moped",
"motorcycle",
"rocket",
"scooter",
"taxi",
"tractor",
"rickshaw",
"shuttle",
"van",
"airpurifier",
"battery",
"bulb",
"climate",
"coffeemaker",
"compute",
"cooking",
"cooler",
"desktop",
"device",
"dishwasher",
"dryer",
"floorlamp",
"generic",
"heater",
"heatexchange",
"heatpump",
"kettle",
"laundry",
"laundry2",
"machine",
"meter",
"microwave",
"pump",
"tool",
"waterheater",
];
export default {
title: "VehicleIcon/VehicleIcon",
component: VehicleIcon,
argTypes: {
name: {
control: "select",
options: icons,
},
names: { control: "object" },
size: {
control: "select",
options: ["sm", "md", "lg", "xl"],
defaultValue: "xl",
},
},
};
const Template = (args) => ({
components: { VehicleIcon },
setup() {
return { args };
},
template: '<VehicleIcon v-bind="args" />',
});
// Single icon story that can be controlled via args
export const SingleIcon = Template.bind({});
SingleIcon.args = {
name: "car",
size: "xl",
};
// Multiple icons stories
export const TwoCars = Template.bind({});
TwoCars.args = {
names: ["car", "car"],
size: "xl",
};
export const CarAndThreeBikes = Template.bind({});
CarAndThreeBikes.args = {
names: ["car", "bike", "bike", "bike"],
size: "xl",
};
// Story showing all icons at once
export const AllIcons = () => ({
components: { VehicleIcon },
setup() {
return { icons };
},
template: `
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 30px;">
<div v-for="icon in icons" :key="icon" style="display: flex; flex-direction: column; align-items: center; gap: 10px;">
<VehicleIcon :name="icon" size="xl" />
<small>{{ icon }}</small>
</div>
</div>
`,
});

View file

@ -1,58 +0,0 @@
<script setup>
import VehicleIcon from "./VehicleIcon.vue";
const icons = [
"car",
"bike",
"bus",
"moped",
"motorcycle",
"rocket",
"scooter",
"taxi",
"tractor",
"rickshaw",
"shuttle",
"van",
"airpurifier",
"battery",
"bulb",
"climate",
"coffeemaker",
"compute",
"cooking",
"cooler",
"desktop",
"device",
"dishwasher",
"dryer",
"floorlamp",
"generic",
"heater",
"heatexchange",
"heatpump",
"kettle",
"laundry",
"laundry2",
"machine",
"meter",
"microwave",
"pump",
"tool",
"waterheater",
];
</script>
<template>
<Story :layout="{ type: 'grid', width: 150 }">
<Variant v-for="icon in icons" :key="icon" :title="icon">
<VehicleIcon :name="icon" size="xl" />
</Variant>
<Variant title="2 cars">
<VehicleIcon :names="['car', 'car']" size="xl" />
</Variant>
<Variant title="car & 3 bikes">
<VehicleIcon :names="['car', 'bike', 'bike', 'bike']" size="xl" />
</Variant>
</Story>
</template>

View file

@ -0,0 +1,188 @@
import Vehicle from "./Vehicle.vue";
const hoursFromNow = (hours) => {
const now = new Date();
now.setHours(now.getHours() + hours);
return now.toISOString();
};
const baseState = {
vehicle: { title: "Mein Auto", icon: "car", capacity: 72, features: [] },
enabled: false,
connected: true,
vehicleName: "meinauto",
vehicleSoc: 42.742,
vehicleRange: 231,
limitSoc: 90,
chargedEnergy: 14123,
socBasedCharging: true,
id: 0,
};
export default {
title: "Vehicles/Vehicle",
component: Vehicle,
argTypes: {
chargedEnergy: { control: "number" },
charging: { control: "boolean" },
vehicleClimaterActive: { control: "boolean" },
vehicleWelcomeActive: { control: "boolean" },
connected: { control: "boolean" },
currency: { control: "text" },
effectiveLimitSoc: { control: "number" },
effectivePlanSoc: { control: "number" },
effectivePlanTime: { control: "text" },
batteryBoostActive: { control: "boolean" },
enabled: { control: "boolean" },
heating: { control: "boolean" },
id: { control: "text" },
integratedDevice: { control: "boolean" },
limitEnergy: { control: "number" },
mode: { control: "text" },
chargerStatusReason: { control: "text" },
vehicle: { control: "object" },
vehicleName: { control: "text" },
vehicleRange: { control: "number" },
vehicleSoc: { control: "number" },
vehicleLimitSoc: { control: "number" },
socBasedCharging: { control: "boolean" },
},
};
const Template = (args) => ({
components: { Vehicle },
setup() {
return { args };
},
template: '<Vehicle v-bind="args" />',
});
export const Disconnected = Template.bind({});
Disconnected.args = {
...baseState,
connected: false,
};
export const Connected = Template.bind({});
Connected.args = {
...baseState,
};
export const Ready = Template.bind({});
Ready.args = {
...baseState,
enabled: true,
};
export const Charging = Template.bind({});
Charging.args = {
...baseState,
enabled: true,
charging: true,
};
export const UnknownVehicle = Template.bind({});
UnknownVehicle.args = {
...baseState,
enabled: true,
charging: true,
vehicleName: "",
socBasedCharging: false,
vehicle: { ...baseState.vehicle, capacity: null },
mode: "pv",
};
export const OfflineVehicle = Template.bind({});
OfflineVehicle.args = {
...baseState,
enabled: true,
charging: true,
socBasedCharging: false,
vehicle: {
...baseState.vehicle,
title: "Opel Corsa-e",
capacity: 72,
features: ["Offline"],
},
mode: "pv",
};
export const OfflineVehicleWithTarget = Template.bind({});
OfflineVehicleWithTarget.args = {
...baseState,
enabled: true,
charging: true,
socBasedCharging: false,
vehicle: {
...baseState.vehicle,
title: "Opel Corsa-e",
capacity: 72,
features: ["Offline"],
},
targetEnergy: 30,
mode: "pv",
};
export const VehicleLimit = Template.bind({});
VehicleLimit.args = {
...baseState,
enabled: true,
charging: true,
vehicleLimitSoc: 80,
};
export const VehicleLimitReached = Template.bind({});
VehicleLimitReached.args = {
...baseState,
enabled: true,
vehicleLimitSoc: 80,
vehicleSoc: 80,
};
export const TargetChargePlanned = Template.bind({});
TargetChargePlanned.args = {
...baseState,
targetTime: hoursFromNow(34),
mode: "pv",
};
export const TargetChargeActive = Template.bind({});
TargetChargeActive.args = {
...baseState,
enabled: true,
charging: true,
targetTime: hoursFromNow(14),
mode: "pv",
};
export const SmartChargeCostLimitActive = Template.bind({});
SmartChargeCostLimitActive.args = {
...baseState,
enabled: true,
charging: true,
smartCostLimit: 0.13,
mode: "pv",
};
export const PvEnableTimer = Template.bind({});
PvEnableTimer.args = {
...baseState,
pvAction: "enable",
pvRemainingInterpolated: 32,
};
export const PvDisableTimer = Template.bind({});
PvDisableTimer.args = {
...baseState,
enabled: true,
charging: true,
pvAction: "disable",
pvRemainingInterpolated: 155,
};
export const VehicleSwitch = Template.bind({});
VehicleSwitch.args = {
...baseState,
vehicleTitle: "Blauer e-Golf",
vehicles: ["Blauer e-Golf", "Weißes Model 3"],
};

View file

@ -1,119 +0,0 @@
<script setup>
import { reactive } from "vue";
import Vehicle from "./Vehicle.vue";
const state = reactive({
vehicle: { title: "Mein Auto", icon: "car", capacity: 72, features: [] },
enabled: false,
connected: true,
vehicleName: "meinauto",
vehicleSoc: 42.742,
vehicleRange: 231,
limitSoc: 90,
chargedEnergy: 14123,
socBasedCharging: true,
id: 0,
});
const hoursFromNow = function (hours) {
const now = new Date();
now.setHours(now.getHours() + hours);
return now.toISOString();
};
</script>
<template>
<Story>
<Variant title="disconnected">
<Vehicle v-bind="state" :connected="false" />
</Variant>
<Variant title="connected">
<Vehicle v-bind="state" />
</Variant>
<Variant title="ready">
<Vehicle v-bind="state" enabled />
</Variant>
<Variant title="charging">
<Vehicle v-bind="state" enabled charging />
</Variant>
<Variant title="charging">
<Vehicle v-bind="state" enabled charging />
</Variant>
<Variant title="unknown vehicle">
<Vehicle
v-bind="state"
enabled
charging
vehicleName=""
:socBasedCharging="false"
:vehicle="{ ...state.vehicle, capacity: null }"
mode="pv"
/>
</Variant>
<Variant title="offline vehicle">
<Vehicle
v-bind="state"
enabled
charging
:socBasedCharging="false"
:vehicle="{
...state.vehicle,
title: 'Opel Corsa-e',
capacity: 72,
features: ['Offline'],
}"
mode="pv"
/>
</Variant>
<Variant title="offline vehicle with target">
<Vehicle
v-bind="state"
enabled
charging
:socBasedCharging="false"
:vehicle="{
...state.vehicle,
title: 'Opel Corsa-e',
capacity: 72,
features: ['Offline'],
}"
:targetEnergy="30"
mode="pv"
/>
</Variant>
<Variant title="vehicle limit">
<Vehicle v-bind="state" enabled charging :vehicleLimitSoc="80" />
</Variant>
<Variant title="vehicle limit reached">
<Vehicle v-bind="state" enabled :vehicleLimitSoc="80" :vehicleSoc="80" />
</Variant>
<Variant title="target charge planned">
<Vehicle v-bind="state" :targetTime="hoursFromNow(34)" mode="pv" />
</Variant>
<Variant title="target charge active">
<Vehicle v-bind="state" enabled charging :targetTime="hoursFromNow(14)" mode="pv" />
</Variant>
<Variant title="smart charge cost limit active">
<Vehicle v-bind="state" enabled charging :smartCostLimit="0.13" mode="pv" />
</Variant>
<Variant title="pv enable timer">
<Vehicle v-bind="state" pvAction="enable" :pvRemainingInterpolated="32" />
</Variant>
<Variant title="pv disable timer">
<Vehicle
v-bind="state"
enabled
charging
pvAction="disable"
:pvRemainingInterpolated="155"
/>
</Variant>
<Variant title="vehicle switch">
<Vehicle
v-bind="state"
vehicleTitle="Blauer e-Golf"
:vehicles="['Blauer e-Golf', 'Weißes Model 3']"
/>
</Variant>
</Story>
</template>

View file

@ -1,21 +0,0 @@
import { defineConfig } from "histoire";
import { HstVue } from "@histoire/plugin-vue";
export default defineConfig({
plugins: [HstVue()],
setupFile: "./histoire.setup.js",
viteNodeInlineDeps: [/!axios/, /shopicons/],
routerMode: "hash",
backgroundPresets: [
{
label: "default",
color: "var(--evcc-background)",
contrastColor: "var(--evcc-default-text)",
},
{
label: "box",
color: "var(--evcc-box)",
contrastColor: "var(--evcc-default-text)",
},
],
});

3698
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,9 +7,9 @@
"test": "TZ=Europe/Berlin vitest",
"lint": "prettier assets './**/*.{yml,yaml,sh}' --write && eslint assets/**/*.js assets/**/*.vue --fix --max-warnings=0",
"dev": "vite",
"histoire": "histoire dev",
"playwright": "playwright test --ui",
"simulator": "vite tests/simulator"
"simulator": "vite tests/simulator",
"storybook": "storybook dev -p 6006"
},
"type": "module",
"main": "index.js",
@ -18,7 +18,6 @@
"@eslint/js": "^9.13.0",
"@guolao/vue-monaco-editor": "^1.5.1",
"@h2d2/shopicons": "^1.2.0",
"@histoire/plugin-vue": "^1.0.0-alpha.1",
"@playwright/test": "^1.46.1",
"@popperjs/core": "^2.11.5",
"@unhead/vue": "^1.8.9",
@ -47,7 +46,6 @@
"eslint-plugin-vue": "^9.3.0",
"globals": "^16.0.0",
"happy-dom": "^17.4.4",
"histoire": "^1.0.0-alpha.1",
"markty-toml": "^0.1.1",
"prettier": "^3.0.0",
"prettier-plugin-sh": "^0.14.0",
@ -72,5 +70,21 @@
"node": ">=22.0.0"
},
"license": "MIT",
"repository": "github:evcc-io/evcc"
}
"repository": "github:evcc-io/evcc",
"eslintConfig": {
"extends": [
"plugin:storybook/recommended"
]
},
"devDependencies": {
"@chromatic-com/storybook": "^3.2.5",
"@storybook/addon-essentials": "^8.6.4",
"@storybook/addon-interactions": "^8.6.4",
"@storybook/blocks": "^8.6.4",
"@storybook/test": "^8.6.4",
"@storybook/vue3": "^8.6.4",
"@storybook/vue3-vite": "^8.6.4",
"eslint-plugin-storybook": "^0.11.4",
"storybook": "^8.6.4"
}
}