Browse Source

started on icons

pull/1/head
Stian Fredrik Aune 3 years ago
parent
commit
ea3037b03a
  1. 4
      webui/package.json
  2. 34
      webui/src/App.tsx
  3. 10
      webui/src/actions/ColorPresets.ts
  4. 53
      webui/src/contexts/DataContext.tsx
  5. 2
      webui/src/helpers/kelvin.ts
  6. 3
      webui/src/helpers/misc.ts
  7. 81
      webui/src/helpers/rest.ts
  8. 5
      webui/src/index.tsx
  9. 28
      webui/src/models/Icons.ts
  10. 4
      webui/src/models/Shared.ts
  11. 24
      webui/src/pages/SettingsPage.tsx
  12. 54
      webui/src/primitives/Elements.tsx
  13. 10
      webui/src/primitives/Layout.sass
  14. 54
      webui/src/primitives/Layout.tsx
  15. 53
      webui/yarn.lock

4
webui/package.json

@ -3,6 +3,8 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@iconify-icons/mdi": "^1.1.15",
"@iconify/react": "^1.1.4",
"@jaames/iro": "^5.5.1",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
@ -11,11 +13,13 @@
"@types/node": "^12.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"axios": "^0.21.1",
"hookrouter": "^1.2.5",
"kelvin-to-rgb": "^1.0.2",
"node-sass": "4.14.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-modal": "^3.13.1",
"react-scripts": "4.0.3",
"react-semantic-ui-range": "^0.7.1",
"typescript": "^4.1.2",

34
webui/src/App.tsx

@ -2,7 +2,9 @@ import React from 'react';
import {HookRouter, navigate, usePath, useRoutes} from "hookrouter";
import {Tabs} from "./primitives/Layout";
import {HSColorPicker} from "./primitives/Forms";
import {ColorElement} from "./primitives/Elements";
import {IconElement} from "./primitives/Elements";
import SettingsPage from "./pages/SettingsPage";
import {LuciferIcon} from "./models/Icons";
const routeObj: HookRouter.RouteObject = {
"/": () => (
@ -12,22 +14,22 @@ const routeObj: HookRouter.RouteObject = {
),
"/devices": () => (
<div>
<ColorElement name="Grønn" value={{h: 125, s: 0.5}}/>
<ColorElement name="Kelvin 1k" value={{kelvin: 1000}}/>
<ColorElement name="Kelvin 2k" value={{kelvin: 2000}}/>
<ColorElement name="Kelvin 3k" value={{kelvin: 3000}}/>
<ColorElement name="Kelvin 4k" value={{kelvin: 4000}}/>
<ColorElement name="Kelvin 5k" value={{kelvin: 5000}}/>
<ColorElement name="Kelvin 6k" value={{kelvin: 6000}}/>
<ColorElement name="Kelvin 7k" value={{kelvin: 7000}}/>
<ColorElement name="Kelvin 8k" value={{kelvin: 8000}}/>
<ColorElement name="Kelvin 9k" value={{kelvin: 9000}}/>
<ColorElement name="Kelvin 10k" value={{kelvin: 10000}}/>
<ColorElement name="Mer... (HS)" value="hs-gradient"/>
<ColorElement name="Mer... (K)" value="k-gradient"/>
<IconElement icon={LuciferIcon.Hexagon} caption="Grønn" color={{h: 125, s: 0.5}}/>
<IconElement icon={LuciferIcon.Square} caption="Kelvin 1k" color={{kelvin: 1000}}/>
<IconElement icon={LuciferIcon.Bulb} caption="Kelvin 2k" color={{kelvin: 2000}}/>
<IconElement icon={LuciferIcon.BulbGroup} caption="Kelvin 3k" color={{kelvin: 3000}}/>
<IconElement icon={LuciferIcon.Bridge} caption="Kelvin 4k" color={{kelvin: 4000}}/>
<IconElement caption="Kelvin 5k" color={{kelvin: 5000}}/>
<IconElement caption="Kelvin 6k" color={{kelvin: 6000}}/>
<IconElement caption="Kelvin 7k" color={{kelvin: 7000}}/>
<IconElement caption="Kelvin 8k" color={{kelvin: 8000}}/>
<IconElement caption="Kelvin 9k" color={{kelvin: 9000}}/>
<IconElement caption="Kelvin 10k" color={{kelvin: 10000}}/>
<IconElement caption="Mer" color="hs-gradient"/>
<IconElement caption="Mer" color="k-gradient"/>
</div>
),
"/settings": () => <div>3</div>,
"/settings": () => <SettingsPage/>,
}
const routeList = ["/", "/devices", "/settings"];
@ -43,7 +45,7 @@ function App() {
<Tabs tabNames={tabNames}
index={routeList.indexOf(path)}
onChange={i => navigate(routeList[i])}
boldIndex={0}
large boldIndex={0}
/>
{route || <div>B</div>}
</div>

10
webui/src/actions/ColorPresets.ts

@ -0,0 +1,10 @@
import {ColorPreset} from "../models/Colors";
import {getRequest, postRequest} from "../helpers/rest";
export function listColorPresets(): Promise<ColorPreset[]> {
return getRequest("/color-presets");
}
export function addColorPreset(name: string, colorString: string) {
return postRequest("/color-presets", {name, colorString});
}

53
webui/src/contexts/DataContext.tsx

@ -1,5 +1,56 @@
import React from "react";
import React, {createContext, useCallback, useEffect, useState} from "react";
import {ColorPreset} from "../models/Colors";
import {DataTarget} from "../models/Shared";
import {unimplemented} from "../helpers/misc";
import {listColorPresets} from "../actions/ColorPresets";
interface DataContextValue {
colorPresets: ColorPreset[]
ready(target?: DataTarget): boolean
refresh(target?: DataTarget): void
}
const DataContext = createContext<DataContextValue>({
colorPresets: [],
ready: unimplemented,
refresh: unimplemented,
});
export const DataContextProvider: React.FC = ({children}) => {
const [colorPresets, setColorPresets] = useState<ColorPreset[]>([]);
const [readyMap, setReadyMap] = useState<{ [key: string]: boolean }>({
[DataTarget.Default]: false,
[DataTarget.ColorPresets]: false,
});
const ready = useCallback((target: DataTarget = DataTarget.Default) => {
return readyMap[target] || false;
}, [readyMap]);
const refresh = useCallback((target: DataTarget = DataTarget.Default) => {
const refreshing: Promise<any>[] = [];
if ([DataTarget.Default, DataTarget.ColorPresets].includes(target)) {
refreshing.push(listColorPresets().then(setColorPresets));
}
Promise.all(refreshing).then(() => setReadyMap(prev => ({...prev, [target]: true})));
}, []);
useEffect(() => {
refresh();
}, [refresh]);
return (
<DataContext.Provider value={{
colorPresets,
ready, refresh,
}}>
{children}
</DataContext.Provider>
);
};
export default DataContext;

2
webui/src/helpers/kelvin.ts

@ -1,5 +1,3 @@
// @ts-ignore
import kelvinToRgb from "kelvin-to-rgb";
import {ColorValue} from "../models/Colors";
import iro from "@jaames/iro";

3
webui/src/helpers/misc.ts

@ -0,0 +1,3 @@
export function unimplemented<T>(): T {
throw new Error("unimplemented");
}

81
webui/src/helpers/rest.ts

@ -0,0 +1,81 @@
import Axios, {AxiosError, AxiosRequestConfig, AxiosResponse} from "axios";
interface Response<T> {
code: number
message: string
data: T
}
export const authError = new Error("unauthenticated");
export async function getRequest<T>(url: string): Promise<T> {
const cors = url.includes("//");
return sendRequest<T>({
method: "GET",
url: cors ? url : `/api${url}`,
responseType: "json",
}, cors);
}
export function postRequest<T>(url: string, data: object = {}): Promise<T> {
return sendRequest<T>({
method: "POST",
url: `/api${url}`,
responseType: "json",
data,
});
}
export function putRequest<T>(url: string, data: object): Promise<T> {
return sendRequest<T>({
method: "PUT",
url: `/api${url}`,
responseType: "json",
data,
});
}
export function deleteRequest<T>(url: string): Promise<T> {
return sendRequest<T>({
method: "DELETE",
url: `/api${url}`,
responseType: "json",
});
}
async function sendRequest<T>(config: AxiosRequestConfig, cors: boolean = false): Promise<T> {
try {
const res = await Axios(config);
return await handleResponse(res, cors);
} catch (e) {
if (e === authError) {
throw e;
}
return await handleResponse((e as AxiosError).response as AxiosResponse);
}
}
function handleResponse<T>(response: AxiosResponse, object: boolean = false): Promise<T> {
if (object) {
return Promise.resolve(response.data);
} else {
const obj: Response<T> | undefined = response?.data;
if (![200, 201].includes(response.status)) {
if (response.status === 403) {
return Promise.reject(authError);
} else {
return Promise.reject(obj?.message || "Ukjent feil");
}
}
if (!obj) {
return Promise.reject("Ingen data mottatt");
}
return Promise.resolve(obj.data);
}
}

5
webui/src/index.tsx

@ -3,10 +3,13 @@ import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from "./reportWebVitals";
import {DataContextProvider} from "./contexts/DataContext";
ReactDOM.render(
<React.StrictMode>
<App />
<DataContextProvider>
<App/>
</DataContextProvider>
</React.StrictMode>,
document.getElementById('root')
);

28
webui/src/models/Icons.ts

@ -0,0 +1,28 @@
import bridgeIcon from "@iconify-icons/mdi/bridge";
import hexagonIcon from "@iconify-icons/mdi/hexagon";
import lightBulbIcon from "@iconify-icons/mdi/lightbulb";
import lightBulbGroupIcon from "@iconify-icons/mdi/lightbulb-group";
import squareIcon from "@iconify-icons/mdi/square-rounded";
// To add a new icon, follow the instructions on this page:
// https://iconify.design/icon-sets/mdi/
export enum LuciferIcon {
Bridge = "Bridge",
Bulb = "Bulb",
BulbGroup = "BulbGroup",
Hexagon = "Hexagon",
Square = "Square",
}
const iconMap = {
[LuciferIcon.Bridge]: bridgeIcon,
[LuciferIcon.Bulb]: lightBulbIcon,
[LuciferIcon.BulbGroup]: lightBulbGroupIcon,
[LuciferIcon.Hexagon]: hexagonIcon,
[LuciferIcon.Square]: squareIcon,
}
export function toIconify(icon: LuciferIcon): object {
return iconMap[icon];
}

4
webui/src/models/Shared.ts

@ -0,0 +1,4 @@
export enum DataTarget {
Default = "",
ColorPresets = "ColorPresets",
}

24
webui/src/pages/SettingsPage.tsx

@ -0,0 +1,24 @@
import React, {useContext, useEffect} from "react";
import {Page} from "../primitives/Layout";
import DataContext from "../contexts/DataContext";
import {IconElement} from "../primitives/Elements";
import {addColorPreset} from "../actions/ColorPresets";
const SettingsPage: React.FC = () => {
const {colorPresets, refresh} = useContext(DataContext);
useEffect(() => {
}, []);
return (
<Page title="Oppsett">
<h1>Definerte farger</h1>
{colorPresets.map(cp => (
<IconElement key={cp.id} {...cp} caption={cp.name}/>
))}
<button>Add</button>
</Page>
);
};
export default SettingsPage;

54
webui/src/primitives/Elements.tsx

@ -1,10 +1,10 @@
import React, {CSSProperties, useMemo} from "react";
import {ColorValue} from "../models/Colors";
// @ts-ignore
import kelvinToRgb from "kelvin-to-rgb";
import {kelvinToRgbHex} from "../helpers/kelvin";
import {Icon} from "@iconify/react";
import "./Elements.sass";
import {kelvinToColorValue, kelvinToRgbHex} from "../helpers/kelvin";
import {LuciferIcon, toIconify} from "../models/Icons";
function Element({children}: React.PropsWithChildren<{}>) {
return (
@ -13,35 +13,41 @@ function Element({children}: React.PropsWithChildren<{}>) {
}
interface ColorElementProps {
name: string
value: ColorValue | "hs-gradient" | "k-gradient"
caption: string
icon?: LuciferIcon
color?: ColorValue | "hs-gradient" | "k-gradient"
}
export function ColorElement({name, value}: ColorElementProps) {
export function IconElement({caption, color, icon}: ColorElementProps) {
const style: CSSProperties = useMemo(() => {
const s: CSSProperties = {};
if (value === "hs-gradient") {
s.backgroundImage = "linear-gradient(to bottom right, red, yellow, green, blue, violet)";
} else if (value === "k-gradient") {
s.backgroundImage = "linear-gradient(to bottom right, #ff9329, #ffd6aa, #fffaf4, #fff, #c9e2ff, #40a3ff)";
} else if (value.kelvin !== undefined && value.kelvin > 0) {
s.backgroundColor = kelvinToRgbHex(value.kelvin);
} else {
const hue = value.h || 0;
const sat = Math.floor((value.s || 0) * 100);
s.backgroundColor = `hsl(${hue}, ${sat}%, 50%)`;
const s: CSSProperties = {
backgroundClip: "text",
// @ts-ignore
textFillColor: "transparent",
};
if (color !== undefined) {
if (color === "hs-gradient") {
s.backgroundImage = "linear-gradient(to bottom right, red, yellow, green, blue, violet)";
} else if (color === "k-gradient") {
s.backgroundImage = "linear-gradient(to bottom right, #ff9329, #ffd6aa, #fffaf4, #fff, #c9e2ff, #40a3ff)";
} else if (color.kelvin !== undefined && color.kelvin > 0) {
s.color = kelvinToRgbHex(color.kelvin);
} else {
const hue = color.h || 0;
const sat = Math.floor((color.s || 0) * 100);
s.color = `hsl(${hue}, ${sat}%, 50%)`;
}
}
return s;
}, [value]);
}, [color]);
return (
<Element>
<div className="ColorElement-block"
style={style}
/>
{name}
<Icon icon={toIconify(icon || LuciferIcon.Square)} className="ColorElement-block" style={style}/>
{caption}
</Element>
);
}

10
webui/src/primitives/Layout.sass

@ -3,6 +3,10 @@
.Tabs-container
background-color: $color-foreground-dark
&.Tabs-large
text-align: center
font-size: 175%
.Tabs-element
display: inline-block
padding: 0.25em 0.7ch
@ -14,4 +18,8 @@
.Tabs-bold
font-family: 'Bitstream Vera Serif', 'Lucida Fax', serif
font-weight: 600
font-weight: 600
.Page
max-width: 800px
margin: 0.25em auto

54
webui/src/primitives/Layout.tsx

@ -1,4 +1,4 @@
import React, {useCallback, useEffect} from "react";
import React, {PropsWithChildren, useCallback, useEffect} from "react";
import "./Layout.sass";
interface TabsProps {
@ -6,9 +6,10 @@ interface TabsProps {
index: number
onChange: (newIndex: number) => void
boldIndex?: number
large?: boolean
}
export function Tabs({tabNames, index, onChange, boldIndex}: TabsProps) {
export function Tabs({tabNames, index, onChange, large, boldIndex}: TabsProps) {
useEffect(() => {
if (index < 0) {
onChange(0);
@ -32,14 +33,47 @@ export function Tabs({tabNames, index, onChange, boldIndex}: TabsProps) {
}, [index, boldIndex])
return (
<div className="Tabs-container">
{tabNames.map((name, i) => (
<div className={tabClass(i)}
onClick={() => onChange(i)}
>
{name}
</div>
))}
<div className={`Tabs-container ${large ? "Tabs-large" : ""}`}>
<div className="Tabs-inner">
{tabNames.map((name, i) => (
<div className={tabClass(i)}
onClick={() => onChange(i)}
>
{name}
</div>
))}
</div>
</div>
);
}
interface PageProps {
title?: string
}
export function Page({title, children}: PropsWithChildren<PageProps>) {
useEffect(() => {
window.document.title = title ? `${title} - Lucifer` : "Lucifer";
}, []);
return (
<div className="Page">
{children}
</div>
);
}
interface DialogProps {
title: string
buttons?: {
text: string
onClick?: (() => void)
}[]
loading?: boolean
}
export function Dialog({}: PropsWithChildren<DialogProps>) {
return undefined;
}

53
webui/yarn.lock

@ -1209,6 +1209,16 @@
dependencies:
"@hapi/hoek" "^8.3.0"
"@iconify-icons/mdi@^1.1.15":
version "1.1.15"
resolved "https://registry.yarnpkg.com/@iconify-icons/mdi/-/mdi-1.1.15.tgz#eff8e16d5095828e60bacc476c9facc993e8c301"
integrity sha512-NozPlEOMiXwYheHet3u5ktu0o/GP+pCtta8kb2jGf0jSgOePirK73wj2u8QniZUot/xtT0KprvwXaBg/IFX2Og==
"@iconify/react@^1.1.4":
version "1.1.4"
resolved "https://registry.yarnpkg.com/@iconify/react/-/react-1.1.4.tgz#c778eddbaf196e55705d0bedff00d039dc1de8d3"
integrity sha512-oxq8IMOq8q3nKGiDHbQPC8FcFuBsKve68JWBo140d5LRnj0bv5TB/FE/y01ZSvEZ7PlI2HIrnb2qivPN8kTDgw==
"@irojs/iro-core@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@irojs/iro-core/-/iro-core-1.2.0.tgz#3587c2db7a6de09f76dbf75b94605ac251039ca8"
@ -2583,6 +2593,13 @@ axe-core@^4.0.2:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.2.tgz#7cf783331320098bfbef620df3b3c770147bc224"
integrity sha512-V+Nq70NxKhYt89ArVcaNL9FDryB3vQOd+BFXZIfO3RP6rwtj+2yqqqdHEkacutglPaZLkJeuXKCjCJDMGPtPqg==
axios@^0.21.1:
version "0.21.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
dependencies:
follow-redirects "^1.10.0"
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@ -4858,6 +4875,11 @@ execa@^4.0.0:
signal-exit "^3.0.2"
strip-final-newline "^2.0.0"
exenv@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
exit@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
@ -5162,6 +5184,11 @@ follow-redirects@^1.0.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.2.tgz#dd73c8effc12728ba5cf4259d760ea5fb83e3147"
integrity sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==
follow-redirects@^1.10.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@ -7207,7 +7234,7 @@ loglevel@^1.6.8:
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==
loose-envify@^1.1.0, loose-envify@^1.4.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@ -9166,7 +9193,7 @@ prompts@2.4.0, prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.7.2:
prop-types@^15.5.10, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -9401,6 +9428,21 @@ react-is@^17.0.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
react-lifecycles-compat@^3.0.0:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-modal@^3.13.1:
version "3.13.1"
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.13.1.tgz#a02dce63bbfee7582936f1e9835d518ef8f56453"
integrity sha512-m6yXK7I4YKssQnsjHK7xITSXy2O81BSOHOsg0/uWAsdKtuT9HF2tdoYhRuxNNQg2V+LgepsoHUPJKS8m6no+eg==
dependencies:
exenv "^1.2.0"
prop-types "^15.5.10"
react-lifecycles-compat "^3.0.0"
warning "^4.0.3"
react-refresh@^0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
@ -11470,6 +11512,13 @@ walker@^1.0.7, walker@~1.0.5:
dependencies:
makeerror "1.0.x"
warning@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
dependencies:
loose-envify "^1.0.0"
watchpack-chokidar2@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"

Loading…
Cancel
Save