Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,8 @@ export type APIKey = {
name: string
}

export type TagType = 'STALE' | 'UNHEALTHY' | 'NONE'
// Mirrors TagType in api/projects/tags/models.py.
export type TagType = 'NONE' | 'STALE' | 'GITHUB' | 'UNHEALTHY' | 'GITLAB'

export type Tag = {
id: number
Expand Down
5 changes: 5 additions & 0 deletions frontend/common/utils/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,11 @@ const Utils = Object.assign({}, BaseUtils, {
return tag?.type === 'STALE' && !hasStaleFlagsPermission
},

// Unhealthy tags exist only where Feature Health does. Asked at the list
// rather than inside Tag: a chip has no business reading feature flags.
tagVisible: (tag: Tag | undefined) =>
tag?.type !== 'UNHEALTHY' || Utils.getFlagsmithHasFeature('feature_health'),

toKebabCase: (string: string) =>
string
.replace(/([a-z])([A-Z])/g, '$1-$2')
Expand Down
20 changes: 9 additions & 11 deletions frontend/documentation/CategoricalPalette.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Chip from 'components/base/Chip'
import DocPage from './components/DocPage'
import Swatch from './components/Swatch'
import tokens from 'common/theme/tokens.json'
import { contentColourNames, contentColours } from 'common/theme/tokens'
import { AA_NORMAL_TEXT, contrastRatio } from 'common/theme/contrast'

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -45,9 +46,9 @@ const PRIMITIVES = tokens.primitives as Record<string, string>
// own surface, so it does not follow the page. One ink serves all of them.
const TAG_INK_NAME = 'content-always-dark'
const TAG_INK = PRIMITIVES[TAG_INK_NAME]
const TAG_FILLS = Object.entries(PRIMITIVES)
.filter(([name]) => name.startsWith('content-') && name !== TAG_INK_NAME)
.map(([name, hex]) => [name.replace('content-', ''), hex] as const)
const TAG_FILLS = contentColourNames.map(
(name) => [name, contentColours[name]] as const,
)

export const TagSwatches: StoryObj = {
name: 'Tag swatches',
Expand All @@ -72,7 +73,7 @@ export const TagSwatches: StoryObj = {
className='d-flex flex-column align-items-center gap-1'
key={name}
>
<Chip className={`border-0 tag-${name}`} size='xs'>
<Chip colour={name} size='xs'>
{name}
</Chip>
<small className='text-secondary'>
Expand All @@ -83,15 +84,12 @@ export const TagSwatches: StoryObj = {
</div>
<p className='cat-note'>
System tags (Issue, PR, Stale, Unhealthy) are not on this scale. They
stay on existing tokens &mdash; <code>bg-surface-default</code>,{' '}
<code>border-default</code>, <code>text-default</code> &mdash; plus a
coloured icon, so the state is carried by the icon rather than the fill.
take no fill at all: <code>border-default</code> and{' '}
<code>text-default</code> plus a coloured icon, so the state is carried
by the icon and the border rather than the fill.
</p>
<div className='d-flex mt-3'>
<Chip
className='bg-surface-default border-default text-default'
size='xs'
>
<Chip size='xs' variant='outline'>
System tag
</Chip>
</div>
Expand Down
49 changes: 47 additions & 2 deletions frontend/documentation/components/Chip.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from 'react'
import type { Meta, StoryObj } from 'storybook'

import Chip from 'components/base/Chip'
import Icon, { IconName } from 'components/icons/Icon'
import { contentColourNames } from 'common/theme/tokens'

const meta: Meta<typeof Chip> = {
args: { children: 'Production' },
Expand All @@ -11,7 +13,7 @@ const meta: Meta<typeof Chip> = {
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.',
'A small labelled pill. Layout comes from Bootstrap utilities and colour from token utilities; padding, sizes, border and truncation are in SCSS. Icons go in as children. `none` leaves the colour to the caller, for a colour a user picked rather than a semantic role.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the description: Chip has no none option.

The description says "none leaves the colour to the caller". ChipVariant is 'neutral' | 'accent' | 'outline', and a caller-chosen colour goes through the colour prop. The Storybook docs therefore describe an API that does not exist. Describe the colour prop instead.

},
},
layout: 'centered',
Expand All @@ -28,10 +30,53 @@ export const Accent: Story = {
args: { children: '"hello"', variant: 'accent' },
}

// How Tag composes Chip: a decorative colour a user picked is not a semantic
// variant, so it arrives on `colour`. System tags take neither, 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: () => (
<div className='d-flex flex-wrap gap-2'>
{SYSTEM_TAGS.map(({ icon, label }) => (
<Chip key={label} size='xs' variant='outline'>
{label}
<Icon name={icon} />
</Chip>
))}
</div>
),
}

export const AsCustomTag: Story = {
name: 'As a custom tag',
parameters: { chromatic: { disableSnapshot: false } },
render: () => (
<div className='d-flex flex-wrap gap-2'>
{contentColourNames.map((colour) => (
<Chip colour={colour} key={colour} size='xs'>
Custom
</Chip>
))}
</div>
),
}

export const Sizes: Story = {
render: () => (
<div className='d-flex align-items-center gap-2'>
<Chip size='default'>Default</Chip>
<Chip size='md'>Medium</Chip>
<Chip size='sm'>Small</Chip>
<Chip size='xs'>Extra small</Chip>
</div>
Expand Down
99 changes: 99 additions & 0 deletions frontend/documentation/components/Tag.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React, { useState } from 'react'
import type { Meta, StoryObj } from 'storybook'

import Tag from 'components/tags/Tag'
import Constants from 'common/constants'
import { contentColourNames, contentColours } from 'common/theme/tokens'
import type { Tag as TTag } from 'common/types/responses'

const meta: Meta<typeof Tag> = {
component: Tag,
parameters: {
chromatic: { disableSnapshot: false },
docs: {
description: {
component:
'A project tag. Custom tags take a fill from the design system Content palette, keyed on the colour stored on the tag so nothing needs migrating. System tags (Stale, GitHub, GitLab, Unhealthy) take no fill and carry their state in a coloured icon and their border rather than the fill, so the state survives for anyone who cannot tell the fills apart.',
},
},
layout: 'padded',
},
title: 'Components/Tags/Tag',
}
export default meta

type Story = StoryObj<typeof Tag>

const tag = (over: Partial<TTag>): Partial<TTag> => ({
color: Constants.tagColors[0],
label: 'Checkout',
type: 'NONE',
...over,
})

export const Custom: Story = { args: { tag: tag({}) } }

export const EveryColour: Story = {
name: 'Every colour',
render: () => (
<div className='d-flex flex-wrap gap-1'>
{contentColourNames.map((name) => (
<Tag
key={name}
tag={tag({ color: contentColours[name], label: 'Checkout' })}
/>
))}
</div>
),
}

// The VCS icon is keyed on the tag's label, which the integration sets, so the
// examples use labels it actually produces rather than the type name.
// No Unhealthy example: Tag returns null for it unless the feature_health flag
// is on, and Storybook's Utils stub answers false to every flag.
Comment on lines +52 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the stale comment about Tag and Unhealthy tags.

The comment says Tag returns null for an Unhealthy tag unless feature_health is on. The new Tag does not read feature flags. Visibility now belongs to Utils.tagVisible in the callers. Update the comment, or add an Unhealthy example, because Tag now renders one.

const SYSTEM_EXAMPLES: Partial<TTag>[] = [
{ label: 'Stale', type: 'STALE' },
{ label: 'PR Open', type: 'GITHUB' },
{ label: 'PR Merged', type: 'GITHUB' },
{ label: 'Issue Open', type: 'GITLAB' },
{ label: 'Issue Closed', type: 'GITLAB' },
]

export const SystemTags: Story = {
name: 'System tags',
render: () => (
<div className='d-flex flex-wrap gap-1'>
{SYSTEM_EXAMPLES.map((over) => (
<Tag key={over.label} tag={tag(over)} />
))}
</div>
),
}

// A colour we never issued, which the API allows. It takes the neutral rather
// than a guess: the label still reads, and the tag claims no category.
export const UnknownColour: Story = {
args: { tag: tag({ color: '#123456', label: 'Set via API' }) },
name: 'Colour outside the scale',
}

/** Hooks cannot live in a story's render, so selection state gets a component. */
const SelectableTags: React.FC = () => {
const [picked, setPicked] = useState<string>('Checkout')
return (
<div className='d-flex flex-wrap gap-1'>
{['Checkout', 'Billing', 'Search'].map((label, i) => (
<Tag
key={label}
onClick={() => setPicked(label)}
selected={picked === label}
tag={tag({ color: Constants.tagColors[i], label })}
/>
))}
</div>
)
}

export const Selectable: Story = {
render: () => <SelectableTags />,
}
50 changes: 0 additions & 50 deletions frontend/documentation/components/ToggleChip.stories.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const meta: Meta<typeof VCSProviderTag> = {
},
component: VCSProviderTag,
parameters: { layout: 'centered' },
title: 'Components/Data Display/VCSProviderTag',
title: 'Components/Tags/VCSProviderTag',
}
export default meta

Expand Down
5 changes: 3 additions & 2 deletions frontend/web/components/ColorSwatch.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { FC } from 'react'
import classNames from 'classnames'

type ColorSwatchSize = 'sm' | 'md' | 'lg'
type ColorSwatchSize = 'sm' | 'md' | 'lg' | 'xl'
type ColorSwatchShape = 'square' | 'circle'

type ColorSwatchProps = {
color: string
color?: string
size?: ColorSwatchSize
shape?: ColorSwatchShape
className?: string
Expand All @@ -15,6 +15,7 @@ const SIZE_MAP: Record<ColorSwatchSize, number> = {
lg: 16,
md: 12,
sm: 8,
xl: 32,
}

const SHAPE_CLASS: Record<ColorSwatchShape, string> = {
Expand Down
51 changes: 0 additions & 51 deletions frontend/web/components/ToggleChip.tsx

This file was deleted.

7 changes: 1 addition & 6 deletions frontend/web/components/base/Chip/Chip.scss
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
// Canonical token-based chip. Layout and bg/text colour are utilities in the
// markup; this holds padding, sizes, the variant border colour, truncation and
// the remove-button reset. Distinct class (`ds-chip`) until the legacy `.chip`
// (_chip.scss) migrates here under #6606.
.ds-chip {
padding: 4px 10px;
font-size: 0.8125rem;
white-space: nowrap;
border: 1px solid var(--color-border-default);
border: 1px solid var(--ds-chip-border, var(--color-border-default));

// Colour variant - bg/text are utilities; only the border colour is here.
&--accent {
border-color: var(--color-border-action);
}
Expand Down
Loading
Loading