Skip to content
Draft
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 packages/craftcms-cp/scripts/generate-colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const availableColors = [
'white',
'gray',
'black',
'slate',
];

const semanticColors = {
Expand Down Expand Up @@ -169,7 +170,7 @@ ${buildColorableTokens()}
${buildSemanticTokens()}
}

${[...availableColors, ...Object.values(semanticColors)].map((c) => buildStyleBlock(c)).join('\n')}
${[...availableColors, ...Object.keys(semanticColors)].map((c) => buildStyleBlock(c)).join('\n')}
`;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import variantsStyles from '@src/styles/variants.styles';
import {classMap} from 'lit/directives/class-map.js';

import '../shortcut/shortcut.js';
import {type ActionFeedback, type BaseAction, type FeedbackData, runAction,} from '@src/actions';
import {
type ActionFeedback,
type BaseAction,
type FeedbackData,
runAction,
} from '@src/actions';
import {Variant, type VariantKey} from '@src/constants/variants';

/**
Expand Down
194 changes: 194 additions & 0 deletions packages/craftcms-cp/src/components/badge/Badge.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import {Canvas, Meta} from '@storybook/addon-docs/blocks';
import BadgeMeta, {
Default,
AllColors,
SemanticColors,
CustomPrefix,
} from './badge.stories';

<Meta of={BadgeMeta} name="Docs" />

# Badge

`<craft-badge>` is a small status pill — a color `<craft-indicator>` dot followed
by a label. Use it to surface an object's state in lists, tables, and headers
(for example an entry's status or an order's payment state).

<Canvas of={Default} />

## Usage

```html
<craft-badge fill="success">Live</craft-badge>
```

The label is the default slot. By default the badge prepends a
`<craft-indicator>` tinted to match `fill`; the badge's own surface, border, and
text render in the quieter tone of the same color.

## Properties

<table>
<thead>
<tr>
<th>Property</th>
<th>Attribute</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>fill</code>
</td>
<td>
<code>fill</code>
</td>
<td>
<code>ColorValue</code>
</td>
<td>
<code>gray</code>
</td>
<td>
The badge color — a value from the shared <code>Color</code> palette
(see <a href="#fill">Fill</a>). Reflected to the <code>fill</code>{' '}
attribute.
</td>
</tr>
</tbody>
</table>

## Fill

`fill` accepts a color **value** from the shared `Color` constant — a palette
swatch (`red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`,
`cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`,
`gray`, `white`, `black`).

Unlike `<craft-indicator>`, the badge is constrained to the palette and does not
take arbitrary CSS colors — `fill` is typed as `ColorValue`, so only known
values type-check. Setting `fill` reflects the resolved color onto the host's
`data-color` attribute, which scopes the `--c-color-*` tokens the badge styles
read.

<Canvas of={AllColors} />

### Semantic colors

The `Color` constant also exposes semantic aliases that resolve to palette
values — `Color.Success` → `emerald`, `Color.Warning` → `orange`,
`Color.Danger` → `red`, `Color.Info` → `blue`, `Color.Neutral` → `slate`, and
`Color.Accent` → `red`. Prefer these when the color carries meaning so intent
survives any future palette retune.

```html
<craft-badge fill="emerald">Success</craft-badge>
```

<Canvas of={SemanticColors} />

## Slots

<table>
<thead>
<tr>
<th>Slot</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<em>(default)</em>
</td>
<td>The label content, shown after the indicator.</td>
</tr>
<tr>
<td>
<code>prefix</code>
</td>
<td>
Leading content. Defaults to a <code>&lt;craft-indicator&gt;</code>{' '}
filled from <code>fill</code>; slot your own element to replace it.
</td>
</tr>
<tr>
<td>
<code>suffix</code>
</td>
<td>Trailing content, shown after the label.</td>
</tr>
</tbody>
</table>

Slotting `prefix` overrides the default indicator — handy for an icon or a
custom marker:

<Canvas of={CustomPrefix} />

## CSS parts

<table>
<thead>
<tr>
<th>Part</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>badge</code>
</td>
<td>The pill wrapper.</td>
</tr>
<tr>
<td>
<code>prefix</code>
</td>
<td>The leading slot, before the label.</td>
</tr>
<tr>
<td>
<code>indicator</code>
</td>
<td>The default indicator rendered in the prefix slot.</td>
</tr>
<tr>
<td>
<code>suffix</code>
</td>
<td>The trailing slot, after the label.</td>
</tr>
</tbody>
</table>

## Styling

The badge sizes itself in `em` and reads the generic colorable tokens, so it
adapts to the surrounding text and to `fill` automatically:

- Surface — `--c-color-fill-quiet`
- Border — `--c-color-border-quiet`
- Text — `--c-color-on-quiet`

Because those tokens are scoped by the `data-color` attribute the component sets
from `fill`, you normally change a badge's color through `fill` rather than by
overriding the tokens. Set `font-size` on the host to scale the whole pill,
including its indicator:

```css
craft-badge {
font-size: 0.875rem;
}
```

## Accessibility

The default indicator is decorative and is left unlabeled, since the badge's
text already names the status. If you replace the `prefix` with a meaningful
graphic, give it its own accessible name (for example a `label` on
`<craft-indicator>` or `<craft-icon>`).
105 changes: 105 additions & 0 deletions packages/craftcms-cp/src/components/badge/badge.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type {Meta, StoryObj} from '@storybook/web-components-vite';

import {html} from 'lit';
import {expect} from 'storybook/test';

import './badge.js';
import type CraftBadge from './badge.js';
import {Color, colors} from '@src/constants/colors';
import {capitalize} from '@src/utilities/string';
import '../icon/icon.js';

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
const meta = {
title: 'Components/Badge',
component: 'craft-badge',
args: {
fill: 'gray',
},
argTypes: {
fill: {control: 'select', options: colors},
},
render: (args) => html`
<craft-badge fill=${args.fill}>
${capitalize(String(args.fill))}
</craft-badge>
`,
} satisfies Meta<CraftBadge>;

export default meta;
type Story = StoryObj<CraftBadge>;

export const Default: Story = {
play: async ({canvasElement}) => {
const host = canvasElement.querySelector('craft-badge')!;
// The default prefix renders an indicator …
const indicator = host.shadowRoot!.querySelector('craft-indicator')!;
await expect(indicator).toBeTruthy();
// … filled in the loud tone of `fill` …
await expect(indicator.getAttribute('fill')).toBe(
'var(--c-color-gray-fill-loud)'
);
// … and the host reflects the resolved color for its own surface styling.
await expect(host.dataset.color).toBe('gray');
},
};

/** `fill` takes a color value from `Color` (e.g. `red`). */
export const FromColorValue: Story = {
render: () => html`<craft-badge fill="red">Value: red</craft-badge>`,
play: async ({canvasElement}) => {
const badge = canvasElement.querySelector('craft-badge')!;
// The badge reflects the resolved color for its own surface styling.
await expect(badge.dataset.color).toBe('red');
},
};

export const AllColors: Story = {
render: () => html`
<div style="display: grid; gap: 0.5rem; justify-items: start">
${Object.entries(Color).map(
([key, value]) =>
html`<craft-badge fill=${value}>${capitalize(key)}</craft-badge>`
)}
</div>
`,
};

export const SemanticColors: Story = {
render: () => html`
<div style="display: grid; gap: 0.5rem; justify-items: start">
${(
[
['Neutral', Color.Neutral],
['Accent', Color.Accent],
['Success', Color.Success],
['Warning', Color.Warning],
['Danger', Color.Danger],
['Info', Color.Info],
] as const
).map(
([label, value]) =>
html`<craft-badge fill=${value}>${label}</craft-badge>`
)}
</div>
`,
};

export const CustomPrefix: Story = {
render: () => html`
<craft-badge fill=${Color.Green}>
<craft-icon slot="prefix" name="circle-check" label="Done"></craft-icon>
Custom prefix
</craft-badge>
`,
play: async ({canvasElement}) => {
const host = canvasElement.querySelector('craft-badge')!;
// A slotted prefix overrides the default indicator fallback.
const slot = host.shadowRoot!.querySelector<HTMLSlotElement>(
'slot[name="prefix"]'
)!;
const [prefix] = slot.assignedElements();
await expect(prefix).toBeTruthy();
await expect(prefix?.tagName.toLowerCase()).toBe('craft-icon');
},
};
25 changes: 25 additions & 0 deletions packages/craftcms-cp/src/components/badge/badge.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {css} from 'lit';

export default css`
:host {
display: inline-flex;
}

.badge {
display: inline-flex;
align-items: center;
background-color: var(--c-color-fill-quiet);
border: 1px solid var(--c-color-border-quiet);
color: var(--c-color-on-quiet);
border-radius: var(--c-radius-full);
font-size: 0.9em;
}

.badge__prefix {
padding-inline: calc(var(--c-spacing-md) / 2);
}

.badge__suffix {
padding-inline: calc(var(--c-spacing-md) / 2);
}
`;
Loading
Loading