mirror of
https://github.com/XFox111/PasswordGeneratorExtension.git
synced 2026-04-22 08:08:01 +03:00
Major 5.0 (#469)
* Advanced generator, UI overhaul (#449) * Major overhaul: - Added advanced generator - Removed "Insert and copy" feature - Moved settings to extension options - General refactoring * Updated custom character options for default generator (#447) * Added state save for advanced generator mode * Fixed state save for advanced password generator * Updated extension description * Minor UI fixes: - Fixed Options UI not displaying in Google Chrome - Fixed Quick Options menus overflowing on some locales - Fixed Advanced generator configuration UI clipping when window height is too small - Fixed divider in Options UI taking up all available space * Minor UI/UX changes and fixes: - Fixed locale in Advanced generator toast notifications - Added toast notification for copying a single password in Advanced generator - Moved custom characters input lables to placeholders - Fixed minor type issues - Removed duplicate "About" text - Fixed input fields alignment in Options - Added "disabled" state for "Include Custom" option in Quick settings * Bump @typescript-eslint/parser from 8.16.0 to 8.19.1 (#468) Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.16.0 to 8.19.1. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.19.1/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/parser" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump @eslint/js from 9.16.0 to 9.18.0 (#467) Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.16.0 to 9.18.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/commits/v9.18.0/packages/js) --- updated-dependencies: - dependency-name: "@eslint/js" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump @fluentui/react-components from 9.56.3 to 9.57.0 (#466) Bumps [@fluentui/react-components](https://github.com/microsoft/fluentui) from 9.56.3 to 9.57.0. - [Release notes](https://github.com/microsoft/fluentui/releases) - [Changelog](https://github.com/microsoft/fluentui/blob/master/azure-pipelines.release.yml) - [Commits](https://github.com/microsoft/fluentui/compare/@fluentui/react-components_v9.56.3...@fluentui/react-components_v9.57.0) --- updated-dependencies: - dependency-name: "@fluentui/react-components" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump wxt from 0.19.17 to 0.19.24 (#465) Bumps [wxt](https://github.com/wxt-dev/wxt) from 0.19.17 to 0.19.24. - [Release notes](https://github.com/wxt-dev/wxt/releases) - [Commits](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.17...wxt-v0.19.24) --- updated-dependencies: - dependency-name: wxt dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump eslint from 9.16.0 to 9.18.0 (#464) Bumps [eslint](https://github.com/eslint/eslint) from 9.16.0 to 9.18.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.16.0...v9.18.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump eslint-plugin-react from 7.37.2 to 7.37.4 (#463) Bumps [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) from 7.37.2 to 7.37.4. - [Release notes](https://github.com/jsx-eslint/eslint-plugin-react/releases) - [Changelog](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/CHANGELOG.md) - [Commits](https://github.com/jsx-eslint/eslint-plugin-react/compare/v7.37.2...v7.37.4) --- updated-dependencies: - dependency-name: eslint-plugin-react dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eugene Fox <eugene.xfox@outlook.com> * Bump typescript from 5.7.2 to 5.7.3 (#462) Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.7.2 to 5.7.3. - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml) - [Commits](https://github.com/microsoft/TypeScript/compare/v5.7.2...v5.7.3) --- updated-dependencies: - dependency-name: typescript dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eugene Fox <eugene.xfox@outlook.com> * Bump @typescript-eslint/eslint-plugin from 8.16.0 to 8.19.1 (#461) Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 8.16.0 to 8.19.1. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.19.1/packages/eslint-plugin) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump wdzeng/chrome-extension from 1.2.4 to 1.3.0 (#450) * Bump wdzeng/chrome-extension from 1.2.4 to 1.3.0 Bumps [wdzeng/chrome-extension](https://github.com/wdzeng/chrome-extension) from 1.2.4 to 1.3.0. - [Release notes](https://github.com/wdzeng/chrome-extension/releases) - [Commits](https://github.com/wdzeng/chrome-extension/compare/v1.2.4...v1.3.0) --- updated-dependencies: - dependency-name: wdzeng/chrome-extension dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * Optimized CD workflow --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eugene Fox <eugene@xfox111.net> * Bump @fluentui/react-icons from 2.0.266 to 2.0.270 (#458) Bumps [@fluentui/react-icons](https://github.com/microsoft/fluentui-system-icons) from 2.0.266 to 2.0.270. - [Changelog](https://github.com/microsoft/fluentui-system-icons/blob/main/fluentui-android-system-icons-release.yml) - [Commits](https://github.com/microsoft/fluentui-system-icons/commits) --- updated-dependencies: - dependency-name: "@fluentui/react-icons" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump @wxt-dev/module-react from 1.1.2 to 1.1.3 (#455) Bumps [@wxt-dev/module-react](https://github.com/wxt-dev/wxt/tree/HEAD/packages/module-react) from 1.1.2 to 1.1.3. - [Release notes](https://github.com/wxt-dev/wxt/releases) - [Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/module-react/CHANGELOG.md) - [Commits](https://github.com/wxt-dev/wxt/commits/module-react-v1.1.3/packages/module-react) --- updated-dependencies: - dependency-name: "@wxt-dev/module-react" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump globals from 15.12.0 to 15.14.0 (#452) Bumps [globals](https://github.com/sindresorhus/globals) from 15.12.0 to 15.14.0. - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v15.12.0...v15.14.0) --- updated-dependencies: - dependency-name: globals dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,30 +0,0 @@
|
||||
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,
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
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;
|
||||
+6
-7
@@ -1,11 +1,6 @@
|
||||
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
input:
|
||||
{
|
||||
fontFamily: tokens.fontFamilyMonospace,
|
||||
@@ -24,6 +19,10 @@ export const useStyles = makeStyles({
|
||||
},
|
||||
msgBar:
|
||||
{
|
||||
...shorthands.padding(tokens.spacingVerticalMNudge, tokens.spacingHorizontalM),
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
|
||||
},
|
||||
msgBarBody:
|
||||
{
|
||||
whiteSpace: "break-spaces",
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { generatePassword } from "@/utils/generators/generatePassword";
|
||||
import { GeneratorOptions } from "@/utils/storage";
|
||||
import useTimeout from "@/utils/useTimeout";
|
||||
import { Button, Input, mergeClasses, MessageBar, MessageBarBody, Tooltip } from "@fluentui/react-components";
|
||||
import { ArrowClockwise20Regular, CheckmarkRegular, Copy20Regular } from "@fluentui/react-icons";
|
||||
import { ReactElement, useEffect, useState } from "react";
|
||||
import { useStyles } from "./GeneratorView.styles";
|
||||
|
||||
export default function GeneratorView({ options }: GeneratorViewProps): ReactElement
|
||||
{
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [refreshTimer, copyTimer] = [useTimeout(310), useTimeout(1000)];
|
||||
const cls = useStyles();
|
||||
|
||||
const refresh = useCallback(() =>
|
||||
{
|
||||
if (!options)
|
||||
return;
|
||||
|
||||
setError(null);
|
||||
try
|
||||
{
|
||||
setPassword(generatePassword({
|
||||
length: options.Length,
|
||||
uppercase: options.Uppercase,
|
||||
lowercase: options.Lowercase,
|
||||
numeric: options.Numeric,
|
||||
special: options.Special,
|
||||
custom: options.Custom,
|
||||
excludeSimilar: options.ExcludeSimilar,
|
||||
excludeAmbiguous: options.ExcludeAmbiguous,
|
||||
excludeRepeating: options.ExcludeRepeating,
|
||||
excludeCustom: options.ExcludeCustom ? options.ExcludeCustomSet : "",
|
||||
customSet: options.IncludeCustomSet
|
||||
}));
|
||||
}
|
||||
catch (e) { setError((e as Error).message); }
|
||||
|
||||
refreshTimer.trigger();
|
||||
}, [options]);
|
||||
|
||||
const copy = useCallback(async () =>
|
||||
{
|
||||
await window.navigator.clipboard.writeText(password);
|
||||
copyTimer.trigger();
|
||||
}, [password]);
|
||||
|
||||
useEffect(refresh, [options]);
|
||||
|
||||
if (error)
|
||||
return (
|
||||
<MessageBar intent="warning" className={ cls.msgBar }>
|
||||
<MessageBarBody className={ cls.msgBarBody }>{ error }</MessageBarBody>
|
||||
</MessageBar>
|
||||
);
|
||||
|
||||
return (
|
||||
<Input
|
||||
className={ cls.input }
|
||||
readOnly value={ password }
|
||||
contentAfter={ <>
|
||||
<Tooltip content={ i18n.t("common.actions.copy") } relationship="label">
|
||||
<Button
|
||||
appearance="subtle" onClick={ copy }
|
||||
icon={
|
||||
copyTimer.isActive ?
|
||||
<CheckmarkRegular className={ cls.copyIcon } /> :
|
||||
<Copy20Regular className={ cls.copyIcon } />
|
||||
} />
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={ i18n.t("popup.refresh") } relationship="label">
|
||||
<Button
|
||||
appearance="subtle" onClick={ refresh }
|
||||
icon={
|
||||
<ArrowClockwise20Regular className={ mergeClasses(refreshTimer.isActive && cls.refreshIcon) } />
|
||||
} />
|
||||
</Tooltip>
|
||||
</> } />
|
||||
);
|
||||
};
|
||||
|
||||
export type GeneratorViewProps =
|
||||
{
|
||||
options: GeneratorOptions | null;
|
||||
};
|
||||
+3
-3
@@ -1,16 +1,16 @@
|
||||
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
characterOptionsContainer:
|
||||
{
|
||||
display: "flex",
|
||||
...shorthands.gap(tokens.spacingHorizontalXS),
|
||||
gap: tokens.spacingHorizontalXS,
|
||||
},
|
||||
options:
|
||||
{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
...shorthands.padding(tokens.spacingVerticalS, tokens.spacingHorizontalS),
|
||||
gap: tokens.spacingVerticalS,
|
||||
},
|
||||
lengthContainer:
|
||||
{
|
||||
@@ -1,10 +1,10 @@
|
||||
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 { ReactElement } from "react";
|
||||
import { useStyles } from "./QuickOptions.styles";
|
||||
|
||||
const QuickOptions: React.FC<IProps> = ({ onChange }) =>
|
||||
export default function QuickOptions({ onChange }: QuickOptionsProps): ReactElement
|
||||
{
|
||||
const { extOptions, generatorOptions } = useStorage();
|
||||
const [quickOpts, setOptions] = useState<GeneratorOptions>(generatorOptions);
|
||||
@@ -15,14 +15,13 @@ const QuickOptions: React.FC<IProps> = ({ onChange }) =>
|
||||
|
||||
const onCheckedValueChange = useCallback((_: unknown, e: fui.MenuCheckedValueChangeData): void =>
|
||||
{
|
||||
const opts: Partial<Omit<GeneratorOptions, "Length">> = {};
|
||||
const opts: Partial<Omit<GeneratorOptions, "Length" | "IncludeCustomSet" | "ExcludeCustomSet">> = {};
|
||||
|
||||
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"));
|
||||
const keys = Object.keys(quickOpts)
|
||||
.filter(i =>
|
||||
i !== "Length" && i !== "IncludeCustomSet" && i !== "ExcludeCustomSet" &&
|
||||
i.startsWith("Exclude") === (e.name === "exclude")
|
||||
) as (keyof Omit<GeneratorOptions, "Length" | "IncludeCustomSet" | "ExcludeCustomSet">)[];
|
||||
|
||||
for (const key of keys)
|
||||
opts[key] = e.checkedItems.includes(key);
|
||||
@@ -38,9 +37,6 @@ const QuickOptions: React.FC<IProps> = ({ onChange }) =>
|
||||
|
||||
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
|
||||
@@ -51,61 +47,79 @@ const QuickOptions: React.FC<IProps> = ({ onChange }) =>
|
||||
|
||||
<div className={ cls.characterOptionsContainer }>
|
||||
<fui.Menu
|
||||
positioning="after" hasCheckmarks
|
||||
positioning={ { position: "after", align: "center", offset: -48 } }
|
||||
checkedValues={ { include: checkedOptions } }
|
||||
onCheckedValueChange={ onCheckedValueChange }>
|
||||
|
||||
<fui.MenuTrigger disableButtonEnhancement>
|
||||
<fui.MenuButton appearance="subtle" icon={ <IncludeIcon /> }>
|
||||
{ i18n.t("generator.options.include") }
|
||||
{ i18n.t("popup.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 name="include" value="Uppercase">
|
||||
{ i18n.t("common.characters.uppercase") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="include" value="Lowercase" icon={ <ic.TextCaseLowercaseRegular /> }>
|
||||
{ i18n.t("settings.include.lowercase") }
|
||||
<fui.MenuItemCheckbox name="include" value="Lowercase">
|
||||
{ i18n.t("common.characters.lowercase") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="include" value="Numeric" icon={ <ic.NumberSymbolRegular /> }>
|
||||
{ i18n.t("settings.include.numeric") }
|
||||
<fui.MenuItemCheckbox name="include" value="Numeric">
|
||||
{ i18n.t("common.characters.numeric") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="include" value="Special" icon={ <ic.MathSymbolsRegular /> }>
|
||||
{ i18n.t("settings.include.special") }
|
||||
<fui.MenuItemCheckbox name="include" value="Special">
|
||||
{ i18n.t("common.characters.special") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuSplitGroup>
|
||||
<fui.MenuItemCheckbox name="include" value="Custom"
|
||||
disabled={ generatorOptions.IncludeCustomSet.length < 1 }>
|
||||
|
||||
{ i18n.t("common.characters.custom") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItem icon={ <ic.EditRegular /> }
|
||||
onClick={ () => browser.runtime.openOptionsPage() } />
|
||||
</fui.MenuSplitGroup>
|
||||
</fui.MenuList>
|
||||
</fui.MenuPopover>
|
||||
</fui.Menu>
|
||||
|
||||
<fui.Menu
|
||||
positioning="before"
|
||||
positioning={ { position: "after", align: "center", offset: -64 } }
|
||||
checkedValues={ { exclude: checkedOptions } }
|
||||
onCheckedValueChange={ onCheckedValueChange }>
|
||||
|
||||
<fui.MenuTrigger disableButtonEnhancement>
|
||||
<fui.MenuButton appearance="subtle" icon={ <ExcludeIcon /> }>
|
||||
{ i18n.t("generator.options.exclude") }
|
||||
{ i18n.t("popup.exclude") }
|
||||
</fui.MenuButton>
|
||||
</fui.MenuTrigger>
|
||||
|
||||
<fui.MenuPopover>
|
||||
<fui.MenuList>
|
||||
<fui.MenuItemCheckbox name="exclude" value="ExcludeSimilar">
|
||||
{ i18n.t("settings.exclude.similar") }
|
||||
{ i18n.t("common.characters.similar") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="exclude" value="ExcludeAmbiguous" disabled={ !quickOpts.Special }>
|
||||
{ i18n.t("settings.exclude.ambiguous") }
|
||||
{ i18n.t("common.characters.ambiguous") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="exclude" value="ExcludeRepeating">
|
||||
{ i18n.t("settings.exclude.repeating.title") }
|
||||
{ i18n.t("common.characters.repeating.label") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuSplitGroup>
|
||||
<fui.MenuItemCheckbox name="exclude" value="ExcludeCustom"
|
||||
disabled={ generatorOptions.ExcludeCustomSet.length < 1 }>
|
||||
|
||||
{ i18n.t("common.characters.custom") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItem icon={ <ic.EditRegular /> }
|
||||
onClick={ () => browser.runtime.openOptionsPage() } />
|
||||
</fui.MenuSplitGroup>
|
||||
</fui.MenuList>
|
||||
</fui.MenuPopover>
|
||||
</fui.Menu>
|
||||
|
||||
<fui.Tooltip content={ i18n.t("common.reset") } relationship="label">
|
||||
<fui.Tooltip content={ i18n.t("common.actions.reset") } relationship="label">
|
||||
<fui.Button appearance="subtle" icon={ <ic.ArrowUndoRegular /> } onClick={ () => setOptions(generatorOptions) } />
|
||||
</fui.Tooltip>
|
||||
</div>
|
||||
@@ -113,9 +127,7 @@ const QuickOptions: React.FC<IProps> = ({ onChange }) =>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickOptions;
|
||||
|
||||
interface IProps
|
||||
{
|
||||
onChange: (value: GeneratorOptions) => void;
|
||||
}
|
||||
export type QuickOptionsProps =
|
||||
{
|
||||
onChange: (value: GeneratorOptions) => void;
|
||||
};
|
||||
@@ -14,12 +14,6 @@ body
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
p, ul, ol, li
|
||||
{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@keyframes scaleUpIn
|
||||
{
|
||||
from
|
||||
@@ -60,18 +54,3 @@ p, ul, ol, li
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,62 @@
|
||||
import { GeneratorOptions } from "@/utils/storage";
|
||||
import { Button, Divider, makeStyles, tokens, Tooltip } from "@fluentui/react-components";
|
||||
import { bundleIcon, FluentIcon, Open20Filled, Open20Regular, Settings20Filled, Settings20Regular } from "@fluentui/react-icons";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./style.css";
|
||||
import App from "../../shared/App";
|
||||
import GeneratorView from "./GeneratorView";
|
||||
import "./main.css";
|
||||
import QuickOptions from "./QuickOptions";
|
||||
|
||||
function Popup(): React.ReactElement
|
||||
{
|
||||
const [options, setOptions] = useState<GeneratorOptions | null>(null);
|
||||
|
||||
const AdvancedIcon: FluentIcon = bundleIcon(Open20Filled, Open20Regular);
|
||||
const SettingsIcon: FluentIcon = bundleIcon(Settings20Filled, Settings20Regular);
|
||||
const cls = useStyles();
|
||||
|
||||
return (
|
||||
<main className={ cls.root }>
|
||||
<GeneratorView options={ options } />
|
||||
<QuickOptions onChange={ setOptions } />
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className={ cls.actionsRoot }>
|
||||
<Button as="a" icon={ <AdvancedIcon /> } href="/advanced.html" target="_blank">
|
||||
{ i18n.t("popup.advanced") }
|
||||
</Button>
|
||||
<Tooltip relationship="label" content={ i18n.t("settings.title") }>
|
||||
<Button
|
||||
appearance="subtle" icon={ <SettingsIcon /> }
|
||||
onClick={ () => browser.runtime.openOptionsPage() } />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<App>
|
||||
<Popup />
|
||||
</App>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalS}`,
|
||||
gap: tokens.spacingVerticalMNudge,
|
||||
},
|
||||
actionsRoot:
|
||||
{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
gap: tokens.spacingHorizontalS,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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),
|
||||
},
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
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;
|
||||
@@ -1,112 +0,0 @@
|
||||
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;
|
||||
@@ -1,26 +0,0 @@
|
||||
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%",
|
||||
},
|
||||
});
|
||||
@@ -1,138 +0,0 @@
|
||||
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;
|
||||
@@ -1,46 +0,0 @@
|
||||
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`,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user