diff --git a/packages/component-library/src/TruncateAddress/index.module.scss b/packages/component-library/src/TruncateAddress/index.module.scss new file mode 100644 index 0000000000..1a45808c04 --- /dev/null +++ b/packages/component-library/src/TruncateAddress/index.module.scss @@ -0,0 +1,32 @@ +@use "../theme"; + +.srOnly { + @include theme.sr-only; +} + +.truncateAddressDynamic { + // Defaults may get overridden by style on span + --min-chars-start-ch: 0ch; + --min-chars-end-ch: 0ch; + + &::before, + &::after { + display: inline-block; + overflow: hidden; + white-space: pre; + max-width: 50%; + } + + &::before { + content: attr(data-text-start); + text-overflow: ellipsis; + min-width: var(--min-chars-start-ch); + } + + &::after { + content: attr(data-text-end); + text-overflow: clip; + direction: rtl; + min-width: var(--min-chars-end-ch); + } +} diff --git a/packages/component-library/src/TruncateAddress/index.stories.tsx b/packages/component-library/src/TruncateAddress/index.stories.tsx new file mode 100644 index 0000000000..ec73d9ceac --- /dev/null +++ b/packages/component-library/src/TruncateAddress/index.stories.tsx @@ -0,0 +1,169 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { TruncateAddress as TruncateAddressComponent } from "./index"; // Only import the main component + +const meta = { + component: TruncateAddressComponent, + argTypes: { + text: { + control: "text", + description: "The address string to truncate.", + table: { + category: "Data", + }, + }, + fixed: { + control: "boolean", + description: + "Determines if the truncation uses a fixed number of characters (true) or is dynamic (false).", + table: { + category: "Behavior", + defaultValue: { summary: "false" }, + }, + }, + minCharsStart: { + control: "number", + description: + "Minimum characters to show at the start. Default for dynamic is 0, for fixed is 6.", + table: { + category: "Behavior", + }, + }, + minCharsEnd: { + control: "number", + description: + "Minimum characters to show at the end. Default for dynamic is 0, for fixed is 6.", + table: { + category: "Behavior", + }, + }, + }, + parameters: { + docs: { + description: { + component: + "A component to truncate long strings, typically addresses, in a user-friendly way. It supports both dynamic (CSS-based, responsive) and fixed (JS-based, specific character counts) truncation.", + }, + }, + }, + tags: ["autodocs"], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const defaultAddress = "0x1234567890abcdef1234567890abcdef12345678"; +const longEnsName = "verylongethereumdomainnamethatshouldbetruncated.eth"; +const shortAddress = "0xABC"; + +export const DynamicDefault: Story = { + args: { + text: defaultAddress, + fixed: false, + }, + name: "Dynamic (Default Behavior)", +}; + +export const FixedDefault: Story = { + args: { + text: defaultAddress, + fixed: true, + }, + name: "Fixed (Default Behavior)", +}; + +export const DynamicCustomChars: Story = { + args: { + text: defaultAddress, + fixed: false, + minCharsStart: 4, + minCharsEnd: 4, + }, + name: "Dynamic (Custom minChars: 4 start, 4 end)", +}; + +export const FixedCustomChars: Story = { + args: { + text: defaultAddress, + fixed: true, + minCharsStart: 8, + minCharsEnd: 8, + }, + name: "Fixed (Custom minChars: 8 start, 8 end)", +}; + +export const DynamicEnsName: Story = { + args: { + text: longEnsName, + fixed: false, + minCharsStart: 10, // Show more for ENS names if dynamic + minCharsEnd: 3, + }, + name: "Dynamic (ENS Name)", +}; + +export const FixedEnsName: Story = { + args: { + text: longEnsName, + fixed: true, + minCharsStart: 10, + minCharsEnd: 8, // .eth + 5 chars + }, + name: "Fixed (ENS Name)", +}; + +export const DynamicShortAddress: Story = { + args: { + text: shortAddress, + fixed: false, + // minCharsStart/End will effectively show the whole string if it's shorter + }, + name: "Dynamic (Short Address)", +}; + +export const FixedShortAddress: Story = { + args: { + text: shortAddress, + fixed: true, + minCharsStart: 2, // Will show 0x...BC if text is 0xABC + minCharsEnd: 2, + }, + name: "Fixed (Short Address)", +}; + +export const FixedVeryShortAddressShowsAll: Story = { + args: { + text: "0x1", + fixed: true, + minCharsStart: 6, + minCharsEnd: 6, + }, + name: "Fixed (Very Short Address, Shows All)", + parameters: { + docs: { + description: + "If the text is shorter than or equal to minCharsStart + minCharsEnd, the original text is shown.", + }, + }, +}; + +export const DynamicZeroMinChars: Story = { + args: { + text: defaultAddress, + fixed: false, + minCharsStart: 0, + minCharsEnd: 0, + }, + name: "Dynamic (Zero minChars)", +}; + +export const FixedZeroMinChars: Story = { + args: { + text: defaultAddress, + fixed: true, + minCharsStart: 0, + minCharsEnd: 0, + }, + name: "Fixed (Zero minChars, shows only ellipsis)", +}; diff --git a/packages/component-library/src/TruncateAddress/index.tsx b/packages/component-library/src/TruncateAddress/index.tsx new file mode 100644 index 0000000000..49220cb794 --- /dev/null +++ b/packages/component-library/src/TruncateAddress/index.tsx @@ -0,0 +1,71 @@ +import React, { useMemo } from "react"; + +import styles from "./index.module.scss"; + +export type Props = { + text: string; + fixed?: boolean; + minCharsStart?: number | undefined; + minCharsEnd?: number | undefined; +}; + +export const TruncateAddress = ({ fixed = false, ...rest }: Props) => { + return fixed ? ( + + ) : ( + + ); +}; + +const TruncateAddressDynamic = ({ + text, + minCharsStart = 0, + minCharsEnd = 0, +}: Props) => { + // We're setting a minimum width using CSS 'ch' units, which are relative to the + // width of the '0' character. This provides a good approximation for showing + // a certain number of characters. However, since character widths vary + // (e.g., 'i' is narrower than 'W'), the exact count of visible characters + // might differ slightly from the specified 'ch' value. + const style = { + "--min-chars-start-ch": `${minCharsStart.toString()}ch`, + "--min-chars-end-ch": `${minCharsEnd.toString()}ch`, + } as React.CSSProperties; + + return ( + <> +