;
+
+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`Value: red`,
+ 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`
+
+ ${Object.entries(Color).map(
+ ([key, value]) =>
+ html`${capitalize(key)}`
+ )}
+
+ `,
+};
+
+export const SemanticColors: Story = {
+ render: () => html`
+
+ ${(
+ [
+ ['Neutral', Color.Neutral],
+ ['Accent', Color.Accent],
+ ['Success', Color.Success],
+ ['Warning', Color.Warning],
+ ['Danger', Color.Danger],
+ ['Info', Color.Info],
+ ] as const
+ ).map(
+ ([label, value]) =>
+ html`${label}`
+ )}
+
+ `,
+};
+
+export const CustomPrefix: Story = {
+ render: () => html`
+
+
+ Custom prefix
+
+ `,
+ play: async ({canvasElement}) => {
+ const host = canvasElement.querySelector('craft-badge')!;
+ // A slotted prefix overrides the default indicator fallback.
+ const slot = host.shadowRoot!.querySelector(
+ 'slot[name="prefix"]'
+ )!;
+ const [prefix] = slot.assignedElements();
+ await expect(prefix).toBeTruthy();
+ await expect(prefix?.tagName.toLowerCase()).toBe('craft-icon');
+ },
+};
diff --git a/packages/craftcms-cp/src/components/badge/badge.styles.ts b/packages/craftcms-cp/src/components/badge/badge.styles.ts
new file mode 100644
index 00000000000..5953a36e0fb
--- /dev/null
+++ b/packages/craftcms-cp/src/components/badge/badge.styles.ts
@@ -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);
+ }
+`;
diff --git a/packages/craftcms-cp/src/components/badge/badge.ts b/packages/craftcms-cp/src/components/badge/badge.ts
new file mode 100644
index 00000000000..f5a532870ed
--- /dev/null
+++ b/packages/craftcms-cp/src/components/badge/badge.ts
@@ -0,0 +1,79 @@
+import {html, LitElement} from 'lit';
+import {property} from 'lit/decorators.js';
+import type {CSSResultGroup, PropertyValues} from 'lit';
+import styles from './badge.styles.js';
+import {Color, type ColorValue} from '@src/constants/colors';
+import '../indicator/indicator.js';
+import {classMap} from 'lit/directives/class-map.js';
+
+/**
+ * @summary A colored status pill: a `` dot (by default)
+ * followed by a label. `fill` sets the badge color from the shared `Color`
+ * palette — the surface renders in the quiet tone of that color and the
+ * indicator in the loud tone.
+ *
+ * @slot - The label content shown after the indicator.
+ * @slot prefix - The leading content; defaults to a `` whose
+ * fill is derived from `fill`.
+ * @slot suffix - The trailing content.
+ *
+ * @csspart badge - The badge wrapper.
+ * @csspart prefix - The leading slot, rendered before the label.
+ * @csspart indicator - The default indicator shown in the prefix slot.
+ * @csspart suffix - The trailing slot, rendered after the label.
+ *
+ * @since 1.0
+ */
+export default class CraftBadge extends LitElement {
+ static override styles: CSSResultGroup = [styles];
+
+ /** The badge color — a color value from `Color` (e.g. `red`, `emerald`). */
+ @property({reflect: true}) fill: ColorValue = Color.Gray;
+
+ /** The resolved color value used for the badge fill. */
+ private getFill(): ColorValue {
+ return this.fill;
+ }
+
+ protected override willUpdate(changed: PropertyValues): void {
+ // Set the colorable context from `fill` so the badge's own surface/border/
+ // text colors (which read --c-color-*) reflect the chosen color.
+ if (changed.has('fill')) {
+ this.dataset.color = this.getFill()
+ }
+ }
+
+ override render() {
+ return html`
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+}
+
+if (!customElements.get('craft-badge')) {
+ customElements.define('craft-badge', CraftBadge);
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'craft-badge': CraftBadge;
+ }
+}
diff --git a/packages/craftcms-cp/src/components/indicator/Indicator.mdx b/packages/craftcms-cp/src/components/indicator/Indicator.mdx
index 3bdf20d3bcf..372036779bc 100644
--- a/packages/craftcms-cp/src/components/indicator/Indicator.mdx
+++ b/packages/craftcms-cp/src/components/indicator/Indicator.mdx
@@ -1,5 +1,9 @@
import {Canvas, Meta} from '@storybook/addon-docs/blocks';
-import IndicatorMeta, {ArbitraryColor, ArbitrarySize, Default,} from './indicator.stories';
+import IndicatorMeta, {
+ ArbitraryColor,
+ ArbitrarySize,
+ Default,
+} from './indicator.stories';
@@ -168,8 +172,8 @@ wrapper to fine-tune it.
- The dot is exposed as `role="img"`, and `label` becomes its `aria-label`.
- A status dot conveys meaning through color alone, so set `label` whenever the
-indicator isn't purely decorative. If it only repeats adjacent visible text,
-it's fine to leave `label` unset.
+ indicator isn't purely decorative. If it only repeats adjacent visible text,
+ it's fine to leave `label` unset.
## Styling
diff --git a/packages/craftcms-cp/src/components/indicator/indicator.ts b/packages/craftcms-cp/src/components/indicator/indicator.ts
index 7aac33d676b..6dcff593798 100644
--- a/packages/craftcms-cp/src/components/indicator/indicator.ts
+++ b/packages/craftcms-cp/src/components/indicator/indicator.ts
@@ -1,4 +1,4 @@
-import {css, html, LitElement} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
import {property} from 'lit/decorators.js';
import {classMap} from 'lit/directives/class-map.js';
import variantsStyles from '@src/styles/variants.styles';
@@ -84,10 +84,12 @@ export default class CraftIndicator extends LitElement {
}
protected override render(): unknown {
+ // Without a label the indicator is purely decorative, so omit the image role
+ // and name rather than exposing an unnamed `role="img"`.
return html`and($labelHtml)->toContain('Pending')
+ ->and($editedHtml)->toContain('and($editedHtml)->toContain('slot="prefix"')
->and($editedHtml)->toContain('Edited');
});
});