From 7403ebea0684932bd9e9e5d5f6639120aa608201 Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Tue, 17 Mar 2026 15:19:22 -0400 Subject: [PATCH] feat: switch form fields to base-ui --- app/components/Input.tsx | 84 --------- app/components/Notice.tsx | 45 ----- app/components/NumberInput.tsx | 99 ---------- app/components/Popover.tsx | 45 ----- app/components/RadioGroup.tsx | 78 -------- app/components/Select.tsx | 170 ------------------ app/components/Switch.tsx | 56 ------ app/components/Text.tsx | 11 -- app/components/Title.tsx | 13 -- .../{Attribute.tsx => attribute.tsx} | 2 +- app/components/{Card.tsx => card.tsx} | 4 +- app/components/{Chip.tsx => chip.tsx} | 0 app/components/{Code.tsx => code.tsx} | 0 app/components/{Dialog.tsx => dialog.tsx} | 0 app/components/error-banner.tsx | 4 +- app/components/input.tsx | 72 ++++++++ app/components/notice.tsx | 34 ++++ app/components/number-input.tsx | 68 +++++++ app/components/radio-group.tsx | 60 +++++++ app/components/select.tsx | 138 ++++++++++++++ .../{StatusCircle.tsx => status-circle.tsx} | 0 app/components/switch.tsx | 45 +++++ .../{TableList.tsx => table-list.tsx} | 0 app/components/{Tabs.tsx => tabs.tsx} | 0 app/components/tags/ExitNode.tsx | 52 +++--- app/components/tags/Expiry.tsx | 4 +- app/components/tags/HeadplaneAgent.tsx | 33 ++-- app/components/tags/Subnet.tsx | 52 +++--- app/components/tags/TailscaleSSH.tsx | 34 ++-- app/components/text.tsx | 12 ++ app/components/title.tsx | 12 ++ app/components/{Tooltip.tsx => tooltip.tsx} | 0 app/layout/link-account.tsx | 2 +- app/routes/acls/overview.tsx | 8 +- app/routes/auth/login/config-error.tsx | 4 +- app/routes/auth/login/logout.tsx | 23 ++- app/routes/auth/login/oidc-error.tsx | 103 ++++++----- app/routes/auth/login/page.tsx | 8 +- app/routes/dns/components/manage-domains.tsx | 6 +- app/routes/dns/components/manage-ns.tsx | 10 +- app/routes/dns/components/manage-records.tsx | 4 +- app/routes/dns/components/rename-tailnet.tsx | 16 +- app/routes/dns/components/toggle-magic.tsx | 6 +- app/routes/dns/dialogs/add-ns.tsx | 22 +-- app/routes/dns/dialogs/add-record.tsx | 37 ++-- app/routes/dns/overview.tsx | 4 +- app/routes/home.tsx | 2 +- .../machines/components/machine-row.tsx | 4 +- app/routes/machines/dialogs/delete.tsx | 6 +- app/routes/machines/dialogs/expire.tsx | 6 +- app/routes/machines/dialogs/move.tsx | 28 +-- app/routes/machines/dialogs/new.tsx | 32 ++-- app/routes/machines/dialogs/rename.tsx | 42 +---- app/routes/machines/dialogs/routes.tsx | 18 +- app/routes/machines/dialogs/tags.tsx | 32 ++-- app/routes/machines/machine.tsx | 10 +- app/routes/machines/overview.tsx | 6 +- .../settings/auth-keys/auth-key-row.tsx | 2 +- .../auth-keys/dialogs/add-auth-key.tsx | 62 +++---- .../auth-keys/dialogs/expire-auth-key.tsx | 6 +- app/routes/settings/auth-keys/overview.tsx | 48 +++-- .../restrictions/dialogs/add-domain.tsx | 12 +- .../restrictions/dialogs/add-group.tsx | 12 +- .../restrictions/dialogs/add-user.tsx | 12 +- app/routes/settings/restrictions/overview.tsx | 2 +- app/routes/settings/restrictions/table.tsx | 2 +- app/routes/ssh/user-prompt.tsx | 6 +- .../users/components/headplane-user-row.tsx | 2 +- .../users/components/headscale-user-row.tsx | 2 +- app/routes/users/dialogs/create-user.tsx | 44 +---- app/routes/users/dialogs/delete-user.tsx | 6 +- app/routes/users/dialogs/link-user.tsx | 8 +- app/routes/users/dialogs/reassign-user.tsx | 18 +- app/routes/users/dialogs/rename-user.tsx | 10 +- .../users/dialogs/transfer-ownership.tsx | 8 +- 75 files changed, 842 insertions(+), 1086 deletions(-) delete mode 100644 app/components/Input.tsx delete mode 100644 app/components/Notice.tsx delete mode 100644 app/components/NumberInput.tsx delete mode 100644 app/components/Popover.tsx delete mode 100644 app/components/RadioGroup.tsx delete mode 100644 app/components/Select.tsx delete mode 100644 app/components/Switch.tsx delete mode 100644 app/components/Text.tsx delete mode 100644 app/components/Title.tsx rename app/components/{Attribute.tsx => attribute.tsx} (98%) rename app/components/{Card.tsx => card.tsx} (88%) rename app/components/{Chip.tsx => chip.tsx} (100%) rename app/components/{Code.tsx => code.tsx} (100%) rename app/components/{Dialog.tsx => dialog.tsx} (100%) create mode 100644 app/components/input.tsx create mode 100644 app/components/notice.tsx create mode 100644 app/components/number-input.tsx create mode 100644 app/components/radio-group.tsx create mode 100644 app/components/select.tsx rename app/components/{StatusCircle.tsx => status-circle.tsx} (100%) create mode 100644 app/components/switch.tsx rename app/components/{TableList.tsx => table-list.tsx} (100%) rename app/components/{Tabs.tsx => tabs.tsx} (100%) create mode 100644 app/components/text.tsx create mode 100644 app/components/title.tsx rename app/components/{Tooltip.tsx => tooltip.tsx} (100%) diff --git a/app/components/Input.tsx b/app/components/Input.tsx deleted file mode 100644 index bbb608a..0000000 --- a/app/components/Input.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { Asterisk } from "lucide-react"; -import { useRef } from "react"; -import { type AriaTextFieldProps, useId, useTextField } from "react-aria"; - -import cn from "~/utils/cn"; - -export interface InputProps extends AriaTextFieldProps { - label: string; - labelHidden?: boolean; - isRequired?: boolean; - className?: string; - isInvalid?: boolean; - errorMessage?: string; -} - -export default function Input(props: InputProps) { - const { label, labelHidden, className, isInvalid: customIsInvalid, errorMessage } = props; - const ref = useRef(null); - const id = useId(props.id); - - const { - labelProps, - inputProps, - descriptionProps, - errorMessageProps, - isInvalid: ariaIsInvalid, - validationErrors, - } = useTextField( - { - ...props, - label, - "aria-label": label, - }, - ref, - ); - - const isInvalid = customIsInvalid ?? ariaIsInvalid; - - return ( -
- - - {props.description && ( -
- {props.description} -
- )} - {isInvalid ? ( -
- {errorMessage ?? validationErrors.join(" ")} -
- ) : null} -
- ); -} diff --git a/app/components/Notice.tsx b/app/components/Notice.tsx deleted file mode 100644 index 9200019..0000000 --- a/app/components/Notice.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { - CircleAlert, - CircleSlash2, - LucideProps, - TriangleAlert, -} from 'lucide-react'; -import React from 'react'; -import Card from '~/components/Card'; - -export interface NoticeProps { - children: React.ReactNode; - title?: string; - variant?: 'default' | 'error' | 'warning'; - icon?: React.ReactElement; -} - -export default function Notice({ - children, - title, - variant, - icon, -}: NoticeProps) { - return ( - -
- {title ? ( - {title} - ) : undefined} - {!variant && icon ? icon : iconForVariant(variant)} -
- {children} -
- ); -} - -function iconForVariant(variant?: 'default' | 'error' | 'warning') { - switch (variant) { - case 'error': - return ; - case 'warning': - return ; - default: - return ; - } -} diff --git a/app/components/NumberInput.tsx b/app/components/NumberInput.tsx deleted file mode 100644 index f520afd..0000000 --- a/app/components/NumberInput.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Minus, Plus } from "lucide-react"; -import { useRef } from "react"; -import { type AriaNumberFieldProps, useButton, useId, useLocale, useNumberField } from "react-aria"; -import { useNumberFieldState } from "react-stately"; - -import cn from "~/utils/cn"; - -export interface InputProps extends AriaNumberFieldProps { - isRequired?: boolean; - name?: string; -} - -export default function NumberInput(props: InputProps) { - const { label, name } = props; - const { locale } = useLocale(); - const state = useNumberFieldState({ ...props, locale }); - const ref = useRef(null); - const id = useId(props.id); - - const { - labelProps, - inputProps, - groupProps, - incrementButtonProps, - decrementButtonProps, - descriptionProps, - errorMessageProps, - isInvalid, - validationErrors, - } = useNumberField(props, state, ref); - - const decrRef = useRef(null); - const incrRef = useRef(null); - const { buttonProps: decrProps } = useButton(decrementButtonProps, decrRef); - const { buttonProps: incrProps } = useButton(incrementButtonProps, incrRef); - - return ( -
- -
- - - - -
- {props.description && ( -
- {props.description} -
- )} - {isInvalid && ( -
- {validationErrors.join(" ")} -
- )} -
- ); -} diff --git a/app/components/Popover.tsx b/app/components/Popover.tsx deleted file mode 100644 index 9a808f9..0000000 --- a/app/components/Popover.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { useRef } from "react"; -import { type AriaPopoverProps, DismissButton, Overlay, usePopover } from "react-aria"; -import type { OverlayTriggerState } from "react-stately"; - -import cn from "~/utils/cn"; - -export interface PopoverProps extends Omit { - children: React.ReactNode; - state: OverlayTriggerState; - popoverRef?: React.RefObject; - className?: string; -} - -export default function Popover(props: PopoverProps) { - const ref = props.popoverRef ?? useRef(null); - const { state, children, className } = props; - const { popoverProps, underlayProps } = usePopover( - { - ...props, - popoverRef: ref, - offset: 8, - }, - state, - ); - - return ( - -
-
- - {children} - -
- - ); -} diff --git a/app/components/RadioGroup.tsx b/app/components/RadioGroup.tsx deleted file mode 100644 index 67d2643..0000000 --- a/app/components/RadioGroup.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React, { createContext, useContext, useRef } from "react"; -import { AriaRadioGroupProps, AriaRadioProps, VisuallyHidden, useFocusRing } from "react-aria"; -import { useRadio, useRadioGroup } from "react-aria"; -import { RadioGroupState } from "react-stately"; -import { useRadioGroupState } from "react-stately"; - -import cn from "~/utils/cn"; - -interface RadioGroupProps extends AriaRadioGroupProps { - children: React.ReactElement[]; - label: string; - className?: string; -} - -const RadioContext = createContext(null); - -function RadioGroup({ children, label, className, ...props }: RadioGroupProps) { - const state = useRadioGroupState(props); - const { radioGroupProps, labelProps } = useRadioGroup( - { - ...props, - "aria-label": label, - }, - state, - ); - - return ( -
- - {label} - - {children} -
- ); -} - -interface RadioProps extends AriaRadioProps { - label: string; - className?: string; -} - -function Radio({ children, label, className, ...props }: RadioProps) { - const state = useContext(RadioContext); - const ref = useRef(null); - const { inputProps, isSelected, isDisabled } = useRadio( - { - ...props, - "aria-label": label, - }, - state!, - ref, - ); - const { isFocusVisible, focusProps } = useFocusRing(); - - return ( -