From aa12b32f00934311c1085a954ecdf51d886a7319 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Sun, 19 Jul 2026 15:43:54 -0500 Subject: [PATCH 1/2] add ItemDescription for combobox/listbox secondary text: shared color + 2-line clamp --- app/components/AttachFloatingIpModal.tsx | 13 ++++++------ app/components/PoolListboxItem.tsx | 5 ++--- .../form/fields/ImageSelectField.tsx | 3 ++- app/forms/disk-create.tsx | 9 +++++---- app/forms/instance-create.tsx | 13 ++++++------ app/pages/system/networking/IpPoolPage.tsx | 7 ++----- app/ui/lib/ItemDescription.tsx | 20 +++++++++++++++++++ 7 files changed, 43 insertions(+), 27 deletions(-) create mode 100644 app/ui/lib/ItemDescription.tsx diff --git a/app/components/AttachFloatingIpModal.tsx b/app/components/AttachFloatingIpModal.tsx index ca4f2a91a2..c9d60de4d3 100644 --- a/app/components/AttachFloatingIpModal.tsx +++ b/app/components/AttachFloatingIpModal.tsx @@ -20,6 +20,7 @@ import { import { ListboxField } from '~/components/form/fields/ListboxField' import { HL } from '~/components/HL' import { addToast } from '~/stores/toast' +import { ItemDescription } from '~/ui/lib/ItemDescription' import { Message } from '~/ui/lib/Message' import { Slash } from '~/ui/lib/Slash' @@ -50,20 +51,18 @@ function IpPoolName({ ipPoolId }: { ipPoolId: string }) { function FloatingIpLabel({ fip }: { fip: FloatingIp }) { return ( -
+
{fip.name}
-
-
{fip.ip}
+ + {fip.ip} {fip.description && ( <> -
- {fip.description} -
+ {fip.description} )} -
+
) } diff --git a/app/components/PoolListboxItem.tsx b/app/components/PoolListboxItem.tsx index c3041d61c9..c5307605fc 100644 --- a/app/components/PoolListboxItem.tsx +++ b/app/components/PoolListboxItem.tsx @@ -10,6 +10,7 @@ import type { IpVersion } from '@oxide/api' import { Badge } from '@oxide/design-system/ui' import { IpVersionBadge } from '~/components/IpVersionBadge' +import { ItemDescription } from '~/ui/lib/ItemDescription' import type { ListboxItem } from '~/ui/lib/Listbox' /** Common fields of SiloIpPool and SiloSubnetPool used for display */ @@ -31,9 +32,7 @@ export function toPoolItem(p: PoolLike): ListboxItem { {p.isDefault && default}
- {!!p.description && ( -
{p.description}
- )} + {!!p.description && {p.description}} ) return { value, selectedLabel, label } diff --git a/app/components/form/fields/ImageSelectField.tsx b/app/components/form/fields/ImageSelectField.tsx index 13bb17663f..b38252d630 100644 --- a/app/components/form/fields/ImageSelectField.tsx +++ b/app/components/form/fields/ImageSelectField.tsx @@ -11,6 +11,7 @@ import type { Image } from '@oxide/api' import type { InstanceCreateInput } from '~/forms/instance-create' import type { ComboboxItem } from '~/ui/lib/Combobox' +import { ItemDescription } from '~/ui/lib/ItemDescription' import { Slash } from '~/ui/lib/Slash' import { diskSizeNearest10 } from '~/util/math' import { bytesToGiB, GiB } from '~/util/units' @@ -82,7 +83,7 @@ export function toImageComboboxItem( label: (
{name}
-
{itemMetadata}
+ {itemMetadata}
), } diff --git a/app/forms/disk-create.tsx b/app/forms/disk-create.tsx index 2e8f182d43..f1abdc3138 100644 --- a/app/forms/disk-create.tsx +++ b/app/forms/disk-create.tsx @@ -34,6 +34,7 @@ import { HL } from '~/components/HL' import { useProjectSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { FieldLabel } from '~/ui/lib/FieldLabel' +import { ItemDescription } from '~/ui/lib/ItemDescription' import { SideModalFormDocs } from '~/ui/lib/ModalLinks' import { Radio } from '~/ui/lib/Radio' import { RadioGroup } from '~/ui/lib/RadioGroup' @@ -420,14 +421,14 @@ const SnapshotSelectField = ({ control }: { control: Control }) value: i.id, selectedLabel: i.name, label: ( - <> +
{i.name}
-
+ Created on {toLocaleDateString(i.timeCreated)} {formattedSize.value}{' '} {formattedSize.unit} -
- + +
), } })} diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx index 2895873ea5..6c0e138ad6 100644 --- a/app/forms/instance-create.tsx +++ b/app/forms/instance-create.tsx @@ -70,6 +70,7 @@ import { Button } from '~/ui/lib/Button' import { toComboboxItems } from '~/ui/lib/Combobox' import { FormDivider } from '~/ui/lib/Divider' import { EmptyMessage } from '~/ui/lib/EmptyMessage' +import { ItemDescription } from '~/ui/lib/ItemDescription' import { Listbox } from '~/ui/lib/Listbox' import { Message } from '~/ui/lib/Message' import { MiniTable } from '~/ui/lib/MiniTable' @@ -862,19 +863,17 @@ export default function CreateInstanceForm() { } const FloatingIpLabel = ({ ip }: { ip: FloatingIp }) => ( -
+
{ip.name}
-
-
{ip.ip}
+ + {ip.ip} {ip.description && ( <> -
- {ip.description} -
+ {ip.description} )} -
+
) diff --git a/app/pages/system/networking/IpPoolPage.tsx b/app/pages/system/networking/IpPoolPage.tsx index bfb544ea59..2e19644d11 100644 --- a/app/pages/system/networking/IpPoolPage.tsx +++ b/app/pages/system/networking/IpPoolPage.tsx @@ -53,6 +53,7 @@ import { type ComboboxItem } from '~/ui/lib/Combobox' import { CreateButton, CreateLink } from '~/ui/lib/CreateButton' import * as Dropdown from '~/ui/lib/DropdownMenu' import { EmptyMessage } from '~/ui/lib/EmptyMessage' +import { ItemDescription } from '~/ui/lib/ItemDescription' import { Message } from '~/ui/lib/Message' import { Modal } from '~/ui/lib/Modal' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' @@ -479,11 +480,7 @@ const toSiloComboboxItem = ({ name, description }: Silo): ComboboxItem => ({ label: (
{name}
- {description && ( -
- {description} -
- )} + {description && {description}}
), }) diff --git a/app/ui/lib/ItemDescription.tsx b/app/ui/lib/ItemDescription.tsx new file mode 100644 index 0000000000..9d76f70947 --- /dev/null +++ b/app/ui/lib/ItemDescription.tsx @@ -0,0 +1,20 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import { classed } from '~/util/classed' + +/** + * Secondary line in a combobox or listbox item label, e.g., a description or + * metadata shown under the item name. The `selected:` variant keeps the text + * legible against the accent background of the highlighted option. User-supplied + * descriptions can be arbitrarily long, so clamp at two lines to keep option + * heights bounded, and break long unbroken tokens (like URLs) so they wrap + * instead of overflowing. Content must be inline (no flex/block children) or + * the line clamp won't apply. + */ +export const ItemDescription = classed.div`text-secondary selected:text-accent-secondary line-clamp-2 break-words` From 737b489d5cc9f995c7e6149443dfb947a64c9ae3 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Sun, 19 Jul 2026 16:49:28 -0500 Subject: [PATCH 2/2] consolidate picker item labels into ItemLabel component --- app/components/AttachFloatingIpModal.tsx | 25 ++++++++----------- app/components/PoolListboxItem.tsx | 21 +++++++++------- .../form/fields/ImageSelectField.tsx | 9 ++----- app/forms/disk-create.tsx | 15 +++++------ app/forms/instance-create.tsx | 23 ++++++++--------- app/pages/system/networking/IpPoolPage.tsx | 9 ++----- .../{ItemDescription.tsx => ItemLabel.tsx} | 22 +++++++++++++++- 7 files changed, 64 insertions(+), 60 deletions(-) rename app/ui/lib/{ItemDescription.tsx => ItemLabel.tsx} (52%) diff --git a/app/components/AttachFloatingIpModal.tsx b/app/components/AttachFloatingIpModal.tsx index c9d60de4d3..a6563145a6 100644 --- a/app/components/AttachFloatingIpModal.tsx +++ b/app/components/AttachFloatingIpModal.tsx @@ -20,7 +20,7 @@ import { import { ListboxField } from '~/components/form/fields/ListboxField' import { HL } from '~/components/HL' import { addToast } from '~/stores/toast' -import { ItemDescription } from '~/ui/lib/ItemDescription' +import { ItemLabel } from '~/ui/lib/ItemLabel' import { Message } from '~/ui/lib/Message' import { Slash } from '~/ui/lib/Slash' @@ -51,19 +51,16 @@ function IpPoolName({ ipPoolId }: { ipPoolId: string }) { function FloatingIpLabel({ fip }: { fip: FloatingIp }) { return ( -
-
{fip.name}
- - {fip.ip} - - {fip.description && ( - <> - - {fip.description} - - )} - -
+ + {fip.ip} + + {fip.description && ( + <> + + {fip.description} + + )} + ) } diff --git a/app/components/PoolListboxItem.tsx b/app/components/PoolListboxItem.tsx index c5307605fc..6b781b3fd4 100644 --- a/app/components/PoolListboxItem.tsx +++ b/app/components/PoolListboxItem.tsx @@ -10,7 +10,7 @@ import type { IpVersion } from '@oxide/api' import { Badge } from '@oxide/design-system/ui' import { IpVersionBadge } from '~/components/IpVersionBadge' -import { ItemDescription } from '~/ui/lib/ItemDescription' +import { ItemLabel } from '~/ui/lib/ItemLabel' import type { ListboxItem } from '~/ui/lib/Listbox' /** Common fields of SiloIpPool and SiloSubnetPool used for display */ @@ -26,14 +26,17 @@ export function toPoolItem(p: PoolLike): ListboxItem { const value = p.name const selectedLabel = p.name const label = ( -
-
- {p.name} - {p.isDefault && default} - -
- {!!p.description && {p.description}} -
+ + {p.name} + {p.isDefault && default} + + + } + > + {p.description} + ) return { value, selectedLabel, label } } diff --git a/app/components/form/fields/ImageSelectField.tsx b/app/components/form/fields/ImageSelectField.tsx index b38252d630..6d23427c14 100644 --- a/app/components/form/fields/ImageSelectField.tsx +++ b/app/components/form/fields/ImageSelectField.tsx @@ -11,7 +11,7 @@ import type { Image } from '@oxide/api' import type { InstanceCreateInput } from '~/forms/instance-create' import type { ComboboxItem } from '~/ui/lib/Combobox' -import { ItemDescription } from '~/ui/lib/ItemDescription' +import { ItemLabel } from '~/ui/lib/ItemLabel' import { Slash } from '~/ui/lib/Slash' import { diskSizeNearest10 } from '~/util/math' import { bytesToGiB, GiB } from '~/util/units' @@ -80,11 +80,6 @@ export function toImageComboboxItem( return { value: id, selectedLabel: name, - label: ( -
-
{name}
- {itemMetadata} -
- ), + label: {itemMetadata}, } } diff --git a/app/forms/disk-create.tsx b/app/forms/disk-create.tsx index f1abdc3138..32c097d1e0 100644 --- a/app/forms/disk-create.tsx +++ b/app/forms/disk-create.tsx @@ -34,7 +34,7 @@ import { HL } from '~/components/HL' import { useProjectSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { FieldLabel } from '~/ui/lib/FieldLabel' -import { ItemDescription } from '~/ui/lib/ItemDescription' +import { ItemLabel } from '~/ui/lib/ItemLabel' import { SideModalFormDocs } from '~/ui/lib/ModalLinks' import { Radio } from '~/ui/lib/Radio' import { RadioGroup } from '~/ui/lib/RadioGroup' @@ -421,14 +421,11 @@ const SnapshotSelectField = ({ control }: { control: Control }) value: i.id, selectedLabel: i.name, label: ( -
-
{i.name}
- - Created on {toLocaleDateString(i.timeCreated)} - {formattedSize.value}{' '} - {formattedSize.unit} - -
+ + Created on {toLocaleDateString(i.timeCreated)} + {formattedSize.value}{' '} + {formattedSize.unit} + ), } })} diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx index 6c0e138ad6..ce57bbea6b 100644 --- a/app/forms/instance-create.tsx +++ b/app/forms/instance-create.tsx @@ -70,7 +70,7 @@ import { Button } from '~/ui/lib/Button' import { toComboboxItems } from '~/ui/lib/Combobox' import { FormDivider } from '~/ui/lib/Divider' import { EmptyMessage } from '~/ui/lib/EmptyMessage' -import { ItemDescription } from '~/ui/lib/ItemDescription' +import { ItemLabel } from '~/ui/lib/ItemLabel' import { Listbox } from '~/ui/lib/Listbox' import { Message } from '~/ui/lib/Message' import { MiniTable } from '~/ui/lib/MiniTable' @@ -863,18 +863,15 @@ export default function CreateInstanceForm() { } const FloatingIpLabel = ({ ip }: { ip: FloatingIp }) => ( -
-
{ip.name}
- - {ip.ip} - {ip.description && ( - <> - - {ip.description} - - )} - -
+ + {ip.ip} + {ip.description && ( + <> + + {ip.description} + + )} + ) const NetworkingSection = ({ diff --git a/app/pages/system/networking/IpPoolPage.tsx b/app/pages/system/networking/IpPoolPage.tsx index 2e19644d11..180314657f 100644 --- a/app/pages/system/networking/IpPoolPage.tsx +++ b/app/pages/system/networking/IpPoolPage.tsx @@ -53,7 +53,7 @@ import { type ComboboxItem } from '~/ui/lib/Combobox' import { CreateButton, CreateLink } from '~/ui/lib/CreateButton' import * as Dropdown from '~/ui/lib/DropdownMenu' import { EmptyMessage } from '~/ui/lib/EmptyMessage' -import { ItemDescription } from '~/ui/lib/ItemDescription' +import { ItemLabel } from '~/ui/lib/ItemLabel' import { Message } from '~/ui/lib/Message' import { Modal } from '~/ui/lib/Modal' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' @@ -477,12 +477,7 @@ const defaultValues: LinkSiloFormValues = { silo: undefined, isDefault: false } const toSiloComboboxItem = ({ name, description }: Silo): ComboboxItem => ({ value: name, selectedLabel: name, - label: ( -
-
{name}
- {description && {description}} -
- ), + label: {description}, }) function LinkSiloModal({ onDismiss }: { onDismiss: () => void }) { diff --git a/app/ui/lib/ItemDescription.tsx b/app/ui/lib/ItemLabel.tsx similarity index 52% rename from app/ui/lib/ItemDescription.tsx rename to app/ui/lib/ItemLabel.tsx index 9d76f70947..49df14c532 100644 --- a/app/ui/lib/ItemDescription.tsx +++ b/app/ui/lib/ItemLabel.tsx @@ -6,6 +6,8 @@ * Copyright Oxide Computer Company */ +import type { ReactNode } from 'react' + import { classed } from '~/util/classed' /** @@ -17,4 +19,22 @@ import { classed } from '~/util/classed' * instead of overflowing. Content must be inline (no flex/block children) or * the line clamp won't apply. */ -export const ItemDescription = classed.div`text-secondary selected:text-accent-secondary line-clamp-2 break-words` +const ItemDescription = classed.div`text-secondary selected:text-accent-secondary line-clamp-2 break-words` + +type ItemLabelProps = { + /** Resource name, plus any badges (the name row is a flex row with gap) */ + name: ReactNode + /** Secondary line: a description, slash-separated metadata, or both */ + children?: ReactNode +} + +/** + * Standard two-line label for combobox and listbox items: resource name over + * an optional secondary line. + */ +export const ItemLabel = ({ name, children }: ItemLabelProps) => ( +
+
{name}
+ {children && {children}} +
+)