From 336908ad513b225335c63401dde25690bde36801 Mon Sep 17 00:00:00 2001 From: Max Barvian Date: Wed, 2 Sep 2026 15:58:35 -0700 Subject: [PATCH 1/6] init form components --- app/components/ui/Field.parts.tsx | 52 +++++++ app/components/ui/Field.tsx | 1 + app/components/ui/Input.tsx | 27 ++++ app/components/ui/InputGroup.parts.tsx | 59 +++++++ app/components/ui/InputGroup.tsx | 1 + app/components/ui/Radio.parts.tsx | 32 ++++ app/components/ui/Radio.tsx | 1 + app/components/ui/RadioGroup.tsx | 12 ++ .../account/components/CreateAccountModal.tsx | 144 +++++++++--------- 9 files changed, 254 insertions(+), 75 deletions(-) create mode 100644 app/components/ui/Field.parts.tsx create mode 100644 app/components/ui/Field.tsx create mode 100644 app/components/ui/Input.tsx create mode 100644 app/components/ui/InputGroup.parts.tsx create mode 100644 app/components/ui/InputGroup.tsx create mode 100644 app/components/ui/Radio.parts.tsx create mode 100644 app/components/ui/Radio.tsx create mode 100644 app/components/ui/RadioGroup.tsx diff --git a/app/components/ui/Field.parts.tsx b/app/components/ui/Field.parts.tsx new file mode 100644 index 00000000..e98f047a --- /dev/null +++ b/app/components/ui/Field.parts.tsx @@ -0,0 +1,52 @@ +'use client'; + +import type { ComponentProps } from 'react'; +import { Field as BaseField } from '@base-ui/react/field'; +import { mergeProps } from '@base-ui/react/merge-props'; + +import { textVariantClasses } from './Text'; + +export function Root(props: ComponentProps) { + return ( + + ); +} + +export function Label(props: ComponentProps) { + return ( + + ); +} + +export function Description(props: ComponentProps) { + return ( + + ); +} + +export function Error(props: ComponentProps) { + return ( + + ); +} + +export const Control = BaseField.Control; +export const Validity = BaseField.Validity; +export const Item = BaseField.Item; diff --git a/app/components/ui/Field.tsx b/app/components/ui/Field.tsx new file mode 100644 index 00000000..28e744fe --- /dev/null +++ b/app/components/ui/Field.tsx @@ -0,0 +1 @@ +export * as Field from './Field.parts'; diff --git a/app/components/ui/Input.tsx b/app/components/ui/Input.tsx new file mode 100644 index 00000000..a073cc99 --- /dev/null +++ b/app/components/ui/Input.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { Input as BaseInput, type InputProps } from '@base-ui/react/input'; +import { mergeProps } from '@base-ui/react/merge-props'; + +import { cn } from './cn'; +import { textVariantClasses } from './Text'; + +export type { InputProps }; + +export const inputClassName = cn( + 'w-full rounded-xl bg-transparent py-2.5 ps-2.5 pe-2.5 outline-none', + textVariantClasses['label.regular'], + 'text-bds-gray-100 placeholder:text-bds-gray-40', + 'ring-1 ring-inset ring-bds-gray-10', + 'transition-[box-shadow] duration-150 ease motion-reduce:transition-none', + 'data-[focused]:ring-2 data-[focused]:ring-bds-gray-100', + 'data-[invalid]:ring-bds-red-40 data-[invalid]:data-[focused]:ring-bds-red-40', + 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50', +); + +// Styled Base UI Input. Compose inside Field.Root so `data-focused` / +// `data-invalid` come from that Field context. Use InputGroup when the +// field has addons (e.g. a trailing button). +export function Input(props: InputProps) { + return ; +} diff --git a/app/components/ui/InputGroup.parts.tsx b/app/components/ui/InputGroup.parts.tsx new file mode 100644 index 00000000..ad71cc10 --- /dev/null +++ b/app/components/ui/InputGroup.parts.tsx @@ -0,0 +1,59 @@ +'use client'; + +import type { ComponentProps } from 'react'; +import { Input as BaseInput, type InputProps } from '@base-ui/react/input'; +import { mergeProps } from '@base-ui/react/merge-props'; + +import { cn } from './cn'; +import { textVariantClasses } from './Text'; + +type RootProps = ComponentProps<'div'> & { + // Same role set as react-aria-components Group: semantic group by default, + // `region` if it belongs in the page outline, `presentation` if visual only. + role?: 'group' | 'region' | 'presentation'; + disabled?: boolean; + invalid?: boolean; +}; + +export function Root({ role = 'group', disabled, invalid, ...props }: RootProps) { + return ( +
+ ); +} + +export function Control(props: InputProps) { + return ( + + ); +} diff --git a/app/components/ui/InputGroup.tsx b/app/components/ui/InputGroup.tsx new file mode 100644 index 00000000..3525034b --- /dev/null +++ b/app/components/ui/InputGroup.tsx @@ -0,0 +1 @@ +export * as InputGroup from './InputGroup.parts'; diff --git a/app/components/ui/Radio.parts.tsx b/app/components/ui/Radio.parts.tsx new file mode 100644 index 00000000..d037adf5 --- /dev/null +++ b/app/components/ui/Radio.parts.tsx @@ -0,0 +1,32 @@ +'use client'; + +import type { ComponentProps } from 'react'; +import { Radio as BaseRadio } from '@base-ui/react/radio'; +import { mergeProps } from '@base-ui/react/merge-props'; + +import { cn } from './cn'; + +export function Root(props: ComponentProps) { + return ( + + ); +} + +export const Indicator = BaseRadio.Indicator; diff --git a/app/components/ui/Radio.tsx b/app/components/ui/Radio.tsx new file mode 100644 index 00000000..78f248b6 --- /dev/null +++ b/app/components/ui/Radio.tsx @@ -0,0 +1 @@ +export * as Radio from './Radio.parts'; diff --git a/app/components/ui/RadioGroup.tsx b/app/components/ui/RadioGroup.tsx new file mode 100644 index 00000000..b781523c --- /dev/null +++ b/app/components/ui/RadioGroup.tsx @@ -0,0 +1,12 @@ +'use client'; + +import { RadioGroup as BaseRadioGroup, type RadioGroupProps } from '@base-ui/react/radio-group'; +import { mergeProps } from '@base-ui/react/merge-props'; + +import { cn } from './cn'; + +export type { RadioGroupProps }; + +export function RadioGroup(props: RadioGroupProps) { + return ; +} diff --git a/app/vibenet/demos/account/components/CreateAccountModal.tsx b/app/vibenet/demos/account/components/CreateAccountModal.tsx index 4de10acd..db34bac1 100644 --- a/app/vibenet/demos/account/components/CreateAccountModal.tsx +++ b/app/vibenet/demos/account/components/CreateAccountModal.tsx @@ -14,8 +14,13 @@ import { useEffect, useMemo, useState } from 'react'; import { Button } from '../../../../components/ui/Button'; import { cn } from '../../../../components/ui/cn'; -import { Text } from '../../../../components/ui/Text'; import { Drawer } from '../../../../components/ui/Drawer'; +import { Field } from '../../../../components/ui/Field'; +import { Input } from '../../../../components/ui/Input'; +import { InputGroup } from '../../../../components/ui/InputGroup'; +import { Radio } from '../../../../components/ui/Radio'; +import { RadioGroup } from '../../../../components/ui/RadioGroup'; +import { Text } from '../../../../components/ui/Text'; import { KIND_LABEL, short, signerIdentity, type CreateMode, type WalletSigner } from '../shared'; import { CheckIcon, KindBadge, TrashIcon } from '../../_shared/primitives'; import { actorPairs, normalizeSalt, randomHex32, sortActors, toStoredActor } from '../library/derive'; @@ -27,10 +32,15 @@ type CreateAccountModalProps = { onClose: () => void; }; -const MODES: ReadonlyArray = [ - ['default', 'Default', 'Simplest account setup'], - ['passkey', 'Passkey', 'Smart account · passkey'], - ['advanced', 'Advanced', 'Pick type, keys & salt'], +const MODES: ReadonlyArray<{ value: CreateMode; title: string; description: string }> = [ + { value: 'default', title: 'Default', description: 'Simplest account setup' }, + { value: 'passkey', title: 'Passkey', description: 'Smart account · passkey' }, + { value: 'advanced', title: 'Advanced', description: 'Pick type, keys & salt' }, +]; + +const ACCOUNT_MODELS = [ + { value: 'smart' as const, title: 'Smart Account', description: 'Counterfactual · keys + salt → address' }, + { value: 'eoa' as const, title: 'EOA', description: 'Your EOA · delegates to DefaultAccount' }, ]; export function CreateAccountModal({ open, onClose }: CreateAccountModalProps) { @@ -239,76 +249,54 @@ export function CreateAccountModal({ open, onClose }: CreateAccountModalProps) { } > - + -
- Account type -
- {MODES.map(([mode, title, hint]) => ( - + + Account type + + {MODES.map((option) => ( + + + {option.title} + + + {option.description} + + ))} -
-
+ + {createMode === 'advanced' ? ( <> -
- Account model -
- {( - [ - ['smart', 'Smart Account', 'Counterfactual · keys + salt → address'], - ['eoa', 'EOA', 'Your EOA · delegates to DefaultAccount'], - ] as const - ).map(([type, title, hint]) => ( - + + Account model + + {ACCOUNT_MODELS.map((option) => ( + + + {option.title} + + + {option.description} + + ))} -
-
+ + {modalType === 'smart' ? ( <> @@ -329,26 +317,25 @@ export function CreateAccountModal({ open, onClose }: CreateAccountModalProps) { if (s) setModalIds((prev) => [...prev, s.id]); }} /> - + + ) : ( onToggle(s)} className={cn( - 'flex flex-1 items-center gap-2 rounded-lg border px-2 py-2.5 text-left transition-colors', - on - ? 'border-foreground' - : 'border-bds-gray-10 hover:border-foreground dark:border-white/10 dark:hover:border-white', + 'flex flex-1 items-center gap-2 rounded-xl px-2 py-2.5 text-left outline-none', + 'ring-1 ring-inset ring-bds-gray-10', + 'transition-[box-shadow] duration-150 ease motion-reduce:transition-none', + on ? 'ring-2 ring-bds-gray-100' : 'hover:ring-bds-gray-20', + 'focus-visible:ring-2 focus-visible:ring-brand-blue', )} > @@ -458,14 +446,20 @@ function KeyPicker({ onClick={() => onDelete?.(s.id)} aria-label={`Delete key ${s.label}`} title="Delete unused key" - className="flex w-8 shrink-0 items-center justify-center rounded-lg border border-bds-gray-10 text-bds-gray-50 transition-colors hover:border-bds-red-40 hover:text-bds-red-60 dark:border-white/10" + className={cn( + 'flex w-8 shrink-0 items-center justify-center rounded-xl text-bds-gray-50 outline-none', + 'ring-1 ring-inset ring-bds-gray-10', + 'transition-[box-shadow,color] duration-150 ease motion-reduce:transition-none', + 'hover:text-bds-red-60 hover:ring-bds-red-40', + 'focus-visible:ring-2 focus-visible:ring-bds-gray-100', + )} > ) : inUse ? ( in use From 845d34497b79afd9faf0ceb33c512037b9b6cbdb Mon Sep 17 00:00:00 2001 From: Max Barvian Date: Wed, 2 Sep 2026 16:30:29 -0700 Subject: [PATCH 2/6] Update faucet --- app/vibenet/faucet/page.tsx | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/app/vibenet/faucet/page.tsx b/app/vibenet/faucet/page.tsx index 1dce1f81..f3f84d3b 100644 --- a/app/vibenet/faucet/page.tsx +++ b/app/vibenet/faucet/page.tsx @@ -1,13 +1,14 @@ 'use client'; import { useCallback, useEffect, useState } from 'react'; -import type { ChangeEvent, FormEvent, MouseEvent, ReactNode } from 'react'; +import type { FormEvent, MouseEvent, ReactNode } from 'react'; import Link from 'next/link'; import { trackFaucetRequest } from '../../analytics/events'; import { Button } from '../../components/ui/Button'; import { Card } from '../../components/ui/Card'; +import { Input } from '../../components/ui/Input'; import { Skeleton } from '../../components/ui/Skeleton'; import { Spinner } from '../../components/ui/Spinner'; import { cn } from '../../components/ui/cn'; @@ -117,8 +118,8 @@ export default function FaucetPage() { [runDrip], ); - const handleAddressChange = useCallback((event: ChangeEvent) => { - setAddress(event.target.value); + const handleAddressChange = useCallback((value: string) => { + setAddress(value); }, []); const validAddress = isAddress(address); @@ -225,19 +226,15 @@ export default function FaucetPage() {
- +
{FAUCET_TOKENS.map((token) => { const enabled = status ? token.isEnabled(status) : token.id === 'eth'; From 645ef8a729933d64cd71cc032484b31774d320b7 Mon Sep 17 00:00:00 2001 From: Max Barvian Date: Thu, 3 Sep 2026 10:12:48 -0700 Subject: [PATCH 3/6] more cleanup --- app/components/ui/Input.tsx | 12 +- app/components/ui/InputGroup.parts.tsx | 4 +- app/components/ui/Radio.parts.tsx | 6 +- app/components/ui/RadioGroup.tsx | 2 +- app/components/ui/Select.tsx | 6 +- app/vibenet/demos/_shared/AccountSwitcher.tsx | 5 +- .../demos/_shared/AddressAutocomplete.tsx | 15 +-- app/vibenet/demos/_shared/form.tsx | 78 +++--------- .../account/components/CreateAccountModal.tsx | 12 +- .../account/components/SessionKeyEditor.tsx | 40 +++--- .../account/components/TransactionModal.tsx | 116 ++++++++---------- .../demos/b20/components/CreatePolicy.tsx | 57 ++++++--- .../demos/b20/components/DeployModule.tsx | 111 ++++++++--------- .../address/[addr]/OwnedAccountView.tsx | 22 ++-- 14 files changed, 229 insertions(+), 257 deletions(-) diff --git a/app/components/ui/Input.tsx b/app/components/ui/Input.tsx index a073cc99..948fe79a 100644 --- a/app/components/ui/Input.tsx +++ b/app/components/ui/Input.tsx @@ -9,19 +9,19 @@ import { textVariantClasses } from './Text'; export type { InputProps }; export const inputClassName = cn( - 'w-full rounded-xl bg-transparent py-2.5 ps-2.5 pe-2.5 outline-none', + 'w-full rounded-lg bg-transparent py-2.5 ps-2.5 pe-2.5 outline-none', textVariantClasses['label.regular'], 'text-bds-gray-100 placeholder:text-bds-gray-40', 'ring-1 ring-inset ring-bds-gray-10', 'transition-[box-shadow] duration-150 ease motion-reduce:transition-none', - 'data-[focused]:ring-2 data-[focused]:ring-bds-gray-100', - 'data-[invalid]:ring-bds-red-40 data-[invalid]:data-[focused]:ring-bds-red-40', + 'focus:ring-2 focus:ring-bds-gray-100', + 'aria-invalid:ring-bds-red-40 data-[invalid]:ring-bds-red-40', + 'focus:aria-invalid:ring-bds-red-40', 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50', ); -// Styled Base UI Input. Compose inside Field.Root so `data-focused` / -// `data-invalid` come from that Field context. Use InputGroup when the -// field has addons (e.g. a trailing button). +// Styled Base UI Input. Use InputGroup when the field has addons +// (e.g. a trailing button). export function Input(props: InputProps) { return ; } diff --git a/app/components/ui/InputGroup.parts.tsx b/app/components/ui/InputGroup.parts.tsx index ad71cc10..f912b997 100644 --- a/app/components/ui/InputGroup.parts.tsx +++ b/app/components/ui/InputGroup.parts.tsx @@ -21,9 +21,9 @@ export function Root({ role = 'group', disabled, invalid, ...props }: RootProps) {...mergeProps( { className: cn( - 'flex items-center rounded-xl ring-1 ring-inset ring-bds-gray-10', + 'flex items-center rounded-lg ring-1 ring-inset ring-bds-gray-10', 'transition-[box-shadow] duration-150 ease motion-reduce:transition-none', - 'has-[input[data-focused]]:ring-2 has-[input[data-focused]]:ring-bds-gray-100', + 'has-[input:focus]:ring-2 has-[input:focus]:ring-bds-gray-100', 'aria-invalid:ring-bds-red-40 has-[input[data-invalid]]:ring-bds-red-40', 'aria-disabled:opacity-50', ), diff --git a/app/components/ui/Radio.parts.tsx b/app/components/ui/Radio.parts.tsx index d037adf5..a451482e 100644 --- a/app/components/ui/Radio.parts.tsx +++ b/app/components/ui/Radio.parts.tsx @@ -13,13 +13,11 @@ export function Root(props: ComponentProps) { {...mergeProps( { className: cn( - 'flex min-w-0 w-full cursor-pointer flex-col gap-1 rounded-xl p-3 text-left select-none outline-none', + 'flex min-w-0 w-full cursor-pointer flex-col gap-1 rounded-lg p-3 text-left select-none outline-none', 'ring-1 ring-inset ring-bds-gray-10', - 'transition-[box-shadow] duration-150 ease motion-reduce:transition-none', 'data-[checked]:ring-2 data-[checked]:ring-bds-gray-100', 'data-[hovered]:ring-bds-gray-20', - 'focus-visible:ring-2 focus-visible:ring-brand-blue', - 'data-[checked]:focus-visible:ring-brand-blue', + 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-brand-blue', 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50', ), }, diff --git a/app/components/ui/RadioGroup.tsx b/app/components/ui/RadioGroup.tsx index b781523c..023cfa33 100644 --- a/app/components/ui/RadioGroup.tsx +++ b/app/components/ui/RadioGroup.tsx @@ -8,5 +8,5 @@ import { cn } from './cn'; export type { RadioGroupProps }; export function RadioGroup(props: RadioGroupProps) { - return ; + return ; } diff --git a/app/components/ui/Select.tsx b/app/components/ui/Select.tsx index 2b6ace7e..3ab8bf31 100644 --- a/app/components/ui/Select.tsx +++ b/app/components/ui/Select.tsx @@ -57,7 +57,11 @@ export function Select({ diff --git a/app/vibenet/demos/_shared/AccountSwitcher.tsx b/app/vibenet/demos/_shared/AccountSwitcher.tsx index 71da24e4..2b6cc941 100644 --- a/app/vibenet/demos/_shared/AccountSwitcher.tsx +++ b/app/vibenet/demos/_shared/AccountSwitcher.tsx @@ -124,7 +124,10 @@ export function AccountSwitcher({ > diff --git a/app/vibenet/demos/_shared/AddressAutocomplete.tsx b/app/vibenet/demos/_shared/AddressAutocomplete.tsx index 5ab143b7..fe421607 100644 --- a/app/vibenet/demos/_shared/AddressAutocomplete.tsx +++ b/app/vibenet/demos/_shared/AddressAutocomplete.tsx @@ -9,6 +9,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { cn } from '../../../components/ui/cn'; +import { InputGroup } from '../../../components/ui/InputGroup'; import { short } from '../account/shared'; export type AddressBookEntry = { label: string; address: string }; @@ -74,15 +75,15 @@ export function AddressAutocomplete({ return (
-
- {tag ? {tag} : null} - + {tag ? {tag} : null} + { - onChange(e.target.value); + onValueChange={(next) => { + onChange(next); setOpen(true); }} onFocus={() => setOpen(true)} @@ -90,7 +91,7 @@ export function AddressAutocomplete({ if (e.key === 'Escape') setOpen(false); }} /> -
+ {open && matches.length > 0 && rect && typeof document !== 'undefined' ? createPortal(
) { - return ( - - ); -} +export { Input } from '../../../components/ui/Input'; +// Demo Field keeps the `label` / `hint` / `help` shorthand used across B20. +// The chrome is the shared Field parts; help stays a tooltip beside the label +// because that trigger is a button and must not nest inside
- Policy type -
- {([ - ['allowlist', 'Allowlist', 'Only member wallets pass.'], - ['blocklist', 'Blocklist', 'Every wallet except listed members passes.'], - ['advanced', 'Advanced', 'Combine existing policies.'], - ] as const).map(([value, title, body]) => { - const selected = value === 'advanced' ? mode === 'composite' : mode === 'simple' && simpleKind === value; - return ( - - ); - })} -
+ + selectType(next as SimplePolicyKind | 'advanced')} + className="grid-cols-1 sm:grid-cols-3" + > + {( + [ + ['allowlist', 'Allowlist', 'Only member wallets pass.'], + ['blocklist', 'Blocklist', 'Every wallet except listed members passes.'], + ['advanced', 'Advanced', 'Combine existing policies.'], + ] as const + ).map(([value, title, body]) => ( + + + {title} + + + {body} + + + ))} + +
{mode === 'simple' ? (
@@ -227,7 +239,22 @@ export function CreatePolicy({ />
-
{compositeKinds.map((item) => )}
+
+ + {compositeKinds.map((item) => ( + + + {policyKindLabel(item)} + + + {item === 'union' + ? 'UNION · A wallet passes when any child allows it.' + : 'INTERSECT · A wallet passes only when every child allows it.'} + + + ))} + +
{children.map((child, index) => { // Keep this row's current selection in the option list so the Select diff --git a/app/vibenet/demos/b20/components/DeployModule.tsx b/app/vibenet/demos/b20/components/DeployModule.tsx index e8e1ed13..15b2f21b 100644 --- a/app/vibenet/demos/b20/components/DeployModule.tsx +++ b/app/vibenet/demos/b20/components/DeployModule.tsx @@ -5,7 +5,8 @@ import { useState } from 'react'; import { encodeFunctionData, type Address, type Hex } from 'viem'; import { Button } from '../../../../components/ui/Button'; -import { cn } from '../../../../components/ui/cn'; +import { Radio } from '../../../../components/ui/Radio'; +import { RadioGroup } from '../../../../components/ui/RadioGroup'; import { Text } from '../../../../components/ui/Text'; import { CopyableValue } from '../../../components/CopyableValue'; import { formatTokenAmount, short } from '../../account/shared'; @@ -239,71 +240,59 @@ export function DeployModule({
- - Token type - -
- {( - [ - ['asset', 'Asset', 'Flexible decimals, announcements, and displayed-balance changes.'], - ['stablecoin', 'Stablecoin', 'A currency-linked token: always six decimals and a currency code.'], - ['advanced', 'Advanced', 'Set a supply cap, salt, and a token info link.'], - ] as const - ).map(([value, title, body]) => { - const selected = value === 'advanced' ? advanced : !advanced && variant === value; - return ( - - ); - })} -
-
- {advanced ? ( -
- - Token variant - -
+ + { + if (next === 'advanced') { + setAdvanced(true); + } else { + setAdvanced(false); + setVariant(next as 'asset' | 'stablecoin'); + } + }} + className="grid-cols-1 sm:grid-cols-3" + > {( [ - ['asset', 'Asset', 'Configurable decimals and announcements.'], - ['stablecoin', 'Stablecoin', 'Fixed six decimals and a currency code.'], + ['asset', 'Asset', 'Flexible decimals, announcements, and displayed-balance changes.'], + ['stablecoin', 'Stablecoin', 'A currency-linked token: always six decimals and a currency code.'], + ['advanced', 'Advanced', 'Set a supply cap, salt, and a token info link.'], ] as const ).map(([value, title, body]) => ( - + + + {title} + + + {body} + + ))} -
+ + +
+ {advanced ? ( +
+ + + {( + [ + ['asset', 'Asset', 'Configurable decimals and announcements.'], + ['stablecoin', 'Stablecoin', 'Fixed six decimals and a currency code.'], + ] as const + ).map(([value, title, body]) => ( + + + {title} + + + {body} + + + ))} + +
) : null}
diff --git a/app/vibenet/explorer/address/[addr]/OwnedAccountView.tsx b/app/vibenet/explorer/address/[addr]/OwnedAccountView.tsx index dbf15432..63813d23 100644 --- a/app/vibenet/explorer/address/[addr]/OwnedAccountView.tsx +++ b/app/vibenet/explorer/address/[addr]/OwnedAccountView.tsx @@ -15,6 +15,8 @@ import { useRouter } from 'next/navigation'; import { Button } from '../../../../components/ui/Button'; import { Card } from '../../../../components/ui/Card'; import { cn } from '../../../../components/ui/cn'; +import { Field } from '../../../../components/ui/Field'; +import { Input } from '../../../../components/ui/Input'; import { Modal } from '../../../../components/ui/Modal'; import { Select } from '../../../../components/ui/Select'; import { Spinner } from '../../../../components/ui/Spinner'; @@ -40,9 +42,6 @@ import { AccountShell, useSectionParam, type ShellSection } from './AccountShell import { ActivityTable } from './ActivityTable'; import { AssetsCard } from './AssetsCard'; -const INPUT_CLS = - 'w-full rounded-lg border border-bds-gray-10 bg-background px-3.5 py-2.5 text-[14px] outline-none transition-colors placeholder:text-bds-gray-40 focus:border-foreground dark:border-white/10 dark:bg-white/5 dark:focus:border-white/40'; - const SECTIONS: ShellSection[] = [ { id: 'overview', label: 'Overview' }, { id: 'owners', label: 'Owners' }, @@ -526,14 +525,13 @@ function SubAccountsSection() { } > - - - A delegated account with its own address, controlled by this account. It deploys on first use. - + + A delegated account with its own address, controlled by this account. It deploys on first use. + +
); From 7dfce2532b88da02d42d4446a90442629d122bf4 Mon Sep 17 00:00:00 2001 From: Max Barvian Date: Thu, 3 Sep 2026 13:14:34 -0700 Subject: [PATCH 4/6] outline instead of ring or border --- app/components/ui/Input.tsx | 13 +- app/components/ui/InputGroup.parts.tsx | 9 +- app/components/ui/Radio.parts.tsx | 13 +- app/components/ui/Select.tsx | 9 +- app/snapshots/SnapshotsClient.tsx | 154 ++++++++---------- app/vibenet/demos/_shared/AccountSwitcher.tsx | 9 +- .../account/components/CreateAccountModal.tsx | 25 ++- 7 files changed, 112 insertions(+), 120 deletions(-) diff --git a/app/components/ui/Input.tsx b/app/components/ui/Input.tsx index 948fe79a..f9f368e1 100644 --- a/app/components/ui/Input.tsx +++ b/app/components/ui/Input.tsx @@ -9,14 +9,15 @@ import { textVariantClasses } from './Text'; export type { InputProps }; export const inputClassName = cn( - 'w-full rounded-lg bg-transparent py-2.5 ps-2.5 pe-2.5 outline-none', + 'w-full rounded-lg bg-transparent py-2.5 ps-2.5 pe-2.5', textVariantClasses['label.regular'], 'text-bds-gray-100 placeholder:text-bds-gray-40', - 'ring-1 ring-inset ring-bds-gray-10', - 'transition-[box-shadow] duration-150 ease motion-reduce:transition-none', - 'focus:ring-2 focus:ring-bds-gray-100', - 'aria-invalid:ring-bds-red-40 data-[invalid]:ring-bds-red-40', - 'focus:aria-invalid:ring-bds-red-40', + 'outline outline-1 outline-offset-[-1px] outline-bds-gray-10', + 'transition-[outline-color] duration-150 ease-out motion-reduce:transition-none', + 'hover:outline-bds-gray-15', + 'focus:outline-2 focus:outline-foreground', + 'aria-invalid:outline-bds-red-40 data-[invalid]:outline-bds-red-40', + 'focus:aria-invalid:outline-bds-red-40', 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50', ); diff --git a/app/components/ui/InputGroup.parts.tsx b/app/components/ui/InputGroup.parts.tsx index f912b997..ce44273c 100644 --- a/app/components/ui/InputGroup.parts.tsx +++ b/app/components/ui/InputGroup.parts.tsx @@ -21,10 +21,11 @@ export function Root({ role = 'group', disabled, invalid, ...props }: RootProps) {...mergeProps( { className: cn( - 'flex items-center rounded-lg ring-1 ring-inset ring-bds-gray-10', - 'transition-[box-shadow] duration-150 ease motion-reduce:transition-none', - 'has-[input:focus]:ring-2 has-[input:focus]:ring-bds-gray-100', - 'aria-invalid:ring-bds-red-40 has-[input[data-invalid]]:ring-bds-red-40', + 'flex items-center rounded-lg outline outline-1 outline-offset-[-1px] outline-bds-gray-10', + 'transition-[outline-color] duration-150 ease-out motion-reduce:transition-none', + 'hover:outline-bds-gray-15', + 'has-[input:focus]:outline-2 has-[input:focus]:outline-foreground', + 'aria-invalid:outline-bds-red-40 has-[input[data-invalid]]:outline-bds-red-40', 'aria-disabled:opacity-50', ), }, diff --git a/app/components/ui/Radio.parts.tsx b/app/components/ui/Radio.parts.tsx index a451482e..5f4e2b2f 100644 --- a/app/components/ui/Radio.parts.tsx +++ b/app/components/ui/Radio.parts.tsx @@ -13,11 +13,14 @@ export function Root(props: ComponentProps) { {...mergeProps( { className: cn( - 'flex min-w-0 w-full cursor-pointer flex-col gap-1 rounded-lg p-3 text-left select-none outline-none', - 'ring-1 ring-inset ring-bds-gray-10', - 'data-[checked]:ring-2 data-[checked]:ring-bds-gray-100', - 'data-[hovered]:ring-bds-gray-20', - 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-brand-blue', + 'flex min-w-0 w-full cursor-pointer flex-col gap-1 rounded-xl px-4 py-3 text-left select-none', + 'outline outline-1 outline-offset-[-1px] outline-bds-gray-10', + 'transition-[outline-color] duration-150 ease-out', + 'data-[checked]:outline-2 data-[checked]:outline-foreground', + 'data-[unchecked]:data-[hovered]:outline-bds-gray-15', + 'focus:outline-2 focus:outline-foreground', + 'focus-visible:outline-2 focus-visible:outline-brand-blue', + 'data-[checked]:focus-visible:outline-brand-blue', 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50', ), }, diff --git a/app/components/ui/Select.tsx b/app/components/ui/Select.tsx index 3ab8bf31..f912c98e 100644 --- a/app/components/ui/Select.tsx +++ b/app/components/ui/Select.tsx @@ -57,10 +57,11 @@ export function Select({ ) { - const value = event.currentTarget.dataset.preset as PresetName | undefined; - if (value) selectPreset(value); - } - function handleComponentClick(event: MouseEvent) { const value = event.currentTarget.dataset.name; if (value) toggleComponent(value); @@ -332,36 +329,29 @@ export function SnapshotsClient({ snapshots }: SnapshotsClientProps) { Choose the Base network for your node.
-
- {snapshots.map((snap) => { - const selected = snap.network === activeNetwork; - return ( - - ); - })} -
+ { + if (next == null) return; + trackSnapshotNetworkSelect(next); + setNetwork(next); + }} + aria-label="Select Network" + className="grid-cols-1 lg:grid-cols-3" + > + {snapshots.map((snap) => ( + + + {NETWORK_LABELS[snap.network] ?? snap.chainName} + + + block {formatNumber(snap.block)} + · + {formatDate(snap.date)} + + + ))} +
@@ -390,57 +380,53 @@ export function SnapshotsClient({ snapshots }: SnapshotsClientProps) { animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15, ease: 'easeOut' }} - className="grid grid-cols-1 gap-3 lg:grid-cols-3" > - {PRESETS.map((p) => { - const size = activeSnapshot.components - .filter((c) => p.components.includes(c.name)) - .reduce((sum, c) => sum + c.size, 0); - const selected = preset === p.name; - const includedComponents = displayComponents.filter((c) => { - if (c.name === 'state_history') return p.components.includes('account_changesets'); - return p.components.includes(c.name); - }); - return ( - - ); - })} +
+ {includedComponents.map((c) => ( +
+ + + + + {c.displayName} + +
+ ))} +
+ + ); + })} + ) : ( diff --git a/app/vibenet/demos/account/components/CreateAccountModal.tsx b/app/vibenet/demos/account/components/CreateAccountModal.tsx index f79a58d7..9778fe3a 100644 --- a/app/vibenet/demos/account/components/CreateAccountModal.tsx +++ b/app/vibenet/demos/account/components/CreateAccountModal.tsx @@ -424,12 +424,12 @@ function KeyPicker({ type="button" onClick={() => onToggle(s)} className={cn( - 'flex flex-1 items-center gap-2 rounded-lg px-2 py-2.5 text-left outline-none', - 'ring-1 ring-inset ring-bds-gray-10', - 'transition-[box-shadow] duration-150 ease motion-reduce:transition-none', - on ? 'ring-2 ring-bds-gray-100' : 'hover:ring-bds-gray-20', - 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-brand-blue', - 'focus-visible:transition-none', + 'flex flex-1 items-center gap-2 rounded-lg px-2 py-2.5 text-left', + 'outline outline-1 outline-offset-[-1px] outline-bds-gray-10', + 'transition-[outline-color] duration-150 ease-out motion-reduce:transition-none', + on ? 'outline-2 outline-foreground' : 'hover:outline-bds-gray-15', + 'focus:outline-2 focus:outline-foreground', + 'focus-visible:outline-2 focus-visible:outline-brand-blue', )} > @@ -448,12 +448,11 @@ function KeyPicker({ aria-label={`Delete key ${s.label}`} title="Delete unused key" className={cn( - 'flex w-8 shrink-0 items-center justify-center rounded-lg text-bds-gray-50 outline-none', - 'ring-1 ring-inset ring-bds-gray-10', - 'transition-[box-shadow,color] duration-150 ease motion-reduce:transition-none', - 'hover:text-bds-red-60 hover:ring-bds-red-40', - 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-bds-gray-100', - 'focus-visible:transition-none', + 'flex w-8 shrink-0 items-center justify-center rounded-lg text-bds-gray-50', + 'outline outline-1 outline-offset-[-1px] outline-bds-gray-10', + 'transition-[color,outline-color] duration-150 ease-out motion-reduce:transition-none', + 'hover:text-bds-red-60 hover:outline-bds-red-40', + 'focus-visible:outline-2 focus-visible:outline-bds-gray-100', )} > @@ -461,7 +460,7 @@ function KeyPicker({ ) : inUse ? ( in use From 5723284ad8ae88e78e7322e6d63491e19e89ed7a Mon Sep 17 00:00:00 2001 From: Max Barvian Date: Thu, 3 Sep 2026 13:52:39 -0700 Subject: [PATCH 5/6] inset ring again --- app/components/ui/Input.tsx | 14 +++++------ app/components/ui/InputGroup.parts.tsx | 10 ++++---- app/components/ui/Radio.parts.tsx | 14 +++++------ app/components/ui/Select.tsx | 10 ++++---- app/vibenet/demos/_shared/AccountSwitcher.tsx | 10 ++++---- .../account/components/CreateAccountModal.tsx | 23 +++++++++---------- 6 files changed, 39 insertions(+), 42 deletions(-) diff --git a/app/components/ui/Input.tsx b/app/components/ui/Input.tsx index f9f368e1..a9beb583 100644 --- a/app/components/ui/Input.tsx +++ b/app/components/ui/Input.tsx @@ -9,15 +9,15 @@ import { textVariantClasses } from './Text'; export type { InputProps }; export const inputClassName = cn( - 'w-full rounded-lg bg-transparent py-2.5 ps-2.5 pe-2.5', + 'w-full rounded-lg bg-transparent py-2.5 ps-2.5 pe-2.5 outline-none', textVariantClasses['label.regular'], 'text-bds-gray-100 placeholder:text-bds-gray-40', - 'outline outline-1 outline-offset-[-1px] outline-bds-gray-10', - 'transition-[outline-color] duration-150 ease-out motion-reduce:transition-none', - 'hover:outline-bds-gray-15', - 'focus:outline-2 focus:outline-foreground', - 'aria-invalid:outline-bds-red-40 data-[invalid]:outline-bds-red-40', - 'focus:aria-invalid:outline-bds-red-40', + 'ring-1 ring-inset ring-bds-gray-10', + 'transition-[box-shadow] duration-150 ease-out motion-reduce:transition-none', + 'hover:ring-bds-gray-15', + 'focus:ring-2 focus:ring-foreground', + 'aria-invalid:ring-bds-red-40 data-[invalid]:ring-bds-red-40', + 'focus:aria-invalid:ring-bds-red-40', 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50', ); diff --git a/app/components/ui/InputGroup.parts.tsx b/app/components/ui/InputGroup.parts.tsx index ce44273c..878138cf 100644 --- a/app/components/ui/InputGroup.parts.tsx +++ b/app/components/ui/InputGroup.parts.tsx @@ -21,11 +21,11 @@ export function Root({ role = 'group', disabled, invalid, ...props }: RootProps) {...mergeProps( { className: cn( - 'flex items-center rounded-lg outline outline-1 outline-offset-[-1px] outline-bds-gray-10', - 'transition-[outline-color] duration-150 ease-out motion-reduce:transition-none', - 'hover:outline-bds-gray-15', - 'has-[input:focus]:outline-2 has-[input:focus]:outline-foreground', - 'aria-invalid:outline-bds-red-40 has-[input[data-invalid]]:outline-bds-red-40', + 'flex items-center rounded-lg ring-1 ring-inset ring-bds-gray-10', + 'transition-[box-shadow] duration-150 ease-out motion-reduce:transition-none', + 'hover:ring-bds-gray-15', + 'has-[input:focus]:ring-2 has-[input:focus]:ring-foreground', + 'aria-invalid:ring-bds-red-40 has-[input[data-invalid]]:ring-bds-red-40', 'aria-disabled:opacity-50', ), }, diff --git a/app/components/ui/Radio.parts.tsx b/app/components/ui/Radio.parts.tsx index 5f4e2b2f..244ad737 100644 --- a/app/components/ui/Radio.parts.tsx +++ b/app/components/ui/Radio.parts.tsx @@ -13,14 +13,12 @@ export function Root(props: ComponentProps) { {...mergeProps( { className: cn( - 'flex min-w-0 w-full cursor-pointer flex-col gap-1 rounded-xl px-4 py-3 text-left select-none', - 'outline outline-1 outline-offset-[-1px] outline-bds-gray-10', - 'transition-[outline-color] duration-150 ease-out', - 'data-[checked]:outline-2 data-[checked]:outline-foreground', - 'data-[unchecked]:data-[hovered]:outline-bds-gray-15', - 'focus:outline-2 focus:outline-foreground', - 'focus-visible:outline-2 focus-visible:outline-brand-blue', - 'data-[checked]:focus-visible:outline-brand-blue', + 'flex min-w-0 w-full cursor-pointer flex-col gap-1 rounded-xl px-4 py-3 text-left select-none outline-none', + 'ring-1 ring-inset ring-bds-gray-10', + 'transition-[box-shadow] duration-150 ease-out motion-reduce:transition-none', + 'data-[checked]:ring-2 data-[checked]:ring-foreground', + 'data-[unchecked]:data-[hovered]:ring-bds-gray-15', + 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-brand-blue', 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50', ), }, diff --git a/app/components/ui/Select.tsx b/app/components/ui/Select.tsx index f912c98e..1eab96c3 100644 --- a/app/components/ui/Select.tsx +++ b/app/components/ui/Select.tsx @@ -57,11 +57,11 @@ export function Select({ diff --git a/app/vibenet/demos/account/components/CreateAccountModal.tsx b/app/vibenet/demos/account/components/CreateAccountModal.tsx index 9778fe3a..37979974 100644 --- a/app/vibenet/demos/account/components/CreateAccountModal.tsx +++ b/app/vibenet/demos/account/components/CreateAccountModal.tsx @@ -424,12 +424,11 @@ function KeyPicker({ type="button" onClick={() => onToggle(s)} className={cn( - 'flex flex-1 items-center gap-2 rounded-lg px-2 py-2.5 text-left', - 'outline outline-1 outline-offset-[-1px] outline-bds-gray-10', - 'transition-[outline-color] duration-150 ease-out motion-reduce:transition-none', - on ? 'outline-2 outline-foreground' : 'hover:outline-bds-gray-15', - 'focus:outline-2 focus:outline-foreground', - 'focus-visible:outline-2 focus-visible:outline-brand-blue', + 'flex flex-1 items-center gap-2 rounded-lg px-2 py-2.5 text-left outline-none', + 'ring-1 ring-inset ring-bds-gray-10', + 'transition-[box-shadow] duration-150 ease-out motion-reduce:transition-none', + on ? 'ring-2 ring-foreground' : 'hover:ring-bds-gray-15', + 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-brand-blue', )} > @@ -448,11 +447,11 @@ function KeyPicker({ aria-label={`Delete key ${s.label}`} title="Delete unused key" className={cn( - 'flex w-8 shrink-0 items-center justify-center rounded-lg text-bds-gray-50', - 'outline outline-1 outline-offset-[-1px] outline-bds-gray-10', - 'transition-[color,outline-color] duration-150 ease-out motion-reduce:transition-none', - 'hover:text-bds-red-60 hover:outline-bds-red-40', - 'focus-visible:outline-2 focus-visible:outline-bds-gray-100', + 'flex w-8 shrink-0 items-center justify-center rounded-lg text-bds-gray-50 outline-none', + 'ring-1 ring-inset ring-bds-gray-10', + 'transition-[color,box-shadow] duration-150 ease-out motion-reduce:transition-none', + 'hover:text-bds-red-60 hover:ring-bds-red-40', + 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-bds-gray-100', )} > @@ -460,7 +459,7 @@ function KeyPicker({ ) : inUse ? ( in use From b1dc9f70fa499503c85511a1b79303441190630c Mon Sep 17 00:00:00 2001 From: Max Barvian Date: Thu, 3 Sep 2026 14:01:19 -0700 Subject: [PATCH 6/6] focus-visible outline on select --- app/components/ui/Select.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/ui/Select.tsx b/app/components/ui/Select.tsx index 1eab96c3..6a7c20dc 100644 --- a/app/components/ui/Select.tsx +++ b/app/components/ui/Select.tsx @@ -62,6 +62,7 @@ export function Select({ 'transition-[box-shadow] duration-150 ease-out motion-reduce:transition-none', 'hover:ring-bds-gray-15', 'focus:ring-black/40 data-[popup-open]:ring-black/40 dark:focus:ring-white/40 dark:data-[popup-open]:ring-white/40', + 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-brand-blue', 'data-[placeholder]:text-bds-gray-40 disabled:cursor-not-allowed disabled:opacity-50', className, )}