Files
supabase/apps/studio/components/interfaces/Account/Preferences/AddPasswordRow.tsx
T
Danny White 1131e3e2ce fix(ui): default Button variant to default instead of primary (#50160)
## What kind of change does this PR introduce?

Bug fix / design-system alignment for the legacy `Button` from `ui`.

## What is the current behavior?

Omitting `variant` on the legacy `Button` falls back to brand-green
`primary`. That makes accidental greens easy, and it is hard to spot the
real main action on busy pages.

## What is the new behavior?

- Legacy `Button` now defaults to neutral `default`
- Intentional primary CTAs (create, save, submit, marketing CTAs, and
matching `ButtonTooltip` usages) now set `variant="primary"` so their
appearance is unchanged
- Neutral actions that previously relied on the old fallback (cancel,
close, back, dashboard nav, and similar) become grey/white
- Design-system docs updated; regression tests cover the new default

`Button_Shadcn_` is unchanged. It already uses its own CVA default.

This is PR 1 of 2 in a stack. PR 2 drops now-redundant
`variant="default"` props.

## To test

Studio (http://localhost:8082):

- `/sign-in`: Sign in stays green
- Open a project → Database → Tables: New table stays green
- Auth → Users → Invite: Invite user stays green; Cancel / dismiss
controls stay neutral
- Project Settings → General: edit a field so Cancel and Save appear.
Cancel is neutral, Save is green

Design system (http://localhost:3003):

- Components → Button: default demo is neutral; primary demo is green;
featured preview is the default variant

Marketing (optional):

- www header: Start your project stays green; logged-in Dashboard is
neutral

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Style**
- Buttons now default to a neutral style, while primary actions across
Studio, documentation, marketing pages, forms, dialogs, and error states
use prominent primary styling.
- Updated button examples and previews clarify the distinction between
default and primary variants.
  - Event registration now includes a directional arrow icon.

- **Tests**
- Added coverage confirming default button styling and explicit primary
styling behave as expected.
- Updated related test fixtures to use primary styling where
appropriate.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-10 11:23:17 +10:00

151 lines
4.9 KiB
TypeScript

import { zodResolver } from '@hookform/resolvers/zod'
import { Eye, EyeOff } from 'lucide-react'
import { useState } from 'react'
import { useForm, useWatch } from 'react-hook-form'
import { toast } from 'sonner'
import {
Button,
CardContent,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogTitle,
Form,
FormControl,
FormField,
Input,
} from 'ui'
import { Input as InputWithActions } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { z } from 'zod'
import PasswordConditionsHelper from '@/components/interfaces/SignIn/PasswordConditionsHelper'
import { IdentityProviderIcon } from '@/components/ui/ProviderIcon'
import { useSetPasswordMutation } from '@/data/profile/profile-set-password-mutation'
import { getProviderDisplay } from '@/lib/external-identity-providers'
import { passwordValidation } from '@/lib/password-validation'
const FORM_ID = 'add-password-form'
const EMAIL_INPUT_ID = 'add-password-email'
const FormSchema = z.object({ password: passwordValidation })
type FormValues = z.infer<typeof FormSchema>
const defaultValues: FormValues = { password: '' }
/**
* Offers a user without an email identity (OAuth-only account) a way to create one by setting a
* password.
*
* Auth creates the email identity for the user's current email when the password is
* saved.
*/
export const AddPasswordRow = ({ email }: { email: string }) => {
const [isDialogOpen, setIsDialogOpen] = useState(false)
const providerDisplay = getProviderDisplay('email')
return (
<>
<CardContent className="flex justify-between items-center py-4">
<div className="flex gap-x-4">
<IdentityProviderIcon display={providerDisplay} size={30} />
<div>
<p className="text-sm">{providerDisplay.displayName}</p>
<p className="text-sm text-foreground-lighter">{email}</p>
</div>
</div>
<Button variant="default" onClick={() => setIsDialogOpen(true)}>
Add password
</Button>
</CardContent>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent>
<DialogHeader className="border-b">
<DialogTitle>Add password</DialogTitle>
</DialogHeader>
<AddPasswordForm email={email} onClose={() => setIsDialogOpen(false)} />
</DialogContent>
</Dialog>
</>
)
}
const AddPasswordForm = ({ email, onClose }: { email: string; onClose: () => void }) => {
const [passwordHidden, setPasswordHidden] = useState(true)
const form = useForm<FormValues>({
resolver: zodResolver(FormSchema),
defaultValues,
mode: 'onChange',
})
const password = useWatch({ control: form.control, name: 'password' })
const { mutate: setPassword, isPending } = useSetPasswordMutation({
onSuccess: () => {
toast.success('Password added successfully')
onClose()
},
})
const onSubmit = (values: FormValues) => setPassword({ password: values.password })
return (
<Form {...form}>
<form id={FORM_ID} onSubmit={form.handleSubmit(onSubmit)}>
<DialogSection className="flex flex-col gap-y-4">
<FormItemLayout
isReactForm={false}
id={EMAIL_INPUT_ID}
label="Email"
description="Adding a password lets you sign in with this email address"
>
<Input id={EMAIL_INPUT_ID} disabled value={email} />
</FormItemLayout>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItemLayout label="Password">
<FormControl>
<InputWithActions
id="password"
type={passwordHidden ? 'password' : 'text'}
placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;"
disabled={isPending}
autoComplete="new-password"
actions={
<Button
icon={passwordHidden ? <Eye /> : <EyeOff />}
aria-label={passwordHidden ? 'Show password' : 'Hide Password'}
variant="default"
className="w-7"
onClick={() => setPasswordHidden((prev) => !prev)}
/>
}
{...field}
/>
</FormControl>
</FormItemLayout>
)}
/>
<PasswordConditionsHelper password={password} />
</DialogSection>
<DialogFooter>
<Button variant="default" disabled={isPending} onClick={onClose}>
Cancel
</Button>
<Button variant="primary" type="submit" loading={isPending} disabled={isPending}>
Add password
</Button>
</DialogFooter>
</form>
</Form>
)
}