1
0
mirror of https://github.com/XFox111/PasswordGeneratorExtension.git synced 2026-04-22 08:08:01 +03:00

Major 4.0 (#380)

- Migrated to WXT
- Migrated to NPM
- Added Insert & copy action
- Added ESLint
This commit is contained in:
Eugene Fox
2024-09-25 16:19:12 +03:00
committed by GitHub
parent f2683e37b2
commit 3ecb6c4a31
71 changed files with 14338 additions and 7531 deletions
+24
View File
@@ -0,0 +1,24 @@
export default defineBackground(() => main());
async function main(): Promise<void>
{
await browser.contextMenus.removeAll();
browser.contextMenus.onClicked.addListener(() => browser.action.openPopup());
const showMenu: boolean = (await storage.getItem<boolean>("sync:ContextMenu", { fallback: true }))!;
updateMenus(showMenu);
storage.watch<boolean>("sync:ContextMenu", e => updateMenus(e!));
}
async function updateMenus(showMenus: boolean): Promise<void>
{
await browser.contextMenus.removeAll();
if (showMenus)
browser.contextMenus.create({
id: "password-generator",
title: i18n.t("manifest.name"),
contexts: ["all"],
});
}
+22
View File
@@ -0,0 +1,22 @@
export default defineContentScript({
matches: ["<all_urls>"],
runAt: "document_idle",
main()
{
console.log("Password Generator: script loaded");
browser.runtime.onMessage.addListener((message: string, _, sendResponse) =>
{
if (message === "probe")
// @ts-expect-error sendResponse has incorrect signature
sendResponse(document.querySelectorAll("form input[type=password]").length);
else
document
.querySelectorAll("form input[type=password]")
.forEach(el => {
(el as HTMLInputElement).value = message;
(el as HTMLInputElement).focus();
});
});
},
});
+30
View File
@@ -0,0 +1,30 @@
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
export const useStyles = makeStyles({
root:
{
display: "flex",
flexDirection: "column",
...shorthands.padding(tokens.spacingVerticalM, tokens.spacingHorizontalS),
},
spinner:
{
alignSelf: "center",
...shorthands.margin(tokens.spacingVerticalXXXL, 0),
},
accordionAnimation:
{
"> .fui-AccordionItem .fui-AccordionHeader__expandIcon > svg":
{
transitionProperty: "transform",
transitionDuration: tokens.durationNormal,
transitionTimingFunction: tokens.curveDecelerateMax,
},
"> .fui-AccordionItem > .fui-AccordionPanel":
{
animationName: "fadeIn",
animationDuration: tokens.durationSlow,
animationTimingFunction: tokens.curveDecelerateMin,
}
},
});
+38
View File
@@ -0,0 +1,38 @@
import { StorageProvider } from "@/utils/storage";
import { useTheme } from "@/utils/useTheme";
import { Accordion, FluentProvider, Spinner } from "@fluentui/react-components";
import { useState } from "react";
import { useStyles } from "./App.styles";
import AboutSection from "./sections/AboutSection";
import GeneratorView from "./sections/GeneratorView";
import SettingsSection from "./sections/SettingsSection";
import Snow from "./specials/Snow";
const App: React.FC = () =>
{
const theme = useTheme();
const cls = useStyles();
const [selection, setSelection] = useState<string[]>([]);
return (
<FluentProvider theme={ theme }>
<main className={ cls.root }>
<StorageProvider loader={ <Spinner size="large" className={ cls.spinner } /> }>
<GeneratorView collapse={ selection.includes("settings") } />
<Accordion
openItems={ selection }
onToggle={ (_, e) => setSelection(e.openItems as string[]) }
collapsible>
<SettingsSection />
<AboutSection />
</Accordion>
</StorageProvider>
<Snow />
</main>
</FluentProvider>
);
};
export default App;
+16
View File
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Password generator</title>
<meta name="manifest.type" content="browser_action" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
+10
View File
@@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./style.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
@@ -0,0 +1,16 @@
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
export const useStyles = makeStyles({
root:
{
display: "flex",
flexDirection: "column",
...shorthands.gap(tokens.spacingVerticalM),
paddingBottom: tokens.spacingVerticalS,
},
horizontalContainer:
{
display: "flex",
...shorthands.gap(tokens.spacingHorizontalSNudge),
},
});
@@ -0,0 +1,66 @@
import { bmcDarkTheme, bmcLightTheme } from "@/utils/BmcTheme";
import { getFeedbackLink, githubLinks, personalLinks } from "@/utils/links";
import { useTheme } from "@/utils/useTheme";
import * as fui from "@fluentui/react-components";
import { InfoRegular, PersonFeedbackRegular } from "@fluentui/react-icons";
import { useStyles } from "./AboutSection.styles";
import { ReactNode } from "react";
const link = (text: string, href: string): ReactNode => (
<fui.Link target="_blank" href={ href }>{ text }</fui.Link>
);
const buttonProps = (href: string, icon: JSX.Element): fui.ButtonProps => (
{
as: "a", target: "_blank", href,
appearance: "primary", icon
}
);
const AboutSection: React.FC = () =>
{
const bmcTheme = useTheme(bmcLightTheme, bmcDarkTheme);
const cls = useStyles();
return (
<fui.AccordionItem value="about">
<fui.AccordionHeader as="h2" icon={ <InfoRegular /> }>{ i18n.t("about.title") }</fui.AccordionHeader>
<fui.AccordionPanel className={ cls.root }>
<header className={ cls.horizontalContainer }>
<fui.Subtitle1 as="h1">{ i18n.t("manifest.name") }</fui.Subtitle1>
<fui.Caption1 as="span">v{ browser.runtime.getManifest().version }</fui.Caption1>
</header>
<fui.Text as="p">
{ i18n.t("about.developed_by") } ({ link("@xfox111", personalLinks.twitter) })
<br />
{ i18n.t("about.lincensed_under") } { link(i18n.t("about.mit_license"), githubLinks.license) }
</fui.Text>
<fui.Text as="p">
{ i18n.t("about.translation_cta.text") }<br />
{ link(i18n.t("about.translation_cta.button"), githubLinks.translationGuide) }
</fui.Text>
<fui.Text as="p">
{ link(i18n.t("about.links.website"), personalLinks.website) } <br />
{ link(i18n.t("about.links.source"), githubLinks.repository) } <br />
{ link(i18n.t("about.links.changelog"), githubLinks.changelog) }
</fui.Text>
<div className={ cls.horizontalContainer }>
<fui.Button { ...buttonProps(getFeedbackLink(), <PersonFeedbackRegular />) }>
{ i18n.t("about.cta.feedback") }
</fui.Button>
<fui.FluentProvider theme={ bmcTheme }>
<fui.Button { ...buttonProps(personalLinks.donation, <img style={ { height: 20 } } src="bmc.svg" />) }>
{ i18n.t("about.cta.sponsor") }
</fui.Button>
</fui.FluentProvider>
</div>
</fui.AccordionPanel>
</fui.AccordionItem>
);
};
export default AboutSection;
@@ -0,0 +1,29 @@
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
export const useStyles = makeStyles({
root:
{
display: "flex",
flexDirection: "column",
},
input:
{
fontFamily: tokens.fontFamilyMonospace,
},
copyIcon:
{
animationName: "scaleUpIn",
animationDuration: tokens.durationSlow,
animationTimingFunction: tokens.curveEasyEaseMax,
},
refreshIcon:
{
animationName: "spin",
animationDuration: tokens.durationSlow,
animationTimingFunction: tokens.curveEasyEaseMax,
},
msgBar:
{
...shorthands.padding(tokens.spacingVerticalMNudge, tokens.spacingHorizontalM),
},
});
@@ -0,0 +1,112 @@
import { GeneratePassword } from "@/utils/PasswordGenerator";
import { GeneratorOptions, useStorage } from "@/utils/storage";
import useTimeout from "@/utils/useTimeout";
import * as fui from "@fluentui/react-components";
import * as ic from "@fluentui/react-icons";
import { useEffect, useState } from "react";
import { useStyles } from "./GeneratorView.styles";
import QuickOptions from "./QuickOptions";
const GeneratorView: React.FC<{ collapse: boolean }> = props =>
{
const { generatorOptions } = useStorage();
const [options, setOptions] = useState<GeneratorOptions>(generatorOptions);
const [showInsert, setShowInsert] = useState<boolean>(false);
const [password, setPassword] = useState<string>("");
const [error, setError] = useState<string | null>(null);
const [refreshTimer, copyTimer, insertTimer] = [useTimeout(310), useTimeout(1000), useTimeout(1000)];
const cls = useStyles();
const refresh = useCallback(() =>
{
setError(null);
try { setPassword(GeneratePassword(options)); }
catch (e) { setError((e as Error).message); }
}, [options]);
const copy = useCallback(async () =>
{
await window.navigator.clipboard.writeText(password);
copyTimer.trigger();
}, [password]);
const insert = useCallback(async () =>
{
const tabId: number = (await browser.tabs.query({ active: true, currentWindow: true }))[0].id!;
await browser.tabs.sendMessage(tabId, password);
insertTimer.trigger();
copy();
}, [password]);
useEffect(() => setOptions(generatorOptions), [generatorOptions]);
useEffect(refresh, [options]);
useEffect(refreshTimer.trigger, [password]);
useEffect(() =>
{
(async () =>
{
try
{
const tabId: number = (await browser.tabs.query({ active: true, currentWindow: true }))[0].id!;
const fieldCount: number = await browser.tabs.sendMessage(tabId, "probe");
if (fieldCount > 0)
setShowInsert(true);
}
catch { }
})();
}, []);
return (
<section className={ cls.root }>
{ error ?
<fui.MessageBar intent="warning" className={ cls.msgBar }>
<fui.MessageBarBody>{ error }</fui.MessageBarBody>
</fui.MessageBar>
:
<fui.Input
className={ cls.input }
readOnly value={ password }
contentAfter={ <>
<fui.Tooltip content={ i18n.t("common.copy") } relationship="label">
<fui.Button
appearance="subtle" onClick={ copy }
icon={
copyTimer.isActive ?
<ic.CheckmarkRegular className={ cls.copyIcon } /> :
<ic.CopyRegular className={ cls.copyIcon } />
} />
</fui.Tooltip>
<fui.Tooltip content={ i18n.t("generator.refresh") } relationship="label">
<fui.Button
appearance="subtle" onClick={ refresh }
icon={
<ic.ArrowClockwiseRegular className={ fui.mergeClasses(refreshTimer.isActive && cls.refreshIcon) } />
} />
</fui.Tooltip>
{ showInsert &&
<fui.Tooltip content={ i18n.t("generator.insert") } relationship="label">
<fui.Button
appearance="subtle" onClick={ insert }
icon={
insertTimer.isActive ?
<ic.CheckmarkRegular className={ cls.copyIcon } /> :
<ic.ArrowRightRegular className={ cls.copyIcon } />
} />
</fui.Tooltip>
}
</> } />
}
{ !props.collapse &&
<QuickOptions onChange={ e => setOptions(e) } />
}
</section>
);
};
export default GeneratorView;
@@ -0,0 +1,22 @@
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
export const useStyles = makeStyles({
characterOptionsContainer:
{
display: "flex",
...shorthands.gap(tokens.spacingHorizontalXS),
},
options:
{
display: "flex",
flexDirection: "column",
...shorthands.padding(tokens.spacingVerticalS, tokens.spacingHorizontalS),
},
lengthContainer:
{
display: "grid",
gridTemplateColumns: "1fr auto",
alignItems: "center",
paddingRight: tokens.spacingHorizontalM,
},
});
+121
View File
@@ -0,0 +1,121 @@
import { GeneratorOptions, useStorage } from "@/utils/storage";
import * as fui from "@fluentui/react-components";
import * as ic from "@fluentui/react-icons";
import React from "react";
import { useStyles } from "./QuickOptions.styles";
const QuickOptions: React.FC<IProps> = ({ onChange }) =>
{
const { extOptions, generatorOptions } = useStorage();
const [quickOpts, setOptions] = useState<GeneratorOptions>(generatorOptions);
const checkedOptions = useMemo(
() => Object.keys(quickOpts).filter(k => quickOpts[k as keyof GeneratorOptions] as boolean),
[quickOpts]
);
const onCheckedValueChange = useCallback((_: unknown, e: fui.MenuCheckedValueChangeData): void =>
{
const opts: Partial<Omit<GeneratorOptions, "Length">> = {};
let keys = Object.keys(quickOpts).filter(i => i !== "Length") as (keyof Omit<GeneratorOptions, "Length">)[];
if (e.name === "include")
keys = keys.filter(i => !i.startsWith("Exclude"));
else
keys = keys.filter(i => i.startsWith("Exclude"));
for (const key of keys)
opts[key] = e.checkedItems.includes(key);
setOptions({ ...generatorOptions, ...quickOpts, ...opts });
}, [quickOpts]);
useEffect(() => onChange(quickOpts), [onChange, quickOpts]);
const IncludeIcon: ic.FluentIcon = ic.bundleIcon(ic.AddCircleFilled, ic.AddCircleRegular);
const ExcludeIcon: ic.FluentIcon = ic.bundleIcon(ic.SubtractCircleFilled, ic.SubtractCircleRegular);
const cls = useStyles();
return (
<div className={ cls.options }>
<fui.InfoLabel info={ i18n.t("generator.options.hint") }>
{ i18n.t("generator.options.title") }
</fui.InfoLabel>
<div className={ cls.lengthContainer }>
<fui.Slider
min={ extOptions.MinLength } max={ Math.max(extOptions.MaxLength, generatorOptions.Length) }
value={ quickOpts.Length } onChange={ (_, e) => setOptions({ ...quickOpts, Length: e.value }) } />
<fui.Text>{ quickOpts.Length }</fui.Text>
</div>
<div className={ cls.characterOptionsContainer }>
<fui.Menu
positioning="after" hasCheckmarks
checkedValues={ { include: checkedOptions } }
onCheckedValueChange={ onCheckedValueChange }>
<fui.MenuTrigger disableButtonEnhancement>
<fui.MenuButton appearance="subtle" icon={ <IncludeIcon /> }>
{ i18n.t("generator.options.include") }
</fui.MenuButton>
</fui.MenuTrigger>
<fui.MenuPopover>
<fui.MenuList>
<fui.MenuItemCheckbox name="include" value="Uppercase" icon={ <ic.TextCaseUppercaseRegular /> }>
{ i18n.t("settings.include.uppercase") }
</fui.MenuItemCheckbox>
<fui.MenuItemCheckbox name="include" value="Lowercase" icon={ <ic.TextCaseLowercaseRegular /> }>
{ i18n.t("settings.include.lowercase") }
</fui.MenuItemCheckbox>
<fui.MenuItemCheckbox name="include" value="Numeric" icon={ <ic.NumberSymbolRegular /> }>
{ i18n.t("settings.include.numeric") }
</fui.MenuItemCheckbox>
<fui.MenuItemCheckbox name="include" value="Special" icon={ <ic.MathSymbolsRegular /> }>
{ i18n.t("settings.include.special") }
</fui.MenuItemCheckbox>
</fui.MenuList>
</fui.MenuPopover>
</fui.Menu>
<fui.Menu
positioning="before"
checkedValues={ { exclude: checkedOptions } }
onCheckedValueChange={ onCheckedValueChange }>
<fui.MenuTrigger disableButtonEnhancement>
<fui.MenuButton appearance="subtle" icon={ <ExcludeIcon /> }>
{ i18n.t("generator.options.exclude") }
</fui.MenuButton>
</fui.MenuTrigger>
<fui.MenuPopover>
<fui.MenuList>
<fui.MenuItemCheckbox name="exclude" value="ExcludeSimilar">
{ i18n.t("settings.exclude.similar") }
</fui.MenuItemCheckbox>
<fui.MenuItemCheckbox name="exclude" value="ExcludeAmbiguous" disabled={ !quickOpts.Special }>
{ i18n.t("settings.exclude.ambiguous") }
</fui.MenuItemCheckbox>
<fui.MenuItemCheckbox name="exclude" value="ExcludeRepeating">
{ i18n.t("settings.exclude.repeating.title") }
</fui.MenuItemCheckbox>
</fui.MenuList>
</fui.MenuPopover>
</fui.Menu>
<fui.Tooltip content={ i18n.t("common.reset") } relationship="label">
<fui.Button appearance="subtle" icon={ <ic.ArrowUndoRegular /> } onClick={ () => setOptions(generatorOptions) } />
</fui.Tooltip>
</div>
</div>
);
};
export default QuickOptions;
interface IProps
{
onChange: (value: GeneratorOptions) => void;
}
@@ -0,0 +1,26 @@
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
export const useStyles = makeStyles({
root:
{
display: "flex",
flexDirection: "column",
...shorthands.gap(tokens.spacingVerticalS),
},
checkboxContainer:
{
display: "flex",
flexWrap: "wrap",
},
rangeContainer:
{
display: "grid",
gridTemplateColumns: "1fr auto 1fr auto",
alignItems: "center",
...shorthands.gap(tokens.spacingHorizontalS),
},
rangeInput:
{
width: "100%",
},
});
@@ -0,0 +1,138 @@
import { CharacterHints } from "@/utils/PasswordGenerator";
import { ExtensionOptions, GeneratorOptions, useStorage } from "@/utils/storage";
import * as fui from "@fluentui/react-components";
import { ArrowUndoRegular, SettingsRegular } from "@fluentui/react-icons";
import { useStyles } from "./SettingsSection.styles";
// FIXME: Remove ts-ignore comments once slots override fix is released
// Tracker: https://github.com/microsoft/fluentui/issues/27090
const infoLabel = (content: string, hint: string) => ({
children: (_: unknown, slotProps: fui.LabelProps) => (
<fui.InfoLabel { ...slotProps } info={ hint }>{ content }</fui.InfoLabel>
)
});
const defaultOptions =
{
generator: new GeneratorOptions(),
extension: new ExtensionOptions()
};
const SettingsSection: React.FC = () =>
{
const { extOptions, generatorOptions, updateStorage } = useStorage();
const cls = useStyles();
const resetRange = useCallback(() =>
{
updateStorage({
MinLength: defaultOptions.extension.MinLength,
MaxLength: defaultOptions.extension.MaxLength
});
}, []);
const setOption = (option: keyof (GeneratorOptions & ExtensionOptions)) =>
(_: unknown, args: fui.CheckboxOnChangeData) =>
updateStorage({ [option]: args.checked });
const updateNumberField = (key: keyof (ExtensionOptions & GeneratorOptions), defaultValue: number) =>
(_: unknown, e: fui.InputOnChangeData): void =>
{
if (e.value.length >= 1)
{
const value = parseInt(e.value);
if (!isNaN(value) && value >= 0)
updateStorage({ [key]: value });
}
else
updateStorage({ [key]: defaultValue });
};
return (
<fui.AccordionItem value="settings">
<fui.AccordionHeader as="h2" icon={ <SettingsRegular /> }>{ i18n.t("settings.title") }</fui.AccordionHeader>
<fui.AccordionPanel className={ cls.root }>
<fui.Field label={ i18n.t("settings.length.title") } hint={ i18n.t("settings.length.hint") }>
<fui.Input
value={ generatorOptions.Length.toString() }
onChange={ updateNumberField("Length", 0) } />
</fui.Field>
<fui.Field label={ i18n.t("settings.quick_range") }>
<div className={ cls.rangeContainer }>
<fui.Input
input={ { className: cls.rangeInput } }
value={ extOptions.MinLength.toString() }
onChange={ updateNumberField("MinLength", defaultOptions.extension.MinLength) } />
<fui.Divider />
<fui.Input
input={ { className: cls.rangeInput } }
value={ extOptions.MaxLength.toString() }
onChange={ updateNumberField("MaxLength", defaultOptions.extension.MaxLength) } />
<fui.Tooltip relationship="label" content={ i18n.t("common.reset") }>
<fui.Button
appearance="subtle" icon={ <ArrowUndoRegular /> }
onClick={ resetRange } />
</fui.Tooltip>
</div>
</fui.Field>
<fui.Divider />
<fui.Text>{ i18n.t("settings.include.title") }</fui.Text>
<div className={ cls.checkboxContainer }>
<fui.Checkbox label={ i18n.t("settings.include.uppercase") }
checked={ generatorOptions.Uppercase }
onChange={ setOption("Uppercase") } />
<fui.Checkbox
label={ i18n.t("settings.include.lowercase") }
checked={ generatorOptions.Lowercase }
onChange={ setOption("Lowercase") } />
<fui.Checkbox
label={ i18n.t("settings.include.numeric") }
checked={ generatorOptions.Numeric }
onChange={ setOption("Numeric") } />
<fui.Checkbox
label={ i18n.t("settings.include.special") }
checked={ generatorOptions.Special }
onChange={ setOption("Special") } />
</div>
<fui.Text>{ i18n.t("settings.exclude.title") }</fui.Text>
<div className={ cls.checkboxContainer }>
<fui.Checkbox
// @ts-expect-error See FIXME
label={ infoLabel(i18n.t("settings.exclude.similar"), CharacterHints.Similar) }
checked={ generatorOptions.ExcludeSimilar }
onChange={ setOption("ExcludeSimilar") } />
<fui.Checkbox
// @ts-expect-error See FIXME
label={ infoLabel(i18n.t("settings.exclude.ambiguous"), CharacterHints.Ambiguous) }
disabled={ !generatorOptions.Special }
checked={ generatorOptions.ExcludeAmbiguous }
onChange={ setOption("ExcludeAmbiguous") } />
<fui.Checkbox
// @ts-expect-error See FIXME
label={ infoLabel(i18n.t("settings.exclude.repeating.title"), i18n.t("settings.exclude.repeating.hint")) }
checked={ generatorOptions.ExcludeRepeating }
onChange={ setOption("ExcludeRepeating") } />
</div>
<fui.Checkbox
label={ i18n.t("settings.context_menu") }
checked={ extOptions.ContextMenu }
onChange={ setOption("ContextMenu") } />
</fui.AccordionPanel>
</fui.AccordionItem>
);
};
export default SettingsSection;
+46
View File
@@ -0,0 +1,46 @@
import { GriffelStyle, makeStyles, tokens } from "@fluentui/react-components";
const random = (max: number): number => Math.floor(Math.random() * max);
export const SNOWFLAKES_NUM: number = 100;
export const useStyles = makeStyles({
snow:
{
position: "absolute",
overflow: "hidden",
pointerEvents: "none",
top: 0,
left: 0,
bottom: 0,
right: 0,
},
snowflake:
{
"--size": "1px",
width: "var(--size)",
height: "var(--size)",
backgroundColor: tokens.colorScrollbarOverlay,
borderRadius: tokens.borderRadiusCircular,
position: "absolute",
top: "-5px",
},
...[...Array(SNOWFLAKES_NUM)].reduce(
(acc, _, i): Record<string, GriffelStyle> => ({
...acc,
[`snowflake-${i}`]: {
"--size": `${random(5)}px`,
"--left-start": `${random(20) - 10}vw`,
"--left-end": `${random(20) - 10}vw`,
left: `${random(100)}vw`,
animationName: "snowfall",
animationDuration: `${5 + random(10)}s`,
animationTimingFunction: "linear",
animationIterationCount: "infinite",
animationDelay: `-${random(10)}s`,
},
}),
{},
),
});
+20
View File
@@ -0,0 +1,20 @@
import { mergeClasses } from "@fluentui/react-components";
import { SNOWFLAKES_NUM, useStyles } from "./Snow.styles";
const Snow: React.FC = () =>
{
const cls = useStyles();
if (![0, 11].includes(new Date().getMonth()))
return null;
return (
<div className={ cls.snow }>
{ [...Array(SNOWFLAKES_NUM)].map((_, i) =>
<div key={ i } className={ mergeClasses(cls.snowflake, cls[`snowflake-${i}`]) } />
) }
</div>
);
};
export default Snow;
+77
View File
@@ -0,0 +1,77 @@
html,
body
{
width: 400px;
min-width: 400px;
padding: 0;
margin: 0;
max-height: 600px;
overflow-y: hidden;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
}
h1, h2, h3, h4, h5, h6,
p, ul, ol, li
{
margin: 0;
}
@keyframes scaleUpIn
{
from
{
transform: scale(0.5);
filter: opacity(0);
}
to
{
transform: scale(1);
filter: opacity(1)
}
}
@keyframes spin
{
from
{
transform: rotate(0deg);
}
to
{
transform: rotate(360deg);
}
}
@keyframes fadeIn
{
from
{
filter: opacity(0);
}
to
{
filter: opacity(1);
}
}
@keyframes snowfall
{
0%
{
transform: translate3d(var(--left-start), 0, 0);
filter: opacity(.6);
}
100%
{
transform: translate3d(var(--left-end), 610px, 0);
filter: opacity(0);
}
}