From ad6372df59cdcd9f4009bd8eb9f67e9e7748d3e8 Mon Sep 17 00:00:00 2001 From: Nathnael Dereje Date: Thu, 1 Oct 2026 10:21:30 +0200 Subject: [PATCH 01/12] feat: Add prefix and secondaryText to Token Add a prefix slot before the label (for example a category color dot) and a secondaryText value after it. Includes a Token additions dev page demoing both, plus popover composition in the label slot. --- pages/token/additions.page.tsx | 263 +++++++++++++++++++++++++++++++++ pages/token/styles.scss | 65 ++++++++ src/token/interfaces.ts | 13 ++ src/token/internal.tsx | 12 ++ src/token/styles.scss | 15 ++ 5 files changed, 368 insertions(+) create mode 100644 pages/token/additions.page.tsx diff --git a/pages/token/additions.page.tsx b/pages/token/additions.page.tsx new file mode 100644 index 0000000000..d7bb592d51 --- /dev/null +++ b/pages/token/additions.page.tsx @@ -0,0 +1,263 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import Box from '~components/box'; +import CopyToClipboard from '~components/copy-to-clipboard'; +import ExpandableSection from '~components/expandable-section'; +import Icon, { IconProps } from '~components/icon'; +import Popover from '~components/popover'; +import SpaceBetween from '~components/space-between'; +import Token, { TokenProps } from '~components/token'; +import { colorChartsPaletteCategorical2 } from '~design-tokens'; + +import styles from './styles.scss'; + +// Colors are consumer-owned: the token provides the prefix slot, the consuming team +// chooses the visual (per the kickoff). A real console would use its own design tokens. +const RED = '#d13212'; +const ORANGE = '#ec7211'; +const YELLOW = '#f2c811'; +const GREEN = '#1d8102'; +const BLUE = '#0972d3'; +const PURPLE = '#7d4bcb'; + +function Dot({ color }: { color: string }) { + return ; +} + +function QueryAction({ icon, label }: { icon: IconProps.Name; label: string }) { + return ( +
+ + + {label} + + +
+ ); +} + +function SectionLabel({ text, counter }: { text: string; counter?: string }) { + return ( + + {text} + {counter && {counter}} + + ); +} + +// Mirrors the CloudWatch Log Analytics field-details popover: field name + copy + category +// token header, coverage bar in the category color, then log groups and query actions. +function FieldDetails({ field, category, color }: { field: string; category: string; color: string }) { + return ( +
+
+ + {field} + + + } /> +
+
+ Coverage + 100% +
+
+ }> +
+ /aws/cognito/userpools/ap-south-1_34n4shGQf 882640845406 + 100% +
+
+ } defaultExpanded={true}> + + + + +
+ ); +} + +interface Spec { + name: string; + color?: string; + icon?: IconProps.Name; + count?: string; +} + +const FIELDS: Spec[] = [ + { name: '@log', color: RED, count: '100%' }, + { name: '@message', color: RED, count: '100%' }, + { name: 'ResourceType', color: PURPLE, count: '42%' }, + { name: 'statusCode', color: PURPLE, count: '88%' }, +]; +const SEVERITY: Spec[] = [ + { name: 'Critical', color: RED }, + { name: 'High', color: ORANGE }, + { name: 'Medium', color: YELLOW }, + { name: 'Low', color: GREEN }, + { name: 'Info', color: BLUE }, +]; +const ENVIRONMENTS: Spec[] = [ + { name: 'Production', color: RED }, + { name: 'Staging', color: ORANGE }, + { name: 'Development', color: GREEN }, +]; +const META: Spec[] = [ + { name: '@log', count: '100%' }, + { name: '@aws.account', count: '17%' }, + { name: '@aws.region', count: '6%' }, +]; +const META_FULL: Spec[] = [ + { name: 'Critical', color: RED, icon: 'status-negative', count: '3' }, + { name: 'Warning', color: ORANGE, icon: 'status-warning', count: '12' }, + { name: 'Info', color: BLUE, icon: 'status-info', count: '847' }, +]; + +type Kind = 'static' | 'popover'; + +function Tokens({ + items, + kind = 'static', + testIdPrefix, + variant, + dismissible, +}: { + items: Spec[]; + kind?: Kind; + testIdPrefix: string; + variant?: TokenProps.Variant; + dismissible?: boolean; +}) { + const inline = variant === 'inline'; + + return ( + + {items.map(it => { + const common = { + 'data-testid': `${testIdPrefix}-${it.name}`, + label: it.name, + variant, + prefix: it.color ? : undefined, + secondaryText: it.count, + icon: it.icon ? : undefined, + ariaLabel: it.count ? `${it.name}, ${it.count}` : undefined, + }; + const dismiss = dismissible ? { dismissLabel: `Remove ${it.name}`, onDismiss: () => {} } : {}; + + if (kind === 'popover') { + return ( + } + > + {it.name} + + } + /> + ); + } + return ; + })} + + ); +} + +export default function TokenAdditionsPage() { + return ( + +

Token additions

+ + +
+

API proposal usage examples

+

Renders the exact code from the API proposal, for doc screenshots.

+

1. Field token with prefix and secondary text

+ } + secondaryText="17%" + dismissLabel="Remove @data_format" + onDismiss={() => {}} + /> +

2. Popover token - contextual actions

+ } + > + @logStream + + } + secondaryText="100%" + dismissLabel="Remove @logStream" + onDismiss={() => {}} + /> +

3. Existing usage - unchanged

+ {}} /> +
+ +
+

Prefix

+

+ A color dot marks a category, passed via the prefix slot. Always paired with a label, never color alone. +

+

With dismiss

+ +

Not dismissible

+ +
+ +
+

Secondary text

+

A secondary value next to the label, such as coverage or a count.

+

With percentage

+ +

With color, icon, and count

+ +
+ +
+

Popover

+

+ Clicking the label opens a popover with contextual actions. Composed by passing Popover in the label slot, + no dedicated prop. +

+

Field details

+ +
+ +
+

Inline

+

With prefix and secondary text

+ +
+ +
+

States

+ + + + + {}} /> + +
+
+
+ ); +} diff --git a/pages/token/styles.scss b/pages/token/styles.scss index bd446b1ba4..397c758264 100644 --- a/pages/token/styles.scss +++ b/pages/token/styles.scss @@ -22,3 +22,68 @@ white-space: nowrap; overflow: hidden; } + +// Demo-only helpers for the Token additions page. Dynamic, consumer-owned colors +// (the category dot, coverage bar, log-group highlight) stay inline on the element. +.dot { + display: inline-block; + inline-size: 8px; + block-size: 8px; + border-start-start-radius: 50%; + border-start-end-radius: 50%; + border-end-start-radius: 50%; + border-end-end-radius: 50%; +} + +.field-details { + inline-size: 320px; +} + +.field-details-header, +.coverage-row, +.query-action { + display: flex; + align-items: center; + justify-content: space-between; +} + +.coverage-row { + margin-block-start: awsui.$space-scaled-xxs; +} + +.coverage-bar { + block-size: 6px; + border-start-start-radius: 3px; + border-start-end-radius: 3px; + border-end-start-radius: 3px; + border-end-end-radius: 3px; + margin-block: awsui.$space-scaled-xxs; +} + +.section-label { + display: inline-flex; + justify-content: space-between; + inline-size: 272px; + text-transform: uppercase; +} + +.log-group-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: awsui.$space-scaled-xxs; + border-start-start-radius: awsui.$border-radius-item; + border-start-end-radius: awsui.$border-radius-item; + border-end-start-radius: awsui.$border-radius-item; + border-end-end-radius: awsui.$border-radius-item; + padding-block: awsui.$space-scaled-xxs; + padding-inline: awsui.$space-scaled-xs; +} + +.log-group-name { + overflow-wrap: anywhere; +} + +.query-action { + padding-block: awsui.$space-scaled-xxs; +} diff --git a/src/token/interfaces.ts b/src/token/interfaces.ts index 7e65d12d47..db2dbde1a6 100644 --- a/src/token/interfaces.ts +++ b/src/token/interfaces.ts @@ -63,6 +63,19 @@ export interface TokenProps extends BaseComponentProps { */ onDismiss?: NonCancelableEventHandler; + /** + * Slot for a decorative category marker before the label, typically a color dot. + * + * The slot is `aria-hidden` and must not contain interactive content. Convey the category + * in the `label` or `ariaLabel` as well - color must never be the only signal. + */ + prefix?: React.ReactNode; + + /** + * Secondary text shown inline after the label, such as a count or percentage. + */ + secondaryText?: string; + /** * Content to display in the tooltip when `variant="inline"`. The tooltip appears when the token label is truncated due to insufficient space. * diff --git a/src/token/internal.tsx b/src/token/internal.tsx index 9bc1df0256..753cc16440 100644 --- a/src/token/internal.tsx +++ b/src/token/internal.tsx @@ -63,6 +63,8 @@ function InternalToken({ dismissLabel, onDismiss, tooltipContent, + prefix, + secondaryText, // Internal role, @@ -191,6 +193,11 @@ function InternalToken({ )} style={tokenRootStyleProps} > + {prefix && ( + + )}
Coverage @@ -146,9 +150,12 @@ function Tokens({ 'data-testid': `${testIdPrefix}-${it.name}`, label: it.name, variant, - prefix: it.color ? : undefined, labelTag: it.count, - icon: it.icon ? : undefined, + icon: it.icon ? ( + + ) : it.color ? ( + + ) : undefined, ariaLabel: it.count ? `${it.name}, ${it.count}` : undefined, }; const dismiss = dismissible ? { dismissLabel: `Remove ${it.name}`, onDismiss: () => {} } : {}; @@ -187,11 +194,11 @@ export default function TokenAdditionsPage() {

API proposal usage examples

Renders the exact code from the API proposal, for doc screenshots.

-

1. Field token with prefix and label tag

+

1. Field token with a color dot icon and label tag

} + icon={} labelTag="17%" dismissLabel="Remove @data_format" onDismiss={() => {}} @@ -217,9 +224,10 @@ export default function TokenAdditionsPage() {
-

Prefix

+

Color dot icon

- A color dot marks a category, passed via the prefix slot. Always paired with a label, never color alone. + A color dot marks a category, passed as a custom icon through the icon slot. Always paired with a label, + never color alone.

With dismiss

@@ -234,7 +242,7 @@ export default function TokenAdditionsPage() {

With percentage

-

With color, icon, and count

+

With icon and count

@@ -244,13 +252,13 @@ export default function TokenAdditionsPage() { Clicking the label opens a popover with contextual actions. Composed by passing Popover in the label slot, no dedicated prop.

-

Field details with prefix and label tag

+

Field details with color dot icon and label tag

Inline

-

With prefix and label tag

+

With color dot icon and label tag

diff --git a/pages/token/styles.scss b/pages/token/styles.scss index 397c758264..460f1c1968 100644 --- a/pages/token/styles.scss +++ b/pages/token/styles.scss @@ -35,6 +35,15 @@ border-end-end-radius: 50%; } +// Gives the dot the same 16px box as a normal-size Icon, so it lines up in the token's icon slot. +.dot-icon { + display: inline-flex; + align-items: center; + justify-content: center; + inline-size: 16px; + block-size: 16px; +} + .field-details { inline-size: 320px; } diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index 4823100ea3..40cd7e37a0 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -33940,14 +33940,6 @@ For \`variant="inline"\`, only plain text is supported, for example, strings or "isDefault": false, "name": "label", }, - { - "description": "Slot for a decorative category marker before the label, typically a color dot. - -The slot is \`aria-hidden\` and must not contain interactive content. Convey the category -in the \`label\` or \`ariaLabel\` as well - color must never be the only signal.", - "isDefault": false, - "name": "prefix", - }, ], "releaseStatus": "stable", } diff --git a/src/token/__tests__/token.test.tsx b/src/token/__tests__/token.test.tsx index 176011fbb3..b31d2f8e56 100644 --- a/src/token/__tests__/token.test.tsx +++ b/src/token/__tests__/token.test.tsx @@ -162,23 +162,6 @@ describe('Token', () => { }); }); - describe('Prefix', () => { - test('renders prefix in an aria-hidden slot', () => { - const wrapper = renderToken({ - label: 'Test token', - prefix: , - }); - const prefix = wrapper.findByClassName(styles.prefix)!.getElement(); - expect(prefix).toHaveAttribute('aria-hidden', 'true'); - expect(prefix).toContainElement(screen.getByTestId('prefix-dot')); - }); - - test('does not render prefix slot when prefix is not set', () => { - const wrapper = renderToken({ label: 'Test token' }); - expect(wrapper.findByClassName(styles.prefix)).toBeNull(); - }); - }); - describe('Label tag', () => { test('renders label tag for normal variant', () => { const wrapper = renderToken({ label: 'Test token', labelTag: '17%' }); diff --git a/src/token/interfaces.ts b/src/token/interfaces.ts index c6b08e6be1..7dc65af2d7 100644 --- a/src/token/interfaces.ts +++ b/src/token/interfaces.ts @@ -63,14 +63,6 @@ export interface TokenProps extends BaseComponentProps { */ onDismiss?: NonCancelableEventHandler; - /** - * Slot for a decorative category marker before the label, typically a color dot. - * - * The slot is `aria-hidden` and must not contain interactive content. Convey the category - * in the `label` or `ariaLabel` as well - color must never be the only signal. - */ - prefix?: React.ReactNode; - /** * Content to display in the tooltip when `variant="inline"`. The tooltip appears when the token label is truncated due to insufficient space. * diff --git a/src/token/internal.tsx b/src/token/internal.tsx index 048bd973ed..81268c396a 100644 --- a/src/token/internal.tsx +++ b/src/token/internal.tsx @@ -63,7 +63,6 @@ function InternalToken({ dismissLabel, onDismiss, tooltipContent, - prefix, // Internal role, @@ -193,11 +192,6 @@ function InternalToken({ )} style={tokenRootStyleProps} > - {prefix && ( - - )}