From 8126886fb5543f7e797c1c74c6872573c0823a07 Mon Sep 17 00:00:00 2001 From: Eugene Fox Date: Mon, 13 Jan 2025 14:08:31 +0300 Subject: [PATCH] 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] 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] 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] 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] 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] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eugene Fox * 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eugene Fox * 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] 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] * Optimized CD workflow --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eugene Fox * 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] 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] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cd_pipeline.yaml | 10 +- LICENSE | 2 +- README.md | 9 +- entrypoints/advanced/Page.styles.ts | 59 + entrypoints/advanced/Page.tsx | 58 + .../components/GeneratorForm.styles.ts | 22 + .../advanced/components/GeneratorForm.tsx | 91 + .../components/PasswordList.styles.ts | 50 + .../advanced/components/PasswordList.tsx | 79 + entrypoints/advanced/index.html | 15 + entrypoints/advanced/main.css | 51 + entrypoints/advanced/main.tsx | 15 + .../sections/PassphraseSection.styles.ts | 16 + .../advanced/sections/PassphraseSection.tsx | 89 + .../advanced/sections/PasswordSection.tsx | 188 + entrypoints/background.ts | 24 - entrypoints/content.ts | 22 - entrypoints/options/AboutSection.styles.ts | 16 + entrypoints/options/AboutSection.tsx | 61 + entrypoints/options/SettingsSection.styles.ts | 38 + entrypoints/options/SettingsSection.tsx | 128 + entrypoints/options/index.html | 15 + entrypoints/options/main.css | 65 + entrypoints/options/main.tsx | 35 + entrypoints/popup/App.styles.ts | 30 - entrypoints/popup/App.tsx | 38 - .../{sections => }/GeneratorView.styles.ts | 13 +- entrypoints/popup/GeneratorView.tsx | 88 + .../{sections => }/QuickOptions.styles.ts | 6 +- .../popup/{sections => }/QuickOptions.tsx | 80 +- entrypoints/popup/{style.css => main.css} | 21 - entrypoints/popup/main.tsx | 58 +- .../popup/sections/AboutSection.styles.ts | 16 - entrypoints/popup/sections/AboutSection.tsx | 66 - entrypoints/popup/sections/GeneratorView.tsx | 112 - .../popup/sections/SettingsSection.styles.ts | 26 - .../popup/sections/SettingsSection.tsx | 138 - eslint.config.js | 2 +- locales/en.yml | 75 +- locales/pl.yml | 73 +- locales/pt_BR.yml | 73 +- locales/ru.yml | 75 +- locales/uk.yml | 73 +- locales/zh_CN.yml | 75 +- package-lock.json | 3170 +- package.json | 27 +- shared/App.tsx | 28 + shared/DoubleLabeledSwitch.styles.ts | 48 + shared/DoubleLabeledSwitch.tsx | 65 + shared/specials/Snow.css | 14 + .../popup => shared}/specials/Snow.styles.ts | 0 .../popup => shared}/specials/Snow.tsx | 1 + utils/PasswordGenerator.ts | 128 - utils/generators/dictionary.json | 50107 ++++++++++++++++ utils/generators/generatePassphrase.ts | 108 + utils/generators/generatePassword.ts | 142 + utils/generators/randomUtils.ts | 46 + utils/infoLabel.tsx | 15 + utils/links.ts | 2 +- utils/storage/ExtensionOptions.ts | 1 - utils/storage/GeneratorOptions.ts | 6 +- wxt.config.ts | 2 +- 62 files changed, 53646 insertions(+), 2560 deletions(-) create mode 100644 entrypoints/advanced/Page.styles.ts create mode 100644 entrypoints/advanced/Page.tsx create mode 100644 entrypoints/advanced/components/GeneratorForm.styles.ts create mode 100644 entrypoints/advanced/components/GeneratorForm.tsx create mode 100644 entrypoints/advanced/components/PasswordList.styles.ts create mode 100644 entrypoints/advanced/components/PasswordList.tsx create mode 100644 entrypoints/advanced/index.html create mode 100644 entrypoints/advanced/main.css create mode 100644 entrypoints/advanced/main.tsx create mode 100644 entrypoints/advanced/sections/PassphraseSection.styles.ts create mode 100644 entrypoints/advanced/sections/PassphraseSection.tsx create mode 100644 entrypoints/advanced/sections/PasswordSection.tsx delete mode 100644 entrypoints/background.ts delete mode 100644 entrypoints/content.ts create mode 100644 entrypoints/options/AboutSection.styles.ts create mode 100644 entrypoints/options/AboutSection.tsx create mode 100644 entrypoints/options/SettingsSection.styles.ts create mode 100644 entrypoints/options/SettingsSection.tsx create mode 100644 entrypoints/options/index.html create mode 100644 entrypoints/options/main.css create mode 100644 entrypoints/options/main.tsx delete mode 100644 entrypoints/popup/App.styles.ts delete mode 100644 entrypoints/popup/App.tsx rename entrypoints/popup/{sections => }/GeneratorView.styles.ts (64%) create mode 100644 entrypoints/popup/GeneratorView.tsx rename entrypoints/popup/{sections => }/QuickOptions.styles.ts (60%) rename entrypoints/popup/{sections => }/QuickOptions.tsx (55%) rename entrypoints/popup/{style.css => main.css} (67%) delete mode 100644 entrypoints/popup/sections/AboutSection.styles.ts delete mode 100644 entrypoints/popup/sections/AboutSection.tsx delete mode 100644 entrypoints/popup/sections/GeneratorView.tsx delete mode 100644 entrypoints/popup/sections/SettingsSection.styles.ts delete mode 100644 entrypoints/popup/sections/SettingsSection.tsx create mode 100644 shared/App.tsx create mode 100644 shared/DoubleLabeledSwitch.styles.ts create mode 100644 shared/DoubleLabeledSwitch.tsx create mode 100644 shared/specials/Snow.css rename {entrypoints/popup => shared}/specials/Snow.styles.ts (100%) rename {entrypoints/popup => shared}/specials/Snow.tsx (95%) delete mode 100644 utils/PasswordGenerator.ts create mode 100644 utils/generators/dictionary.json create mode 100644 utils/generators/generatePassphrase.ts create mode 100644 utils/generators/generatePassword.ts create mode 100644 utils/generators/randomUtils.ts create mode 100644 utils/infoLabel.tsx diff --git a/.github/workflows/cd_pipeline.yaml b/.github/workflows/cd_pipeline.yaml index 294755a..e706867 100644 --- a/.github/workflows/cd_pipeline.yaml +++ b/.github/workflows/cd_pipeline.yaml @@ -101,16 +101,10 @@ jobs: with: name: chrome - - name: Get version from package.json - id: get_version - run: | - extname=`ls password-generator-*-chrome.zip` - echo "filename=$extname" >> "$GITHUB_OUTPUT" - - - uses: wdzeng/chrome-extension@v1.2.4 + - uses: wdzeng/chrome-extension@v1.3.0 with: extension-id: jnjobgjobffgmgfnkpkjfjkkfhfikmfl - zip-path: ${{ steps.get_version.outputs.filename }} + zip-path: password-generator-*-chrome.zip client-id: ${{ secrets.CHROME_CLIENT_ID }} client-secret: ${{ secrets.CHROME_CLIENT_SECRET }} refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }} diff --git a/LICENSE b/LICENSE index 7452667..db0177c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Eugene Fox +Copyright (c) 2025 Eugene Fox Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index caa129d..a0e2079 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,14 @@ Password generator -Extension for web browsers which helps you to easily generate strong passwords in one click +Extension for web browsers which helps you to easily generate strong and customizable passwords in a few clicks ## Features - Customizable generator - Clean and simple UI - Dark mode -- **NEW:** Insert and copy generated password in one click - -![Demo](https://cdn.xfox111.net/projects/pwdgen/demo.gif) +- **NEW:** Advanced password generator +- **NEW:** Passphrase generator ## Languages - Chinese (Simplified) @@ -93,4 +92,4 @@ If you are interested in fixing issues and contributing directly to the code bas [![GitHub followers](https://img.shields.io/github/followers/xfox111?label=Follow%20@xfox111&style=social)](https://github.com/xfox111) [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-%40xfox111-orange)](https://buymeacoffee.com/xfox111) -> ©2024 Eugene Fox. Licensed under [MIT license](https://github.com/XFox111/PasswordGeneratorExtension/blob/main/LICENSE) +> ©2025 Eugene Fox. Licensed under [MIT license](https://github.com/XFox111/PasswordGeneratorExtension/blob/main/LICENSE) diff --git a/entrypoints/advanced/Page.styles.ts b/entrypoints/advanced/Page.styles.ts new file mode 100644 index 0000000..4d7c5e9 --- /dev/null +++ b/entrypoints/advanced/Page.styles.ts @@ -0,0 +1,59 @@ +import { makeStyles, tokens } from "@fluentui/react-components"; + +export const useStyles = makeStyles({ + root: + { + display: "grid", + gridTemplateColumns: "1fr 1fr", + height: "100vh", + gap: tokens.spacingHorizontalXL, + padding: `${tokens.spacingVerticalXL} ${tokens.spacingHorizontalXL}`, + boxSizing: "border-box", + }, + smallRoot: + { + display: "flex", + flexFlow: "column-reverse", + overflowX: "hidden", + overflowY: "auto", + padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalS}`, + }, + hideScroll: + { + overflowY: "visible", + maxHeight: "unset", + }, + configRoot: + { + maxHeight: "90vh", + display: "flex", + flexFlow: "column", + gap: tokens.spacingVerticalM, + width: "100%", + maxWidth: "480px", + borderRadius: tokens.borderRadiusLarge, + boxShadow: tokens.shadow4, + backgroundColor: tokens.colorNeutralBackground2, + padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`, + margin: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL}`, + boxSizing: "border-box", + justifySelf: "center", + alignSelf: "center", + overflowY: "auto", + }, + switch: + { + margin: `${tokens.spacingVerticalXL} ${tokens.spacingVerticalNone}`, + }, + listRoot: + { + width: "100%", + maxWidth: "840px", + alignSelf: "center", + justifySelf: "center", + padding: `${tokens.spacingVerticalXL} ${tokens.spacingHorizontalXL}`, + boxSizing: "border-box", + overflowY: "auto", + maxHeight: "100%", + }, +}); diff --git a/entrypoints/advanced/Page.tsx b/entrypoints/advanced/Page.tsx new file mode 100644 index 0000000..0b67e47 --- /dev/null +++ b/entrypoints/advanced/Page.tsx @@ -0,0 +1,58 @@ +import DoubleLabledSwitch from "@/shared/DoubleLabeledSwitch"; +import { mergeClasses, Toaster } from "@fluentui/react-components"; +import { ReactElement } from "react"; +import { useMediaQuery } from "react-responsive"; +import PasswordList from "./components/PasswordList"; +import { useStyles } from "./Page.styles"; +import PassphraseSection from "./sections/PassphraseSection"; +import PasswordSection from "./sections/PasswordSection"; + +export default function Page(): ReactElement +{ + const [isPassphrase, setIsPassphrase] = useState(null); + const [passwords, setPasswords] = useState([]); + const isSmall = useMediaQuery({ query: "(max-width: 1000px)" }); + + const cls = useStyles(); + + useEffect(() => + { + advancedPassphraseSelected.getValue().then(setIsPassphrase); + const unwatch = advancedPassphraseSelected.watch(setIsPassphrase); + + return () => unwatch(); + }, []); + + if (isPassphrase === null) + return <>; + + return ( +
+ + +
+ advancedPassphraseSelected.setValue(e.checked) } + offLabel={ i18n.t("advanced.password.title") } + onLabel={ i18n.t("advanced.passphrase.title") } /> + + { isPassphrase ? + + : + + } +
+ +
+ ); +}; + +export type GeneratorProps = + { + onGenerated: (passwords: string[]) => void; + }; + +const advancedPassphraseSelected = storage.defineItem("sync:AdvancedPassphraseSelected", { fallback: false }); diff --git a/entrypoints/advanced/components/GeneratorForm.styles.ts b/entrypoints/advanced/components/GeneratorForm.styles.ts new file mode 100644 index 0000000..58f5147 --- /dev/null +++ b/entrypoints/advanced/components/GeneratorForm.styles.ts @@ -0,0 +1,22 @@ +import { makeStyles, tokens } from "@fluentui/react-components"; + +export const useStyles = makeStyles({ + root: + { + display: "flex", + flexDirection: "column", + gap: tokens.spacingVerticalXL, + }, + actionRoot: + { + display: "flex", + gap: tokens.spacingHorizontalM, + alignSelf: "center", + }, + bulkRoot: + { + display: "grid", + alignItems: "center", + gridTemplateColumns: "24px 24px 56px", + } +}); diff --git a/entrypoints/advanced/components/GeneratorForm.tsx b/entrypoints/advanced/components/GeneratorForm.tsx new file mode 100644 index 0000000..008ccf4 --- /dev/null +++ b/entrypoints/advanced/components/GeneratorForm.tsx @@ -0,0 +1,91 @@ +import { Button, Input, InputOnChangeData, MessageBar, MessageBarBody, MessageBarTitle, Text, Toast, ToastTitle, useToastController } from "@fluentui/react-components"; +import { bundleIcon, Key24Regular, Save20Filled, Save20Regular } from "@fluentui/react-icons"; +import { PropsWithChildren, ReactElement } from "react"; +import { useStyles } from "./GeneratorForm.styles"; + +export default function GeneratorForm(props: GeneratorFormProps): ReactElement +{ + const [passwordCount, private_setPasswordCount] = useState(5); + const [error, setError] = useState(null); + const toaster = useToastController(); + + const cls = useStyles(); + const SaveIcon = bundleIcon(Save20Filled, Save20Regular); + + const setPasswordCount = useCallback((_: any, e: InputOnChangeData) => + { + const n = parseInt(e.value ?? "1"); + private_setPasswordCount(isNaN(n) || n < 1 ? null : Math.min(n, 1000)); + }, []); + + const onSubmit = useCallback((args: React.FormEvent) => + { + args.preventDefault(); + + try + { + setError(null); + props.onGenerate(passwordCount ?? 1); + } + catch (ex) + { + setError((ex as Error).message); + } + }, [props.onGenerate, passwordCount]); + + const onSave = useCallback(async () => + { + props.onSave(); + await browser.storage.sync.set({ AdvancedBulkCount: passwordCount ?? 5 }); + + toaster.dispatchToast( + + { i18n.t("advanced.saved_msg") } + , + { + intent: "success", + timeout: 1000 + } + ); + }, [props.onSave, toaster, passwordCount]); + + useEffect(() => + { + browser.storage.sync.get("AdvancedBulkCount").then(({ AdvancedBulkCount }) => + private_setPasswordCount(AdvancedBulkCount as number ?? 5) + ); + }, []); + + return ( +
+ { props.children } + + { error && + + + { error } + + + } + +
+
+ + x + +
+ +
+ + +
+ ); +} + +export type GeneratorFormProps = PropsWithChildren & +{ + onSave: () => void; + onGenerate: (count: number) => void; +}; diff --git a/entrypoints/advanced/components/PasswordList.styles.ts b/entrypoints/advanced/components/PasswordList.styles.ts new file mode 100644 index 0000000..4c01d4b --- /dev/null +++ b/entrypoints/advanced/components/PasswordList.styles.ts @@ -0,0 +1,50 @@ +import { makeStyles, tokens } from "@fluentui/react-components"; + +export const useStyles = makeStyles({ + root: + { + display: "flex", + flexFlow: "column", + gap: tokens.spacingVerticalS, + }, + copyAll: + { + alignSelf: "flex-end", + minHeight: "32px", + }, + table: + { + backgroundColor: tokens.colorNeutralBackground2, + borderRadius: "16px", + overflow: "clip", + }, + row: + { + "&:last-child": + { + borderBottom: "none", + } + }, + cell: + { + padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL}`, + cursor: "pointer", + }, + cellLayout: + { + overflowX: "auto", + }, + passwordText: + { + textOverflow: "ellipsis", + whiteSpace: "nowrap", + overflowX: "hidden", + display: "block", + marginRight: "36px", + }, + copyIcon: + { + verticalAlign: "middle", + padding: tokens.spacingHorizontalXL, + } +}); diff --git a/entrypoints/advanced/components/PasswordList.tsx b/entrypoints/advanced/components/PasswordList.tsx new file mode 100644 index 0000000..2a3508b --- /dev/null +++ b/entrypoints/advanced/components/PasswordList.tsx @@ -0,0 +1,79 @@ +import * as fui from "@fluentui/react-components"; +import { bundleIcon, Copy32Regular, CopyFilled, CopyRegular } from "@fluentui/react-icons"; +import { ReactElement } from "react"; +import { useStyles } from "./PasswordList.styles"; + +export default function PasswordList({ passwords, className }: PasswordListProps): ReactElement +{ + const toaster = fui.useToastController(); + + const CopyIcon = bundleIcon(CopyFilled, CopyRegular); + const cls = useStyles(); + + const copy = (password?: string) => + { + navigator.clipboard.writeText(password ?? passwords.join("\n")); + toaster.dispatchToast( + + { i18n.t("advanced.copied_msg") } + , + { + intent: "success", + timeout: 1000 + } + ); + }; + + return ( +
+ { passwords.length > 0 && + <> + } + onClick={ () => copy() }> + + { i18n.t("advanced.actions.copy_all") } + + + + + + { passwords.map((password, index) => + copy(password) }> + + + + + + + + + { password } + + + + + + + + + + + + ) } + + + + + } +
+ ); +} + +export type PasswordListProps = + { + className?: string; + passwords: string[]; + }; diff --git a/entrypoints/advanced/index.html b/entrypoints/advanced/index.html new file mode 100644 index 0000000..c89e0b3 --- /dev/null +++ b/entrypoints/advanced/index.html @@ -0,0 +1,15 @@ + + + + + + + Advanced password generator + + + +
+ + + + diff --git a/entrypoints/advanced/main.css b/entrypoints/advanced/main.css new file mode 100644 index 0000000..07e12e9 --- /dev/null +++ b/entrypoints/advanced/main.css @@ -0,0 +1,51 @@ +html, +body, +#root > .fui-FluentProvider +{ + padding: 0; + margin: 0; + height: 100vh; + + overflow: hidden; + + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; + + color-scheme: light dark; +} + +p, h1, h2 +{ + margin: 0; +} + +/* width */ +::-webkit-scrollbar +{ + width: 8px; +} + +/* Track */ +::-webkit-scrollbar-track +{ + background: transparent; +} + +/* Handle */ +::-webkit-scrollbar-thumb +{ + border-radius: 4px; + background: light-dark(#d1d1d1, #666666); +} + +/* Handle on hover */ +::-webkit-scrollbar-thumb:hover +{ + background: light-dark(#c7c7c7, #757575); +} + +::-webkit-scrollbar-thumb:hover:active +{ + background: light-dark(#b3b3b3, #6b6b6b); +} diff --git a/entrypoints/advanced/main.tsx b/entrypoints/advanced/main.tsx new file mode 100644 index 0000000..10cb94d --- /dev/null +++ b/entrypoints/advanced/main.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "../../shared/App"; +import "./main.css"; +import Page from "./Page"; + +document.title = i18n.t("advanced.title"); + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + +); diff --git a/entrypoints/advanced/sections/PassphraseSection.styles.ts b/entrypoints/advanced/sections/PassphraseSection.styles.ts new file mode 100644 index 0000000..b80e455 --- /dev/null +++ b/entrypoints/advanced/sections/PassphraseSection.styles.ts @@ -0,0 +1,16 @@ +import { makeStyles, tokens } from "@fluentui/react-components"; + +export const useStyles = makeStyles({ + root: + { + display: "flex", + flexFlow: "column", + gap: tokens.spacingVerticalSNudge, + }, + checkboxes: + { + display: "flex", + flexFlow: "column", + gap: tokens.spacingVerticalXXS, + }, +}); diff --git a/entrypoints/advanced/sections/PassphraseSection.tsx b/entrypoints/advanced/sections/PassphraseSection.tsx new file mode 100644 index 0000000..435a186 --- /dev/null +++ b/entrypoints/advanced/sections/PassphraseSection.tsx @@ -0,0 +1,89 @@ +import generatePassphrase, { PassphraseProps } from "@/utils/generators/generatePassphrase"; +import infoLabel from "@/utils/infoLabel"; +import { Checkbox, Field, Input, InputOnChangeData } from "@fluentui/react-components"; +import { ReactElement } from "react"; +import GeneratorForm from "../components/GeneratorForm"; +import { GeneratorProps } from "../Page"; +import { useStyles } from "./PassphraseSection.styles"; + +export default function PassphraseSection(props: GeneratorProps): ReactElement +{ + const [wordCount, private_setWordCount] = useState(2); + const [swapCharacters, setSwapCharacters] = useState(false); + const [separate, setSeparate] = useState(true); + const [separator, setSeparator] = useState(""); + const [allowRepeating, setAllowRepeating] = useState(false); + const [randomizeCase, setRandomizeCase] = useState(false); + + const config = useMemo(() => ({ + wordCount: wordCount ?? 2, + allowRepeating, + swapCharacters, + randomizeCase, + separator: separate ? (separator ? separator : " ") : "" + }), [wordCount, allowRepeating, swapCharacters, randomizeCase, separate, separator]); + + const cls = useStyles(); + + const setWordCount = useCallback((_: any, e: InputOnChangeData) => + { + const n = parseInt(e.value ?? ""); + private_setWordCount(isNaN(n) || n < 1 ? null : Math.min(n, 100)); + }, []); + + const saveConfiguration = useCallback( + async () => await browser.storage.sync.set({ AdvancedPassphraseOptions: config }), + [config] + ); + + const generate = useCallback((count: number) => + { + const passwords: string[] = []; + + for (let i = 0; i < count; i++) + passwords.push(generatePassphrase(config)); + + props.onGenerated(passwords); + }, [config, props.onGenerated]); + + useEffect(() => + { + browser.storage.sync.get("AdvancedPassphraseOptions").then(({ AdvancedPassphraseOptions }) => + { + if (!AdvancedPassphraseOptions) + return; + + private_setWordCount(AdvancedPassphraseOptions.wordCount ?? 2); + setAllowRepeating(AdvancedPassphraseOptions.allowRepeating); + setSwapCharacters(AdvancedPassphraseOptions.swapCharacters); + setRandomizeCase(AdvancedPassphraseOptions.randomizeCase); + setSeparate(!!AdvancedPassphraseOptions.separator); + setSeparator(AdvancedPassphraseOptions.separator ?? ""); + }); + }, []); + + return ( + +
+ + + +
+ setSwapCharacters(e.checked as boolean) } /> + setRandomizeCase(e.checked as boolean) } /> + setAllowRepeating(e.checked as boolean) } /> + +
+ setSeparate(e.checked as boolean) } /> + setSeparator(e.value) } /> +
+
+
+
+ ); +} diff --git a/entrypoints/advanced/sections/PasswordSection.tsx b/entrypoints/advanced/sections/PasswordSection.tsx new file mode 100644 index 0000000..f469f69 --- /dev/null +++ b/entrypoints/advanced/sections/PasswordSection.tsx @@ -0,0 +1,188 @@ +import { CharacterHints, generatePassword } from "@/utils/generators/generatePassword"; +import infoLabel from "@/utils/infoLabel"; +import * as fui from "@fluentui/react-components"; +import { ReactElement } from "react"; +import { GeneratorProps } from "../Page"; +import GeneratorForm from "../components/GeneratorForm"; + +// TODO: needs refactoring +export default function PasswordSection(props: GeneratorProps): ReactElement +{ + const [state, private_setState] = useState({ + length: 8, + enableUppercase: true, uppercaseCount: 1, + enableLowercase: true, lowercaseCount: 1, + enableNumeric: true, numericCount: 1, + enableSpecial: true, specialCount: 1, + enableCustom: false, customCount: 1, customSet: "", + + excludeSimilar: true, + excludeAmbiguous: true, + excludeRepeating: false, + excludeCustom: false, excludeCustomSet: "" + }); + + const cls = useStyles(); + + const setState = useCallback((newState: Partial) => + { + private_setState({ ...state, ...newState }); + }, [state]); + + const setLength = useCallback((_: any, e: fui.InputOnChangeData) => + { + const n = parseInt(e.value ?? ""); + setState({ length: isNaN(n) || n < 1 ? null : n }); + }, [state]); + + const saveConfiguration = useCallback( + async () => await browser.storage.sync.set({ AdvancedPasswordOptions: state }), + [state] + ); + + const generate = useCallback((count: number) => + { + const passwords: string[] = []; + + for (let i = 0; i < count; i++) + passwords.push(generatePassword({ + length: state.length ?? 8, + custom: state.enableCustom ? state.customCount ?? 1 : 0, + customSet: state.customSet, + numeric: state.enableNumeric ? state.numericCount ?? 1 : 0, + special: state.enableSpecial ? state.specialCount ?? 1 : 0, + uppercase: state.enableUppercase ? state.uppercaseCount ?? 1 : 0, + lowercase: state.enableLowercase ? state.lowercaseCount ?? 1 : 0, + excludeAmbiguous: state.excludeAmbiguous, + excludeCustom: state.excludeCustom ? state.excludeCustomSet : "", + excludeRepeating: state.excludeRepeating, + excludeSimilar: state.excludeSimilar, + })); + + props.onGenerated(passwords); + }, [state, props.onGenerated]); + + useEffect(() => + { + browser.storage.sync.get("AdvancedPasswordOptions").then(({ AdvancedPasswordOptions }) => + private_setState({ ...state, ...AdvancedPasswordOptions as PasswordSectionState })); + }, []); + + const checkboxControls = useCallback((key: keyof PasswordSectionState): Partial => ({ + checked: state[key] as boolean, + onChange: (_, e) => setState({ [key]: e.checked as boolean }) + }), [state]); + + const minInputControls = useCallback((enabledKey: keyof PasswordSectionState, key: keyof PasswordSectionState): Partial => ({ + size: "small", + disabled: !state[enabledKey], + value: state[key]?.toString() ?? "", + onChange: (_, e) => setState({ [key]: parseCount(e.value) }) + }), [state]); + + return ( + + + + + + + + { i18n.t("common.sections.include") } + { i18n.t("advanced.password.min_of_type") } + + + + + + + + + + + + + + + + + + + + + <> + + setState({ customSet: e.value }) } /> + + + + + + +
+ { i18n.t("common.sections.exclude") } + + + +
+ + setState({ excludeCustomSet: e.value }) } /> +
+
+
+ ); +} + +function parseCount(value: string): number | null +{ + const n = parseInt(value); + return isNaN(n) || n < 1 ? null : Math.min(n, 100); +}; + +function Row(props: { children: ReactElement[]; }): ReactElement +{ + return ( + + { props.children.map((i, index) => + + { i } + + ) } + + ); +} + +const useStyles = fui.makeStyles({ + section: + { + display: "flex", + flexDirection: "column", + }, +}); + +type PasswordSectionState = + { + length: number | null; + enableUppercase: boolean; + uppercaseCount: number | null; + enableLowercase: boolean; + lowercaseCount: number | null; + enableNumeric: boolean; + numericCount: number | null; + enableSpecial: boolean; + specialCount: number | null; + enableCustom: boolean; + customCount: number | null; + + excludeSimilar: boolean; + excludeAmbiguous: boolean; + excludeRepeating: boolean; + excludeCustom: boolean; + + excludeCustomSet: string; + customSet: string; + }; diff --git a/entrypoints/background.ts b/entrypoints/background.ts deleted file mode 100644 index ecd68b0..0000000 --- a/entrypoints/background.ts +++ /dev/null @@ -1,24 +0,0 @@ -export default defineBackground(() => main()); - -async function main(): Promise -{ - await browser.contextMenus.removeAll(); - browser.contextMenus.onClicked.addListener(() => browser.action.openPopup()); - - const showMenu: boolean = (await storage.getItem("sync:ContextMenu", { fallback: true }))!; - updateMenus(showMenu); - - storage.watch("sync:ContextMenu", e => updateMenus(e!)); -} - -async function updateMenus(showMenus: boolean): Promise -{ - await browser.contextMenus.removeAll(); - - if (showMenus) - browser.contextMenus.create({ - id: "password-generator", - title: i18n.t("manifest.name"), - contexts: ["all"], - }); -} diff --git a/entrypoints/content.ts b/entrypoints/content.ts deleted file mode 100644 index 9301d36..0000000 --- a/entrypoints/content.ts +++ /dev/null @@ -1,22 +0,0 @@ -export default defineContentScript({ - matches: [""], - 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(); - }); - }); - }, -}); diff --git a/entrypoints/options/AboutSection.styles.ts b/entrypoints/options/AboutSection.styles.ts new file mode 100644 index 0000000..234cf53 --- /dev/null +++ b/entrypoints/options/AboutSection.styles.ts @@ -0,0 +1,16 @@ +import { makeStyles, tokens } from "@fluentui/react-components"; + +export const useStyles = makeStyles({ + root: + { + display: "flex", + flexDirection: "column", + gap: tokens.spacingVerticalM, + padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`, + }, + horizontalContainer: + { + display: "flex", + gap: tokens.spacingHorizontalSNudge, + }, +}); diff --git a/entrypoints/options/AboutSection.tsx b/entrypoints/options/AboutSection.tsx new file mode 100644 index 0000000..4030776 --- /dev/null +++ b/entrypoints/options/AboutSection.tsx @@ -0,0 +1,61 @@ +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 { PersonFeedbackRegular } from "@fluentui/react-icons"; +import { ReactElement, ReactNode } from "react"; +import { useStyles } from "./AboutSection.styles"; + +export default function AboutSection(): ReactElement +{ + const bmcTheme = useTheme(bmcLightTheme, bmcDarkTheme); + const cls = useStyles(); + + return ( +
+
+ { i18n.t("manifest.name") } + v{ browser.runtime.getManifest().version } +
+ + + { i18n.t("about.developed_by") } ({ link("@xfox111.net", personalLinks.social) }) +
+ { i18n.t("about.licensed_under") } { link(i18n.t("about.mit_license"), githubLinks.license) } +
+ + + { i18n.t("about.translation_cta.text") }
+ { link(i18n.t("about.translation_cta.button"), githubLinks.translationGuide) } +
+ + + { link(i18n.t("about.links.website"), personalLinks.website) }
+ { link(i18n.t("about.links.source"), githubLinks.repository) }
+ { link(i18n.t("about.links.changelog"), githubLinks.changelog) } +
+ +
+ ) }> + { i18n.t("about.cta.feedback") } + + + ) }> + { i18n.t("about.cta.sponsor") } + + +
+
+ ); +}; + +const link = (text: string, href: string): ReactNode => ( + { text } +); + +const buttonProps = (href: string, icon: JSX.Element): fui.ButtonProps => ( + { + as: "a", target: "_blank", href, + appearance: "primary", icon + } +); diff --git a/entrypoints/options/SettingsSection.styles.ts b/entrypoints/options/SettingsSection.styles.ts new file mode 100644 index 0000000..629ab87 --- /dev/null +++ b/entrypoints/options/SettingsSection.styles.ts @@ -0,0 +1,38 @@ +import { makeStyles, tokens } from "@fluentui/react-components"; + +export const useStyles = makeStyles({ + root: + { + display: "flex", + flexDirection: "column", + gap: tokens.spacingVerticalMNudge, + padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalMNudge}`, + }, + checkboxContainer: + { + display: "flex", + flexWrap: "wrap", + }, + rangeContainer: + { + display: "grid", + gridTemplateColumns: "1fr auto 1fr auto", + alignItems: "center", + gap: tokens.spacingHorizontalS, + }, + rangeInput: + { + width: "100%", + }, + excludeCustom: + { + display: "grid", + gridTemplateColumns: "auto 1fr", + gap: tokens.spacingHorizontalS, + alignItems: "center", + }, + divider: + { + flexGrow: 0, + }, +}); diff --git a/entrypoints/options/SettingsSection.tsx b/entrypoints/options/SettingsSection.tsx new file mode 100644 index 0000000..c118fe0 --- /dev/null +++ b/entrypoints/options/SettingsSection.tsx @@ -0,0 +1,128 @@ +import { CharacterHints } from "@/utils/generators/generatePassword"; +import { ExtensionOptions, GeneratorOptions, useStorage } from "@/utils/storage"; +import * as fui from "@fluentui/react-components"; +import { ArrowUndoRegular } from "@fluentui/react-icons"; +import { ReactElement } from "react"; +import infoLabel from "../../utils/infoLabel"; +import { useStyles } from "./SettingsSection.styles"; + +export default function SettingsSection(): ReactElement +{ + 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 ( +
+ + + + + + +
+ + + + + + + + } + onClick={ resetRange } /> + +
+
+ + + + { i18n.t("common.sections.include") } +
+ + + + +
+ + updateStorage({ IncludeCustomSet: e.value }) } /> +
+
+ + { i18n.t("common.sections.exclude") } +
+ + + +
+ + updateStorage({ ExcludeCustomSet: e.value }) } /> +
+
+
+ ); +}; + +const defaultOptions = +{ + generator: new GeneratorOptions(), + extension: new ExtensionOptions() +}; diff --git a/entrypoints/options/index.html b/entrypoints/options/index.html new file mode 100644 index 0000000..43e96a6 --- /dev/null +++ b/entrypoints/options/index.html @@ -0,0 +1,15 @@ + + + + + + + Settings - Password generator + + + +
+ + + + diff --git a/entrypoints/options/main.css b/entrypoints/options/main.css new file mode 100644 index 0000000..d4cc4d5 --- /dev/null +++ b/entrypoints/options/main.css @@ -0,0 +1,65 @@ +html, +body, +#root > .fui-FluentProvider +{ + padding: 0; + margin: 0; + height: 450px; + + overflow: auto; + + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; + + color-scheme: light dark; +} + +main +{ + display: flex; + flex-flow: column; + height: 100%; + overflow: auto; +} + +section +{ + flex-grow: 1; + overflow: auto; +} + +p, h1, h2 +{ + margin: 0; +} + +/* width */ +::-webkit-scrollbar +{ + width: 8px; +} + +/* Track */ +::-webkit-scrollbar-track +{ + background: transparent; +} + +/* Handle */ +::-webkit-scrollbar-thumb +{ + border-radius: 4px; + background: light-dark(#d1d1d1, #666666); +} + +/* Handle on hover */ +::-webkit-scrollbar-thumb:hover +{ + background: light-dark(#c7c7c7, #757575); +} + +::-webkit-scrollbar-thumb:hover:active +{ + background: light-dark(#b3b3b3, #6b6b6b); +} diff --git a/entrypoints/options/main.tsx b/entrypoints/options/main.tsx new file mode 100644 index 0000000..6fdad18 --- /dev/null +++ b/entrypoints/options/main.tsx @@ -0,0 +1,35 @@ +import { Tab, TabList } from "@fluentui/react-components"; +import { bundleIcon, FluentIcon, Info20Filled, Info20Regular, Settings20Filled, Settings20Regular } from "@fluentui/react-icons"; +import { ReactElement, StrictMode } from "react"; +import ReactDOM from "react-dom/client"; +import App from "../../shared/App"; +import AboutSection from "./AboutSection"; +import "./main.css"; +import SettingsSection from "./SettingsSection"; + +function Options(): ReactElement +{ + const [selection, setSelection] = useState("settings"); + + const SettingsIcon: FluentIcon = bundleIcon(Settings20Filled, Settings20Regular); + const AboutIcon: FluentIcon = bundleIcon(Info20Filled, Info20Regular); + + return ( +
+ setSelection(e.value as string) }> + } content={ i18n.t("settings.title") } value="settings" /> + } content={ i18n.t("about.title") } value="about" /> + + { selection === "settings" && } + { selection === "about" && } +
+ ); +}; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + +); diff --git a/entrypoints/popup/App.styles.ts b/entrypoints/popup/App.styles.ts deleted file mode 100644 index d3cd411..0000000 --- a/entrypoints/popup/App.styles.ts +++ /dev/null @@ -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, - } - }, -}); diff --git a/entrypoints/popup/App.tsx b/entrypoints/popup/App.tsx deleted file mode 100644 index 3d4f49c..0000000 --- a/entrypoints/popup/App.tsx +++ /dev/null @@ -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([]); - - return ( - -
- }> - - setSelection(e.openItems as string[]) } - collapsible> - - - - - - - -
-
- ); -}; - -export default App; diff --git a/entrypoints/popup/sections/GeneratorView.styles.ts b/entrypoints/popup/GeneratorView.styles.ts similarity index 64% rename from entrypoints/popup/sections/GeneratorView.styles.ts rename to entrypoints/popup/GeneratorView.styles.ts index c5f0f49..8ff75f3 100644 --- a/entrypoints/popup/sections/GeneratorView.styles.ts +++ b/entrypoints/popup/GeneratorView.styles.ts @@ -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", + } }); diff --git a/entrypoints/popup/GeneratorView.tsx b/entrypoints/popup/GeneratorView.tsx new file mode 100644 index 0000000..476c848 --- /dev/null +++ b/entrypoints/popup/GeneratorView.tsx @@ -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(""); + const [error, setError] = useState(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 ( + + { error } + + ); + + return ( + + + + +