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..a9beb583 --- /dev/null +++ b/app/components/ui/Input.tsx @@ -0,0 +1,28 @@ +'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-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-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', +); + +// 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 new file mode 100644 index 00000000..878138cf --- /dev/null +++ b/app/components/ui/InputGroup.parts.tsx @@ -0,0 +1,60 @@ +'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..244ad737 --- /dev/null +++ b/app/components/ui/Radio.parts.tsx @@ -0,0 +1,31 @@ +'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..023cfa33 --- /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/components/ui/Select.tsx b/app/components/ui/Select.tsx index 2b6ace7e..6a7c20dc 100644 --- a/app/components/ui/Select.tsx +++ b/app/components/ui/Select.tsx @@ -57,7 +57,13 @@ export function Select({ diff --git a/app/snapshots/SnapshotsClient.tsx b/app/snapshots/SnapshotsClient.tsx index 99de84a4..ae5ac697 100644 --- a/app/snapshots/SnapshotsClient.tsx +++ b/app/snapshots/SnapshotsClient.tsx @@ -7,6 +7,8 @@ import { Card } from '../components/ui/Card'; import { Checkbox } from '../components/ui/Checkbox'; import { cn } from '../components/ui/cn'; import { COPY_SQUARES_PATH_40 } from '../components/ui/icons'; +import { Radio } from '../components/ui/Radio'; +import { RadioGroup } from '../components/ui/RadioGroup'; import { Tabs } from '../components/ui/Tabs'; import { Text } from '../components/ui/Text'; @@ -271,11 +273,6 @@ export function SnapshotsClient({ snapshots }: SnapshotsClientProps) { setPreset(match ? match.name : null); } - function handlePresetClick(event: MouseEvent) { - 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/_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
-
+ + {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-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', )} > @@ -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-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', + )} > ) : inUse ? ( in use diff --git a/app/vibenet/demos/account/components/SessionKeyEditor.tsx b/app/vibenet/demos/account/components/SessionKeyEditor.tsx index 7180ce1e..687d6f03 100644 --- a/app/vibenet/demos/account/components/SessionKeyEditor.tsx +++ b/app/vibenet/demos/account/components/SessionKeyEditor.tsx @@ -10,7 +10,9 @@ import { useState } from 'react'; import { Button } from '../../../../components/ui/Button'; import { cn } from '../../../../components/ui/cn'; +import { Field } from '../../../../components/ui/Field'; import { CloseIcon } from '../../../../components/ui/icons'; +import { Input } from '../../../../components/ui/Input'; import { Select } from '../../../../components/ui/Select'; import { Text } from '../../../../components/ui/Text'; import { getDemoChain, DEMO_CHAINS } from '../library/chains'; @@ -29,8 +31,6 @@ import { short } from '../shared'; import { useAccountEngine } from '../useAccountEngine'; const ADDR_RE = /^0x[0-9a-fA-F]{40}$/; -const INPUT_CLS = - 'w-full rounded-lg border border-bds-gray-10 bg-bds-gray-0 px-3 py-2 text-[13px] 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 CHIP_CLS = 'rounded-full border border-bds-gray-10 px-2.5 py-1 text-[12px] text-bds-gray-60 transition-colors hover:border-bds-gray-15 dark:border-white/10 dark:text-bds-gray-40'; const CHIP_ON = 'border-base-blue bg-bds-blue-0 text-base-blue'; @@ -154,8 +154,8 @@ export function SessionKeyEditor({ Register a Session Key
- -