diff --git a/.vscode/settings.json b/.vscode/settings.json index 66b252e97..64b9bd6f0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,12 @@ "source.organizeImports.biome": "explicit", "quickfix.biome": "explicit" }, - "editor.defaultFormatter": "biomejs.biome" + "editor.defaultFormatter": "biomejs.biome", + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "tailwindCSS.experimental.classRegex": [ + ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], + ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] + ] } diff --git a/package.json b/package.json index c7c7f7e21..5d5b94403 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "@electron/remote": "2.1.2", "@primer/octicons-react": "19.10.0", "axios": "1.7.2", + "class-variance-authority": "^0.7.0", "clsx": "2.1.1", "date-fns": "3.6.0", "electron-updater": "6.2.1", @@ -113,6 +114,7 @@ "react-dom": "18.3.1", "react-final-form": "6.5.9", "react-router-dom": "6.24.1", + "tailwind-merge": "^2.3.0", "ts-loader": "9.5.1", "typescript": "5.5.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b018a947..cce9ceed9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: axios: specifier: 1.7.2 version: 1.7.2 + class-variance-authority: + specifier: ^0.7.0 + version: 0.7.0 clsx: specifier: 2.1.1 version: 2.1.1 @@ -47,6 +50,9 @@ importers: react-router-dom: specifier: 6.24.1 version: 6.24.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwind-merge: + specifier: ^2.3.0 + version: 2.3.0 ts-loader: specifier: 9.5.1 version: 9.5.1(typescript@5.5.3)(webpack@5.92.1(webpack-cli@5.1.4)) @@ -1082,6 +1088,9 @@ packages: cjs-module-lexer@1.2.3: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + class-variance-authority@0.7.0: + resolution: {integrity: sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==} + cli-truncate@2.1.0: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} engines: {node: '>=8'} @@ -1097,6 +1106,10 @@ packages: clone-response@1.0.3: resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + clsx@2.0.0: + resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} + engines: {node: '>=6'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -2815,6 +2828,9 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tailwind-merge@2.3.0: + resolution: {integrity: sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==} + tailwindcss@3.4.4: resolution: {integrity: sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==} engines: {node: '>=14.0.0'} @@ -4389,6 +4405,10 @@ snapshots: cjs-module-lexer@1.2.3: {} + class-variance-authority@0.7.0: + dependencies: + clsx: 2.0.0 + cli-truncate@2.1.0: dependencies: slice-ansi: 3.0.0 @@ -4411,6 +4431,8 @@ snapshots: dependencies: mimic-response: 1.0.1 + clsx@2.0.0: {} + clsx@2.1.1: {} co@4.6.0: {} @@ -6272,6 +6294,10 @@ snapshots: symbol-tree@3.2.4: {} + tailwind-merge@2.3.0: + dependencies: + '@babel/runtime': 7.24.1 + tailwindcss@3.4.4(ts-node@10.9.2(@types/node@20.14.9)(typescript@5.5.3)): dependencies: '@alloc/quick-lru': 5.2.0 diff --git a/src/components/__snapshots__/Sidebar.test.tsx.snap b/src/components/__snapshots__/Sidebar.test.tsx.snap index e16d94651..0af5dada7 100644 --- a/src/components/__snapshots__/Sidebar.test.tsx.snap +++ b/src/components/__snapshots__/Sidebar.test.tsx.snap @@ -63,7 +63,7 @@ exports[`components/Sidebar.tsx should render itself & its children (logged in) ); expect(tree).toMatchSnapshot(); }); it('should render with icon', () => { - const tree = render(, + ); expect(tree).toMatchSnapshot(); }); it('should render with url', () => { - render(, + ); const buttonElement = screen.getByLabelText('button'); diff --git a/src/components/buttons/Button.tsx b/src/components/buttons/Button.tsx index 7aaf37353..fa3c6bd98 100644 --- a/src/components/buttons/Button.tsx +++ b/src/components/buttons/Button.tsx @@ -1,45 +1,75 @@ import type { Icon } from '@primer/octicons-react'; -import type { FC } from 'react'; +import { type VariantProps, cva } from 'class-variance-authority'; +import { type ButtonHTMLAttributes, forwardRef } from 'react'; import { type Link, Size } from '../../types'; import { cn } from '../../utils/cn'; import { openExternalLink } from '../../utils/comms'; -export interface IButton { - name: string; - label: string; - className?: string; - icon?: Icon; - size?: Size; +const buttonVariants = cva( + 'ring-offset-background focus-visible:ring-ring inline-flex w-fit items-center justify-center whitespace-nowrap rounded text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: 'bg-gray-300 dark:text-black hover:opacity-90', + destructive: 'bg-red-600 text-white hover:opacity-90', + outline: + 'border-zinc-300 hover:text-inherit dark:hover:text-white border bg-transparent hover:bg-zinc-100 dark:hover:bg-zinc-600', + ghost: 'hover:bg-zinc-100 dark:hover:bg-zinc-600 dark:hover:text-white', + link: 'underline-offset-2 hover:underline', + }, + size: { + default: 'min-w-20 h-10 px-4 py-1', + xs: 'h-6 rounded-md px-2', + sm: 'h-9 rounded-md px-2 py-1', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +export interface IButton + extends ButtonHTMLAttributes, + VariantProps { + icon?: { icon: Icon; size?: Size }; url?: Link; onClick?: () => void; - disabled?: boolean; - type?: 'button' | 'submit'; + label: string; } -export const Button: FC = (props: IButton) => { - const baseClass = - 'rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1'; - return ( - + ); + }, +); +Button.displayName = 'Button'; - if (props.onClick) { - return props.onClick(); - } - }} - > - {props.icon && ( - - )} - {props.name} - - ); -}; +export { Button, buttonVariants }; diff --git a/src/components/buttons/__snapshots__/Button.test.tsx.snap b/src/components/buttons/__snapshots__/Button.test.tsx.snap index fdcedf8a8..e1fb0dfd3 100644 --- a/src/components/buttons/__snapshots__/Button.test.tsx.snap +++ b/src/components/buttons/__snapshots__/Button.test.tsx.snap @@ -7,13 +7,12 @@ exports[`components/buttons/Button.tsx should render with icon 1`] = `
diff --git a/src/components/buttons/__snapshots__/SidebarButton.test.tsx.snap b/src/components/buttons/__snapshots__/SidebarButton.test.tsx.snap index 8275f94cb..c4c6cf409 100644 --- a/src/components/buttons/__snapshots__/SidebarButton.test.tsx.snap +++ b/src/components/buttons/__snapshots__/SidebarButton.test.tsx.snap @@ -6,7 +6,7 @@ exports[`components/buttons/SidebarButton.tsx should render - with specific size "baseElement":
); }; diff --git a/src/routes/LoginWithOAuthApp.test.tsx b/src/routes/LoginWithOAuthApp.test.tsx index 4a4320ef0..2f46a141c 100644 --- a/src/routes/LoginWithOAuthApp.test.tsx +++ b/src/routes/LoginWithOAuthApp.test.tsx @@ -125,7 +125,7 @@ describe('routes/LoginWithOAuthApp.tsx', () => { target: { value: 'abc' }, }); - fireEvent.submit(screen.getByTitle('Login')); + fireEvent.submit(screen.getByLabelText('Login')); expect(screen.getByText('Invalid hostname.')).toBeTruthy(); expect(screen.getByText('Invalid client id.')).toBeTruthy(); diff --git a/src/routes/LoginWithOAuthApp.tsx b/src/routes/LoginWithOAuthApp.tsx index 5838e6a89..bd3b26573 100644 --- a/src/routes/LoginWithOAuthApp.tsx +++ b/src/routes/LoginWithOAuthApp.tsx @@ -72,13 +72,13 @@ export const LoginWithOAuthApp: FC = () => { helpText={
on GitHub then paste your client id and client secret below.
@@ -98,8 +98,7 @@ export const LoginWithOAuthApp: FC = () => { name="Docs" label="GitHub Docs" className="mt-2" - icon={BookIcon} - size={Size.XSMALL} + icon={{ icon: BookIcon, size: Size.XSMALL }} url={Constants.GITHUB_DOCS.OAUTH_URL} /> @@ -107,8 +106,7 @@ export const LoginWithOAuthApp: FC = () => { name="Login" label="Login" className="mt-2 px-4 py-2 !text-sm" - icon={SignInIcon} - size={Size.MEDIUM} + icon={{ icon: SignInIcon, size: Size.MEDIUM }} disabled={submitting || pristine} type="submit" /> diff --git a/src/routes/LoginWithPersonalAccessToken.test.tsx b/src/routes/LoginWithPersonalAccessToken.test.tsx index 83c9c284d..def9feb25 100644 --- a/src/routes/LoginWithPersonalAccessToken.test.tsx +++ b/src/routes/LoginWithPersonalAccessToken.test.tsx @@ -128,7 +128,7 @@ describe('routes/LoginWithPersonalAccessToken.tsx', () => { target: { value: 'github.com' }, }); - fireEvent.submit(screen.getByTitle('Login')); + fireEvent.submit(screen.getByLabelText('Login')); await waitFor(() => expect(mockValidateToken).toHaveBeenCalledTimes(1)); @@ -156,7 +156,7 @@ describe('routes/LoginWithPersonalAccessToken.tsx', () => { fireEvent.change(screen.getByLabelText('Hostname'), { target: { value: 'github.com' }, }); - fireEvent.submit(screen.getByTitle('Login')); + fireEvent.submit(screen.getByLabelText('Login')); }); await waitFor(() => expect(mockValidateToken).toHaveBeenCalledTimes(1)); @@ -179,7 +179,7 @@ describe('routes/LoginWithPersonalAccessToken.tsx', () => { target: { value: '123' }, }); - fireEvent.submit(screen.getByTitle('Login')); + fireEvent.submit(screen.getByLabelText('Login')); expect(screen.getByText('Invalid hostname.')).toBeDefined(); expect(screen.getByText('Invalid token.')).toBeDefined(); diff --git a/src/routes/LoginWithPersonalAccessToken.tsx b/src/routes/LoginWithPersonalAccessToken.tsx index 21faaeb37..b5d6d6e82 100644 --- a/src/routes/LoginWithPersonalAccessToken.tsx +++ b/src/routes/LoginWithPersonalAccessToken.tsx @@ -72,13 +72,13 @@ export const LoginWithPersonalAccessToken: FC = () => {
on GitHub and paste above.
@@ -94,24 +94,23 @@ export const LoginWithPersonalAccessToken: FC = () => {
)} -
+
); @@ -133,7 +132,7 @@ export const LoginWithPersonalAccessToken: FC = () => { ); return ( -
+ <>
Login with Personal Access Token @@ -151,6 +150,6 @@ export const LoginWithPersonalAccessToken: FC = () => { {renderForm}
-
+ ); }; diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx index 878a6e1d7..8397dfe56 100644 --- a/src/routes/Settings.tsx +++ b/src/routes/Settings.tsx @@ -8,7 +8,7 @@ import { SystemSettings } from '../components/settings/SystemSettings'; export const SettingsRoute: FC = () => { return (
-
Settings
+
Settings
diff --git a/src/routes/__snapshots__/Login.test.tsx.snap b/src/routes/__snapshots__/Login.test.tsx.snap index 16c86b809..d40784a56 100644 --- a/src/routes/__snapshots__/Login.test.tsx.snap +++ b/src/routes/__snapshots__/Login.test.tsx.snap @@ -66,13 +66,12 @@ exports[`routes/Login.tsx should render itself & its children 1`] = `
@@ -279,14 +277,13 @@ exports[`routes/LoginWithOAuthApp.tsx renders correctly 1`] = ` >
diff --git a/src/routes/__snapshots__/LoginWithPersonalAccessToken.test.tsx.snap b/src/routes/__snapshots__/LoginWithPersonalAccessToken.test.tsx.snap index 5bfd8c9eb..b0aa2dc5e 100644 --- a/src/routes/__snapshots__/LoginWithPersonalAccessToken.test.tsx.snap +++ b/src/routes/__snapshots__/LoginWithPersonalAccessToken.test.tsx.snap @@ -4,196 +4,6 @@ exports[`routes/LoginWithPersonalAccessToken.tsx renders correctly 1`] = ` { "asFragment": [Function], "baseElement": -
-
-
- -

- - Login with Personal Access Token -

-
-
-
-
- - -
-
- Change only if you are using GitHub Enterprise Server. -
-
-
-
- - -
-
-
- - - on GitHub and paste above. - -
-
- The required scopes will be selected for you. -
-
-
-
-
- - -
-
-
-
-
- , - "container":
+ , + "container":
+
+ +

+ + Login with Personal Access Token +

+
+
+
+
+ + +
+
+ Change only if you are using GitHub Enterprise Server. +
+
+
+
+ + +
+
+
+ + + on GitHub and paste above. + +
+
+ The required scopes will be selected for you. +
+
+
+
+
+ + +
+
+
, "debug": [Function], "findAllByAltText": [Function], diff --git a/src/routes/__snapshots__/Settings.test.tsx.snap b/src/routes/__snapshots__/Settings.test.tsx.snap index 83afa9748..54e341ca1 100644 --- a/src/routes/__snapshots__/Settings.test.tsx.snap +++ b/src/routes/__snapshots__/Settings.test.tsx.snap @@ -48,7 +48,7 @@ exports[`routes/Settings.tsx should render itself & its children 1`] = ` Appearance