diff --git a/webui/src/components/BundleSelectForm.tsx b/webui/src/components/BundleSelectForm.tsx index b23fdd0..2eeeec0 100644 --- a/webui/src/components/BundleSelectForm.tsx +++ b/webui/src/components/BundleSelectForm.tsx @@ -1,63 +1,88 @@ -import * as Select from '@radix-ui/react-select'; -import { cx } from 'class-variance-authority'; import { useRouter } from 'expo-router'; -// @ts-expect-error -import ChevronDownIcon from 'lucide-react/dist/esm/icons/chevron-down'; +import { useMemo } from 'react'; -import { BundleTag } from '~/components/BundleTag'; +import { EnvironmentIcon } from '~/components/EnvironmentIcon'; +import { PlatformName } from '~/components/PlatformName'; import { useBundle } from '~/providers/bundle'; -import { Button } from '~/ui/Button'; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from '~/ui/Select'; import { relativeBundlePath } from '~/utils/bundle'; +import type { PartialAtlasBundle } from '~core/data/types'; export function BundleSelectForm() { const router = useRouter(); const { bundle, bundles } = useBundle(); + const bundlesByPlatform = useMemo(() => groupBundlesByPlatform(bundles), [bundles]); return ( - router.setParams({ bundle })}> - - - - - - - {bundles.map((item) => ( -
- - - -
- ))} -
-
-
-
+ + ); +} + +function groupBundlesByPlatform(bundles: PartialAtlasBundle[]) { + const groups: Record = { + android: [], + ios: [], + web: [], + unknown: [], + }; + + for (const bundle of bundles) { + if (groups[bundle.platform]) { + groups[bundle.platform]!.push(bundle); + } + } + + return Object.entries(groups).map( + ([platform, bundles]) => + [platform as PartialAtlasBundle['platform'], bundles.sort(sortBundlesByEnvironment)] as const ); } + +/** Sort all bundles by environment, in alphabetical order "client -> node -> react-server" */ +function sortBundlesByEnvironment(a: PartialAtlasBundle, b: PartialAtlasBundle) { + return a.environment.localeCompare(b.environment); +} diff --git a/webui/src/components/BundleTag.tsx b/webui/src/components/BundleTag.tsx index cde692d..04c0797 100644 --- a/webui/src/components/BundleTag.tsx +++ b/webui/src/components/BundleTag.tsx @@ -1,22 +1,25 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { type ComponentProps } from 'react'; +import { EnvironmentIcon } from '~/components/EnvironmentIcon'; +import { EnvironmentName } from '~/components/EnvironmentName'; +import { PlatformName } from '~/components/PlatformName'; import { Tag } from '~/ui/Tag'; import type { AtlasBundle } from '~core/data/types'; const bundleTagVariants = cva('', { variants: { platform: { - android: 'bg-palette-green3 text-palette-green11', - ios: 'bg-palette-blue3 text-palette-blue11', - web: 'bg-palette-orange3 text-palette-orange11', + android: 'bg-palette-green3', + ios: 'bg-palette-blue3', + web: 'bg-palette-orange3', unknown: '', - } satisfies typeof platformChildren, + } satisfies Record, environment: { client: '', - node: 'bg-palette-orange3 text-palette-orange11', - 'react-server': 'bg-palette-green3 text-palette-green11', - } satisfies typeof environmentChildren, + node: 'bg-palette-orange3', + 'react-server': 'bg-palette-orange3', + } satisfies Record, }, defaultVariants: { platform: 'unknown', // Default platform value, see MetroGraphSource @@ -24,19 +27,6 @@ const bundleTagVariants = cva('', { }, }); -const platformChildren: Record = { - android: 'Android', - ios: 'iOS', - web: 'Web', - unknown: '???', -}; - -const environmentChildren: Record = { - client: 'Client', - node: 'SSR', - 'react-server': 'RSC', -}; - type BundelTagProps = Omit< ComponentProps & VariantProps, 'children' @@ -49,22 +39,12 @@ export function BundleTag({ className, platform, environment, ...props }: Bundel variant="none" {...props} > - {getBundelTagChildren({ platform, environment })} + + + × + + + ); } - -function getBundelTagChildren(props: BundelTagProps) { - const children: string[] = []; - - if (props.platform) { - children.push(platformChildren[props.platform]); - } - - // Only add the environment specifier if it's not bundled for the client - if (props.environment && props.environment !== 'client') { - children.push(environmentChildren[props.environment]); - } - - return children.join(' × '); -} diff --git a/webui/src/components/EnvironmentIcon.tsx b/webui/src/components/EnvironmentIcon.tsx new file mode 100644 index 0000000..7726be9 --- /dev/null +++ b/webui/src/components/EnvironmentIcon.tsx @@ -0,0 +1,21 @@ +import type { LucideProps } from 'lucide-react'; + +import type { AtlasBundle } from '~core/data/types'; + +type EnvironmentIconProps = Omit< + LucideProps & { + environment: AtlasBundle['environment']; + }, + 'children' +>; + +const iconsByEnvironment: Record = { + client: require('lucide-react/dist/esm/icons/tablet-smartphone').default, + node: require('lucide-react/dist/esm/icons/hexagon').default, + 'react-server': require('lucide-react/dist/esm/icons/server').default, +}; + +export function EnvironmentIcon({ className, environment, ...props }: EnvironmentIconProps) { + const Icon = iconsByEnvironment[environment]; + return ; +} diff --git a/webui/src/components/EnvironmentName.tsx b/webui/src/components/EnvironmentName.tsx new file mode 100644 index 0000000..d070576 --- /dev/null +++ b/webui/src/components/EnvironmentName.tsx @@ -0,0 +1,18 @@ +import type { PropsWithChildren } from 'react'; + +import type { AtlasBundle } from '~core/data/types'; + +type EnvironmentNameProps = PropsWithChildren<{ + environment: AtlasBundle['environment']; + className?: string; +}>; + +export const environmentNames: Record = { + client: 'Client', + node: 'SSR', + 'react-server': 'RSC', +}; + +export function EnvironmentName({ children, environment, ...props }: EnvironmentNameProps) { + return {children || environmentNames[environment]}; +} diff --git a/webui/src/components/PlatformName.tsx b/webui/src/components/PlatformName.tsx new file mode 100644 index 0000000..455d47f --- /dev/null +++ b/webui/src/components/PlatformName.tsx @@ -0,0 +1,38 @@ +import { cva } from 'class-variance-authority'; +import type { PropsWithChildren } from 'react'; + +import type { AtlasBundle } from '~core/data/types'; + +type PlatformNameProps = PropsWithChildren<{ + platform: AtlasBundle['platform']; + className?: string; +}>; + +export const platformVariants = cva('', { + variants: { + platform: { + android: 'text-palette-green11', + ios: 'text-palette-blue11', + web: 'text-palette-orange11', + unknown: 'text-secondary', + } satisfies Record, + }, + defaultVariants: { + platform: 'unknown', + }, +}); + +export const platformNames: Record = { + android: 'Android', + ios: 'iOS', + web: 'Web', + unknown: 'Unknown', +}; + +export function PlatformName({ children, className, platform }: PlatformNameProps) { + return ( + + {children || platformNames[platform]} + + ); +} diff --git a/webui/src/ui/Select.tsx b/webui/src/ui/Select.tsx new file mode 100644 index 0000000..cf4a6a3 --- /dev/null +++ b/webui/src/ui/Select.tsx @@ -0,0 +1,154 @@ +// see: https://ui.shadcn.com/docs/components/select + +import * as SelectPrimitive from '@radix-ui/react-select'; +import { cx } from 'class-variance-authority'; +// @ts-expect-error +import CheckIcon from 'lucide-react/dist/esm/icons/check'; +// @ts-expect-error +import ChevronDownIcon from 'lucide-react/dist/esm/icons/chevron-down'; +// @ts-expect-error +import ChevronUpIcon from 'lucide-react/dist/esm/icons/chevron-up'; +import { type ComponentPropsWithoutRef, type ElementRef, forwardRef } from 'react'; + +export const Select = SelectPrimitive.Root; + +export const SelectGroup = SelectPrimitive.Group; + +export const SelectValue = SelectPrimitive.Value; + +export const SelectTrigger = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1', + className + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +export const SelectScrollUpButton = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +export const SelectScrollDownButton = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; + +export const SelectContent = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, children, position = 'popper', ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +export const SelectLabel = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +export const SelectItem = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +export const SelectSeparator = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName;