Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions app/components/ui/Field.parts.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof BaseField.Root>) {
return (
<BaseField.Root
{...mergeProps({ className: 'flex w-full flex-col gap-1.5' }, props)}
/>
);
}

export function Label(props: ComponentProps<typeof BaseField.Label>) {
return (
<BaseField.Label
{...mergeProps(
{ className: `${textVariantClasses['label.medium']} select-none text-bds-gray-60` },
props,
)}
/>
);
}

export function Description(props: ComponentProps<typeof BaseField.Description>) {
return (
<BaseField.Description
{...mergeProps(
{ className: `${textVariantClasses['label.regular']} text-bds-gray-40` },
props,
)}
/>
);
}

export function Error(props: ComponentProps<typeof BaseField.Error>) {
return (
<BaseField.Error
{...mergeProps(
{ className: `${textVariantClasses['label.regular']} text-bds-red-40` },
props,
)}
/>
);
}

export const Control = BaseField.Control;
export const Validity = BaseField.Validity;
export const Item = BaseField.Item;
1 change: 1 addition & 0 deletions app/components/ui/Field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as Field from './Field.parts';
28 changes: 28 additions & 0 deletions app/components/ui/Input.tsx
Original file line number Diff line number Diff line change
@@ -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 <BaseInput {...mergeProps({ className: inputClassName }, props)} />;
}
60 changes: 60 additions & 0 deletions app/components/ui/InputGroup.parts.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
{...mergeProps(
{
className: cn(
'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',
),
},
props,
{
role,
...(disabled && { 'aria-disabled': true as const }),
...(invalid && { 'aria-invalid': true as const }),
},
)}
/>
);
}

export function Control(props: InputProps) {
return (
<BaseInput
{...mergeProps(
{
className: cn(
'min-w-0 flex-1 bg-transparent py-2.5 outline-none',
textVariantClasses['label.regular'],
'text-bds-gray-100 placeholder:text-bds-gray-40',
'px-1 first:ps-2.5 last:pe-2.5',
'disabled:cursor-not-allowed',
),
},
props,
)}
/>
);
}
1 change: 1 addition & 0 deletions app/components/ui/InputGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as InputGroup from './InputGroup.parts';
31 changes: 31 additions & 0 deletions app/components/ui/Radio.parts.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof BaseRadio.Root>) {
return (
<BaseRadio.Root
value={props.value}
{...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-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',
),
},
props,
)}
/>
);
}

export const Indicator = BaseRadio.Indicator;
1 change: 1 addition & 0 deletions app/components/ui/Radio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as Radio from './Radio.parts';
12 changes: 12 additions & 0 deletions app/components/ui/RadioGroup.tsx
Original file line number Diff line number Diff line change
@@ -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 <BaseRadioGroup {...mergeProps({ className: cn('grid w-full gap-3') }, props)} />;
}
8 changes: 7 additions & 1 deletion app/components/ui/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,13 @@ export function Select({
<BaseSelect.Trigger
aria-label={ariaLabel}
className={cn(
'inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg border border-bds-gray-10 bg-background px-3.5 text-[14px] text-foreground outline-none transition-colors focus:border-foreground data-[placeholder]:text-bds-gray-40 disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/10 dark:bg-white/5 dark:text-white dark:focus:border-white',
'inline-flex h-10 w-full items-center justify-between gap-2 rounded-lg bg-transparent px-3.5 text-[14px] text-foreground outline-none',
'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-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,
)}
>
Expand Down
154 changes: 70 additions & 84 deletions app/snapshots/SnapshotsClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -271,11 +273,6 @@ export function SnapshotsClient({ snapshots }: SnapshotsClientProps) {
setPreset(match ? match.name : null);
}

function handlePresetClick(event: MouseEvent<HTMLButtonElement>) {
const value = event.currentTarget.dataset.preset as PresetName | undefined;
if (value) selectPreset(value);
}

function handleComponentClick(event: MouseEvent<HTMLButtonElement>) {
const value = event.currentTarget.dataset.name;
if (value) toggleComponent(value);
Expand Down Expand Up @@ -332,36 +329,29 @@ export function SnapshotsClient({ snapshots }: SnapshotsClientProps) {
Choose the Base network for your node.
</Text>
</div>
<div className="grid grid-cols-1 gap-3 lg:grid-cols-3">
{snapshots.map((snap) => {
const selected = snap.network === activeNetwork;
return (
<button
key={snap.network}
type="button"
onClick={() => {
trackSnapshotNetworkSelect(snap.network);
setNetwork(snap.network);
}}
className={cn(
'rounded-xl border border-bds-gray-10 px-4 py-3 text-left transition-[color,box-shadow] duration-150 ease-out',
selected
? 'border-transparent ring-2 ring-foreground'
: 'hover:border-bds-gray-15',
)}
>
<Text as="span" variant="label.medium" className="block">
{NETWORK_LABELS[snap.network] ?? snap.chainName}
</Text>
<Text as="span" variant="label.regular" tone="muted" className="mt-1 flex flex-wrap items-center gap-1.5">
<span>block {formatNumber(snap.block)}</span>
<span className="text-bds-gray-40">·</span>
<span>{formatDate(snap.date)}</span>
</Text>
</button>
);
})}
</div>
<RadioGroup
value={activeNetwork}
onValueChange={(next) => {
if (next == null) return;
trackSnapshotNetworkSelect(next);
setNetwork(next);
}}
aria-label="Select Network"
className="grid-cols-1 lg:grid-cols-3"
>
{snapshots.map((snap) => (
<Radio.Root key={snap.network} value={snap.network}>
<Text as="span" variant="label.medium" className="block">
{NETWORK_LABELS[snap.network] ?? snap.chainName}
</Text>
<Text as="span" variant="label.regular" tone="muted" className="flex flex-wrap items-center gap-1.5">
<span>block {formatNumber(snap.block)}</span>
<span className="text-bds-gray-40">·</span>
<span>{formatDate(snap.date)}</span>
</Text>
</Radio.Root>
))}
</RadioGroup>
</section>

<section className="mt-10">
Expand Down Expand Up @@ -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 (
<button
key={p.name}
type="button"
data-preset={p.name}
onClick={handlePresetClick}
aria-label={p.displayName}
className={cn(
'flex flex-col items-start rounded-xl border px-4 py-3.5 text-left transition-[color,box-shadow] duration-150 ease-out',
selected
? 'border-transparent ring-2 ring-foreground'
: 'border-bds-gray-10 hover:border-bds-gray-15',
)}
>
<div className="flex w-full items-baseline gap-2">
<Text as="span" variant="label.medium">
{p.displayName}
</Text>
<Text as="span" variant="footnote" tone="muted" className="font-mono">
{formatBytes(size)}
<RadioGroup
value={preset}
onValueChange={(next) => {
if (next == null) return;
selectPreset(next as PresetName);
}}
aria-label="Configure Snapshot"
className="grid-cols-1 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 includedComponents = displayComponents.filter((c) => {
if (c.name === 'state_history') return p.components.includes('account_changesets');
return p.components.includes(c.name);
});
return (
<Radio.Root key={p.name} value={p.name} aria-label={p.displayName} className="items-start py-3.5">
<div className="flex w-full items-baseline gap-2">
<Text as="span" variant="label.medium">
{p.displayName}
</Text>
<Text as="span" variant="footnote" tone="muted" className="font-mono">
{formatBytes(size)}
</Text>
</div>
<Text as="span" variant="label.regular" tone="muted" className="leading-[22px]">
{p.description}
</Text>
</div>
<Text as="span" variant="label.regular" tone="muted" className="mt-1 leading-[22px]">
{p.description}
</Text>
<div className="mt-3 flex w-full flex-col gap-1.5 border-t border-bds-gray-10 pt-3">
{includedComponents.map((c) => (
<div key={c.name} className="flex items-center gap-1.5">
<svg width={14} height={14} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className="shrink-0 text-brand-blue">
<path d="M20 6 9 17l-5-5" />
</svg>
<Text as="span" variant="label.regular">
{c.displayName}
</Text>
</div>
))}
</div>
</button>
);
})}
<div className="mt-2 flex w-full flex-col gap-1.5 border-t border-bds-gray-10 pt-3">
{includedComponents.map((c) => (
<div key={c.name} className="flex items-center gap-1.5">
<svg width={14} height={14} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className="shrink-0 text-brand-blue">
<path d="M20 6 9 17l-5-5" />
</svg>
<Text as="span" variant="label.regular">
{c.displayName}
</Text>
</div>
))}
</div>
</Radio.Root>
);
})}
</RadioGroup>
</motion.div>
) : (
<motion.div
Expand Down
Loading
Loading