diff --git a/frontend/common/theme/__tests__/tagSwatches.test.ts b/frontend/common/theme/__tests__/tagSwatches.test.ts new file mode 100644 index 000000000000..409979a3acb1 --- /dev/null +++ b/frontend/common/theme/__tests__/tagSwatches.test.ts @@ -0,0 +1,39 @@ +import tokens from 'common/theme/tokens.json' +import { AA_NORMAL_TEXT, contrastRatio } from 'common/theme/contrast' + +type Entry = { cssVar: string; light: string; dark: string } + +const surfaces = tokens.tag.surface as Record +const texts = tokens.tag.text as Record +const primitives = tokens.primitives as Record + +const hues = Object.keys(surfaces) + +describe('tag swatches', () => { + it('defines a surface and a text token for every hue', () => { + expect(hues.length).toBeGreaterThan(0) + expect(Object.keys(texts)).toEqual(hues) + }) + + describe.each(hues)('%s', (hue) => { + const surface = surfaces[hue] + const text = texts[hue] + + it.each(['light', 'dark'] as const)('passes AA in %s mode', (theme) => { + expect(contrastRatio(surface[theme], text[theme])).toBeGreaterThanOrEqual( + AA_NORMAL_TEXT, + ) + }) + }) + + // Tags are told apart by colour alone, so two swatches rendering alike is + // the same defect as failing contrast: #8465 found the picker offering 20 + // options that resolved to 7 colours. + it.each(['light', 'dark'] as const)( + 'gives every swatch a distinct surface in %s mode', + (theme) => { + const fills = hues.map((hue) => surfaces[hue][theme].toLowerCase()) + expect(new Set(fills).size).toBe(fills.length) + }, + ) +}) diff --git a/frontend/common/theme/contrast.ts b/frontend/common/theme/contrast.ts new file mode 100644 index 000000000000..28c64e120e1f --- /dev/null +++ b/frontend/common/theme/contrast.ts @@ -0,0 +1,19 @@ +// WCAG relative luminance and contrast ratio, per +// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance + +export const AA_NORMAL_TEXT = 4.5 + +export const relativeLuminance = (hex: string): number => { + const value = hex.replace('#', '') + const [r, g, b] = [0, 2, 4] + .map((i) => parseInt(value.substring(i, i + 2), 16) / 255) + .map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)) + return 0.2126 * r + 0.7152 * g + 0.0722 * b +} + +export const contrastRatio = (a: string, b: string): number => { + const [lighter, darker] = [relativeLuminance(a), relativeLuminance(b)].sort( + (x, y) => y - x, + ) + return (lighter + 0.05) / (darker + 0.05) +} diff --git a/frontend/common/theme/tokens.json b/frontend/common/theme/tokens.json index d75701d1ab1f..0d2fa24baa56 100644 --- a/frontend/common/theme/tokens.json +++ b/frontend/common/theme/tokens.json @@ -143,6 +143,52 @@ "info": { "cssVar": "--color-icon-info", "light": "#0aaddf", "dark": "#0aaddf" } } }, + "tag": { + "surface": { + "indigo": { "cssVar": "--color-tag-indigo-surface", "light": "#ddeaff", "dark": "#222a51", "description": "Custom tag fill, indigo. Pairs with --color-tag-indigo-text." }, + "coral": { "cssVar": "--color-tag-coral-surface", "light": "#ffddd3", "dark": "#4b1e17", "description": "Custom tag fill, coral. Pairs with --color-tag-coral-text." }, + "gold": { "cssVar": "--color-tag-gold-surface", "light": "#f3edbf", "dark": "#362e00", "description": "Custom tag fill, gold. Pairs with --color-tag-gold-text." }, + "green": { "cssVar": "--color-tag-green-surface", "light": "#d6f6d0", "dark": "#183612", "description": "Custom tag fill, green. Pairs with --color-tag-green-text." }, + "orange": { "cssVar": "--color-tag-orange-surface", "light": "#ffdece", "dark": "#4a1f11", "description": "Custom tag fill, orange. Pairs with --color-tag-orange-text." }, + "blue": { "cssVar": "--color-tag-blue-surface", "light": "#cdefff", "dark": "#0c2f4f", "description": "Custom tag fill, blue. Pairs with --color-tag-blue-text." }, + "cyan": { "cssVar": "--color-tag-cyan-surface", "light": "#c1f4ff", "dark": "#003449", "description": "Custom tag fill, cyan. Pairs with --color-tag-cyan-text." }, + "lavender": { "cssVar": "--color-tag-lavender-surface", "light": "#fbe0ff", "dark": "#3b2245", "description": "Custom tag fill, lavender. Pairs with --color-tag-lavender-text." }, + "teal": { "cssVar": "--color-tag-teal-surface", "light": "#bff9ef", "dark": "#003931", "description": "Custom tag fill, teal. Pairs with --color-tag-teal-text." }, + "navy": { "cssVar": "--color-tag-navy-surface", "light": "#d7ecff", "dark": "#1e2e49", "description": "Custom tag fill, navy. Pairs with --color-tag-navy-text." }, + "amber": { "cssVar": "--color-tag-amber-surface", "light": "#ffe5c0", "dark": "#432600", "description": "Custom tag fill, amber. Pairs with --color-tag-amber-text." }, + "mint": { "cssVar": "--color-tag-mint-surface", "light": "#ccf8da", "dark": "#05381e", "description": "Custom tag fill, mint. Pairs with --color-tag-mint-text." }, + "silver": { "cssVar": "--color-tag-silver-surface", "light": "#ebebeb", "dark": "#2e2e2e", "description": "Custom tag fill, silver. Pairs with --color-tag-silver-text." }, + "slate": { "cssVar": "--color-tag-slate-surface", "light": "#dbedff", "dark": "#212f3e", "description": "Custom tag fill, slate. Pairs with --color-tag-slate-text." }, + "maroon": { "cssVar": "--color-tag-maroon-surface", "light": "#ffddd4", "dark": "#4b1e18", "description": "Custom tag fill, maroon. Pairs with --color-tag-maroon-text." }, + "plum": { "cssVar": "--color-tag-plum-surface", "light": "#f4d9ff", "dark": "#3b2246", "description": "Custom tag fill, plum. Pairs with --color-tag-plum-text." }, + "burnt-orange": { "cssVar": "--color-tag-burnt-orange-surface", "light": "#ffdfcb", "dark": "#4a200d", "description": "Custom tag fill, burnt-orange. Pairs with --color-tag-burnt-orange-text." }, + "salmon": { "cssVar": "--color-tag-salmon-surface", "light": "#ffdcda", "dark": "#4b1d1e", "description": "Custom tag fill, salmon. Pairs with --color-tag-salmon-text." }, + "lime": { "cssVar": "--color-tag-lime-surface", "light": "#e7f1c4", "dark": "#2b3200", "description": "Custom tag fill, lime. Pairs with --color-tag-lime-text." }, + "cerise": { "cssVar": "--color-tag-cerise-surface", "light": "#ffdbe2", "dark": "#4a1d26", "description": "Custom tag fill, cerise. Pairs with --color-tag-cerise-text." } + }, + "text": { + "indigo": { "cssVar": "--color-tag-indigo-text", "light": "#3948ac", "dark": "#a7baff", "description": "Custom tag label, indigo. Pairs with --color-tag-indigo-surface." }, + "coral": { "cssVar": "--color-tag-coral-text", "light": "#9a2111", "dark": "#f7a697", "description": "Custom tag label, coral. Pairs with --color-tag-coral-surface." }, + "gold": { "cssVar": "--color-tag-gold-text", "light": "#695400", "dark": "#cbc072", "description": "Custom tag label, gold. Pairs with --color-tag-gold-surface." }, + "green": { "cssVar": "--color-tag-green-text", "light": "#076900", "dark": "#9bce91", "description": "Custom tag label, green. Pairs with --color-tag-green-surface." }, + "orange": { "cssVar": "--color-tag-orange-text", "light": "#992500", "dark": "#f6a78e", "description": "Custom tag label, orange. Pairs with --color-tag-orange-surface." }, + "blue": { "cssVar": "--color-tag-blue-text", "light": "#0055a9", "dark": "#8bc3fc", "description": "Custom tag label, blue. Pairs with --color-tag-blue-surface." }, + "cyan": { "cssVar": "--color-tag-cyan-text", "light": "#006191", "dark": "#72cbef", "description": "Custom tag label, cyan. Pairs with --color-tag-cyan-surface." }, + "lavender": { "cssVar": "--color-tag-lavender-text", "light": "#763090", "dark": "#d7aaea", "description": "Custom tag label, lavender. Pairs with --color-tag-lavender-surface." }, + "teal": { "cssVar": "--color-tag-teal-text", "light": "#00675b", "dark": "#6bd3c4", "description": "Custom tag label, teal. Pairs with --color-tag-teal-surface." }, + "navy": { "cssVar": "--color-tag-navy-text", "light": "#445674", "dark": "#aabfe1", "description": "Custom tag label, navy. Pairs with --color-tag-navy-surface." }, + "amber": { "cssVar": "--color-tag-amber-text", "light": "#8a3d00", "dark": "#e7b374", "description": "Custom tag label, amber. Pairs with --color-tag-amber-surface." }, + "mint": { "cssVar": "--color-tag-mint-text", "light": "#006b2e", "dark": "#88d1a2", "description": "Custom tag label, mint. Pairs with --color-tag-mint-surface." }, + "silver": { "cssVar": "--color-tag-silver-text", "light": "#555555", "dark": "#bebebe", "description": "Custom tag label, silver. Pairs with --color-tag-silver-surface." }, + "slate": { "cssVar": "--color-tag-slate-text", "light": "#485767", "dark": "#aec0d3", "description": "Custom tag label, slate. Pairs with --color-tag-slate-surface." }, + "maroon": { "cssVar": "--color-tag-maroon-text", "light": "#853c32", "dark": "#f7a598", "description": "Custom tag label, maroon. Pairs with --color-tag-maroon-surface." }, + "plum": { "cssVar": "--color-tag-plum-text", "light": "#6d3e82", "dark": "#d6aaeb", "description": "Custom tag label, plum. Pairs with --color-tag-plum-surface." }, + "burnt-orange": { "cssVar": "--color-tag-burnt-orange-text", "light": "#982800", "dark": "#f5a989", "description": "Custom tag label, burnt-orange. Pairs with --color-tag-burnt-orange-surface." }, + "salmon": { "cssVar": "--color-tag-salmon-text", "light": "#922b32", "dark": "#f8a4a2", "description": "Custom tag label, salmon. Pairs with --color-tag-salmon-surface." }, + "lime": { "cssVar": "--color-tag-lime-text", "light": "#4f5f00", "dark": "#b7c77c", "description": "Custom tag label, lime. Pairs with --color-tag-lime-surface." }, + "cerise": { "cssVar": "--color-tag-cerise-text", "light": "#991b41", "dark": "#f6a3af", "description": "Custom tag label, cerise. Pairs with --color-tag-cerise-surface." } + } + }, "chart": { "1": { "cssVar": "--color-chart-1", "light": "#0aaddf", "dark": "#45bce0", "description": "First series in charts. Blue." }, "2": { "cssVar": "--color-chart-2", "light": "#ef4d56", "dark": "#f57c78", "description": "Second series. Red." }, diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index 09acf96940a9..9acafb87ae12 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -572,7 +572,9 @@ export type APIKey = { name: string } -export type TagType = 'STALE' | 'UNHEALTHY' | 'NONE' +// Mirrors TagType in api/projects/tags/models.py. GITHUB and GITLAB were +// missing, though the UI has always branched on them to pick a VCS icon. +export type TagType = 'NONE' | 'STALE' | 'GITHUB' | 'UNHEALTHY' | 'GITLAB' export type Tag = { id: number diff --git a/frontend/documentation/CategoricalPalette.stories.tsx b/frontend/documentation/CategoricalPalette.stories.tsx index 2bcfcadc6a99..0df0931465c4 100644 --- a/frontend/documentation/CategoricalPalette.stories.tsx +++ b/frontend/documentation/CategoricalPalette.stories.tsx @@ -2,8 +2,11 @@ import React from 'react' import type { Meta, StoryObj } from 'storybook' import './docs.scss' +import Chip from 'components/base/Chip' import DocPage from './components/DocPage' import Swatch from './components/Swatch' +import tokens from 'common/theme/tokens.json' +import { AA_NORMAL_TEXT, contrastRatio } from 'common/theme/contrast' // --------------------------------------------------------------------------- // Colour data — inlined to avoid importing Constants (which pulls in the @@ -67,10 +70,10 @@ export const TagColours: StoryObj = { title='Tag colours' description={ <> - 20 decorative colours users pick from when creating tags. Will be - defined in _categorical.scss as CSS custom properties ( - --color-tag-1 through --color-tag-20). - Currently in constants.ts pending migration. These are + The 20 decorative colours users currently pick from when creating a + tag, held in constants.ts. Tags derive their fill, border + and text from these at render time, which is why most of them fail + WCAG AA. #8465 replaces that with the validated scale below. These are NOT semantic tokens — they are categorical identifiers that need to be visually distinct from each other. @@ -88,6 +91,71 @@ export const TagColours: StoryObj = { ), } +type TagEntry = { cssVar: string; light: string; dark: string } + +const TAG_SURFACES = tokens.tag.surface as Record +const TAG_TEXTS = tokens.tag.text as Record +const TAG_HUES = Object.keys(TAG_SURFACES) + +export const TagSwatches: StoryObj = { + name: 'Tag swatches', + parameters: { chromatic: { disableSnapshot: false } }, + render: () => ( + + The scale a custom tag picks from, replacing the runtime colour maths + that made contrast a function of the user’s chosen hue. Each hue + is a surface and text pair built from the + primitive ramps, so a ramp change carries through. Ratios below are + for the current theme; every pair clears AA ({AA_NORMAL_TEXT}:1) in + both, enforced by tagSwatches.test.ts. + + } + > +
+ {TAG_HUES.map((hue) => ( +
+ + {hue} + + + {contrastRatio( + TAG_SURFACES[hue].light, + TAG_TEXTS[hue].light, + ).toFixed(2)} + :1 light ·{' '} + {contrastRatio( + TAG_SURFACES[hue].dark, + TAG_TEXTS[hue].dark, + ).toFixed(2)} + :1 dark + +
+ ))} +
+

+ System tags (Issue, PR, Stale, Unhealthy) are not on this scale. They + stay on existing tokens — bg-surface-default,{' '} + border-default, text-default — plus a + coloured icon, so the state is carried by the icon rather than the fill. +

+
+ + System tag + +
+
+ ), +} + export const ProjectColours: StoryObj = { name: 'Project colours', render: () => ( diff --git a/frontend/documentation/components/Chip.stories.tsx b/frontend/documentation/components/Chip.stories.tsx index c88fe9860935..798a7ad7a484 100644 --- a/frontend/documentation/components/Chip.stories.tsx +++ b/frontend/documentation/components/Chip.stories.tsx @@ -1,7 +1,10 @@ import React from 'react' import type { Meta, StoryObj } from 'storybook' -import Chip from 'components/base/Chip' +import Chip, { ChipDot } from 'components/base/Chip' +import Icon, { IconName } from 'components/icons/Icon' +import Constants from 'common/constants' +import { getTagSwatchUtilities } from 'components/tags/tagSwatch' const meta: Meta = { args: { children: 'Production' }, @@ -11,7 +14,7 @@ const meta: Meta = { docs: { description: { component: - 'Canonical token-based chip primitive: a small labelled pill token. Layout via Bootstrap utilities, colour/radius via token utilities, padding/sizes/border/truncation in SCSS. Leading/trailing icons go in as children. Selection lives in ToggleChip and count badges are a separate Badge concern. The legacy `.chip` (old SCSS vars + manual dark-mode block, ~35×) migrates onto this under #6606.', + 'Canonical token-based chip primitive: a small labelled pill token. Layout via Bootstrap utilities, colour/radius via token utilities, padding/sizes/border/truncation in SCSS. Leading/trailing icons go in as children. `variant` covers neutral, accent, the five status colours and `solid`; `ChipDot` adds the leading dot in `currentColor`. Radius is a fixed 6px from the tags frame, so there is no shape prop. Selection lives in ToggleChip. The legacy `.chip` (old SCSS vars + manual dark-mode block, ~35×) migrates onto this under #6606.', }, }, layout: 'centered', @@ -28,6 +31,61 @@ export const Accent: Story = { args: { children: '"hello"', variant: 'accent' }, } +export const Solid: Story = { + args: { children: 'Enterprise', variant: 'solid' }, +} + +// How Tag composes Chip: a decorative colour a user picked is not a semantic +// variant, so it arrives as a tag-* swatch class in className. System +// tags take none of it, and carry their state in the icon instead. +const SYSTEM_TAGS: { label: string; icon: IconName }[] = [ + { icon: 'issue-closed', label: 'Issue closed' }, + { icon: 'issue-linked', label: 'Issue open' }, + { icon: 'pr-closed', label: 'PR closed' }, + { icon: 'pr-dequeued', label: 'PR dequeued' }, + { icon: 'pr-draft', label: 'PR draft' }, + { icon: 'stale', label: 'Stale' }, + { icon: 'pr-linked', label: 'PR open' }, + { icon: 'pr-merged', label: 'PR merged' }, +] + +export const AsSystemTag: Story = { + name: 'As a system tag', + parameters: { chromatic: { disableSnapshot: false } }, + render: () => ( +
+ {SYSTEM_TAGS.map(({ icon, label }) => ( + + {label} + + + ))} +
+ ), +} + +export const AsCustomTag: Story = { + name: 'As a custom tag', + parameters: { chromatic: { disableSnapshot: false } }, + render: () => ( +
+ {Constants.tagColors.map((colour: string) => ( + + Custom + + ))} +
+ ), +} + export const Sizes: Story = { render: () => (
@@ -38,6 +96,49 @@ export const Sizes: Story = { ), } +export const StatusVariants: Story = { + render: () => ( +
+ + + Draft + + + + Running + + + + Paused + + + + Failed + + + + Completed + +
+ ), +} + +export const Counts: Story = { + render: () => ( +
+ + 5 + + + 0 + + + 128 + +
+ ), +} + export const Removable: Story = { args: { children: 'feature-flag', onRemove: () => undefined }, } diff --git a/frontend/documentation/components/Icons.stories.tsx b/frontend/documentation/components/Icons.stories.tsx index dff1c2ba7125..e35040a5ffb8 100644 --- a/frontend/documentation/components/Icons.stories.tsx +++ b/frontend/documentation/components/Icons.stories.tsx @@ -60,6 +60,7 @@ const CATEGORIES: IconCategory[] = [ 'info-outlined', 'lock', 'shield', + 'stale', 'warning', ], label: 'Status', @@ -70,6 +71,7 @@ const CATEGORIES: IconCategory[] = [ 'issue-closed', 'issue-linked', 'pr-closed', + 'pr-dequeued', 'pr-draft', 'pr-linked', 'pr-merged', diff --git a/frontend/documentation/components/ToggleChip.stories.tsx b/frontend/documentation/components/ToggleChip.stories.tsx index b9d153a091aa..ab6c7cacc858 100644 --- a/frontend/documentation/components/ToggleChip.stories.tsx +++ b/frontend/documentation/components/ToggleChip.stories.tsx @@ -2,19 +2,19 @@ import React, { useState } from 'react' import type { Meta, StoryObj } from 'storybook' import ToggleChip from 'components/ToggleChip' -import { - colorChart2, - colorChart3, - colorSurfaceAction, -} from 'common/theme/tokens' const meta: Meta = { - argTypes: { - color: { control: 'color' }, - }, - args: { active: false, children: 'Feature flag', color: colorSurfaceAction }, + args: { active: false, children: 'Feature flag' }, component: ToggleChip, - parameters: { layout: 'centered' }, + parameters: { + docs: { + description: { + component: + 'A selectable chip with a leading checkbox. It holds no palette: colour arrives as token utilities in className, so a tag hands it a validated swatch pair rather than a hex to derive from.', + }, + }, + layout: 'centered', + }, title: 'Components/Data Display/ToggleChip', } export default meta @@ -35,16 +35,15 @@ export const Default: Story = { } export const AllStates: Story = { + parameters: { chromatic: { disableSnapshot: false } }, render: () => (
- - Active - - Inactive - - Danger + Active + Inactive + + Swatch, active - Success + Swatch
), } diff --git a/frontend/scripts/generate-tokens.mjs b/frontend/scripts/generate-tokens.mjs index c36e86cc0e42..0e7afe1cf7fb 100644 --- a/frontend/scripts/generate-tokens.mjs +++ b/frontend/scripts/generate-tokens.mjs @@ -138,6 +138,21 @@ function buildScssLines() { rootLines.push('') } + // Feature palettes. Themed like the semantic tokens, but scoped to one + // feature, so they sit outside `color` where only cross-cutting roles live. + if (json.tag) { + rootLines.push(' // Tag') + for (const [, entries] of sorted(json.tag)) { + for (const [, e] of sorted(entries)) { + rootLines.push(` ${e.cssVar}: ${toPrimitiveRef(e.light)};`) + if (e.dark && e.dark !== e.light) { + darkLines.push(` ${e.cssVar}: ${toPrimitiveRef(e.dark)};`) + } + } + } + rootLines.push('') + } + // Chart colour tokens if (json[CHART_CATEGORY]) { rootLines.push(' // Chart') @@ -470,6 +485,21 @@ function generateUtilities() { lines.push('') } + // Tag swatches. One class per hue rather than a bg/text pair, because the + // two are only accessible together: applying a fill without its label colour + // is the contrast bug this scale exists to fix. + if (json.tag) { + lines.push('// Tag swatches') + for (const [hue, surface] of sorted(json.tag.surface)) { + const text = json.tag.text[hue] + if (!text) continue + lines.push( + `.tag-${hue} { background-color: var(${surface.cssVar}); color: var(${text.cssVar}); }`, + ) + } + lines.push('') + } + // Radius utilities if (json.radius) { lines.push('// Radius') diff --git a/frontend/web/components/ToggleChip.scss b/frontend/web/components/ToggleChip.scss new file mode 100644 index 000000000000..8b8d51432155 --- /dev/null +++ b/frontend/web/components/ToggleChip.scss @@ -0,0 +1,20 @@ +// The leading checkbox. Its styles used to come from `.chip .icon-check` in +// _chip.scss, which no longer applies now ToggleChip composes the chip +// primitive, so they live with the component that owns them. +.toggle-chip__check { + width: 14px; + height: 14px; + flex-shrink: 0; + border: 1px solid var(--color-border-default); + border-radius: var(--radius-xs); + + // Checked: the tick supplies the colour, so the box gets out of its way. + &--active { + border-color: transparent; + } + + svg { + width: 14px; + height: 14px; + } +} diff --git a/frontend/web/components/ToggleChip.tsx b/frontend/web/components/ToggleChip.tsx index a56708d9e83d..ee9b88c4e9ed 100644 --- a/frontend/web/components/ToggleChip.tsx +++ b/frontend/web/components/ToggleChip.tsx @@ -1,10 +1,10 @@ import React, { FC, ReactNode } from 'react' import cx from 'classnames' import Icon from './icons/Icon' -import Utils from 'common/utils/utils' +import Chip from './base/Chip' +import './ToggleChip.scss' type ToggleChipProps = { - color?: string active?: boolean onClick?: () => void className?: string @@ -15,37 +15,26 @@ const ToggleChip: FC = ({ active, children, className, - color, onClick, -}) => { - const colour = Utils.colour(color) - return ( - ( + + {/* Without a label this is a bare swatch, e.g. the tag colour picker, where + the box would be noise: the tick alone marks the selection. */} + - - {active && } - - {children} - - ) -} + {active && } + + {children} + +) export default ToggleChip diff --git a/frontend/web/components/base/Chip/Chip.scss b/frontend/web/components/base/Chip/Chip.scss index 854427ce3f98..3407d57ff58d 100644 --- a/frontend/web/components/base/Chip/Chip.scss +++ b/frontend/web/components/base/Chip/Chip.scss @@ -13,13 +13,38 @@ border-color: var(--color-border-action); } + // Status colours carry their meaning in the fill, so an outline only adds + // noise. Transparent rather than removed, so a status chip is the same + // height as a bordered one beside it. + &--success, + &--warning, + &--danger, + &--info, + &--muted, + &--solid { + border-color: transparent; + } + + // Status dot. currentColor, so it follows the variant with nothing to wire. + &__dot { + width: 6px; + height: 6px; + border-radius: var(--radius-full); + background: currentColor; + flex-shrink: 0; + } + // Sizes (default is the base above). &--sm { padding: 3px 8px; font-size: 0.75rem; } + // 24px is fixed in the tags frame, so it is stated rather than left to derive + // from the inherited line-height. The frame's 8px vertical padding is an + // artefact of a height override on the auto-layout and is not applied. &--xs { + height: 24px; padding: 1px 6px; font-size: 0.6875rem; } diff --git a/frontend/web/components/base/Chip/Chip.tsx b/frontend/web/components/base/Chip/Chip.tsx index 296219372dda..e381e5d9a064 100644 --- a/frontend/web/components/base/Chip/Chip.tsx +++ b/frontend/web/components/base/Chip/Chip.tsx @@ -5,7 +5,18 @@ import { colorIconSecondary } from 'common/theme/tokens' import './Chip.scss' export type ChipSize = 'default' | 'sm' | 'xs' -export type ChipVariant = 'neutral' | 'accent' +export type ChipVariant = + | 'neutral' + | 'accent' + | 'success' + | 'warning' + | 'danger' + | 'info' + | 'muted' + | 'solid' + // The caller supplies the colour through className. Used by tags, whose + // colour is a user's decorative choice rather than a semantic role. + | 'none' export type ChipProps = { children: ReactNode @@ -30,7 +41,24 @@ export type ChipProps = { // bg + text come from token utilities; the variant border lives in Chip.scss. const VARIANT_UTILITIES: Record = { accent: 'bg-surface-action-subtle text-action', + + danger: 'bg-surface-danger text-danger', + + info: 'bg-surface-info text-info', + + muted: 'bg-surface-muted text-secondary', + neutral: 'bg-surface-subtle text-default', + // No utilities: a caller-supplied colour class would otherwise have to beat + // these on source order alone, which a reordered stylesheet would break. + none: '', + // The one filled variant. `text-white` rather than a token because there is + // no inverse-text token yet; white on --color-surface-action is 5.93:1, so AA + // but not AAA. Note the app has a second, darker solid (`bg-primary900`, used + // by BetaFlag and PlanBasedAccess) that this deliberately does not cover. + solid: 'bg-surface-action text-white', + success: 'bg-surface-success text-success', + warning: 'bg-surface-warning text-warning', } // Token-based chip primitive. Uses `ds-chip` rather than the legacy `.chip` @@ -58,10 +86,12 @@ const Chip = ({