diff --git a/packages/craftcms-cp/scripts/generate-colors.js b/packages/craftcms-cp/scripts/generate-colors.js index 98f80432ee6..5b027768539 100644 --- a/packages/craftcms-cp/scripts/generate-colors.js +++ b/packages/craftcms-cp/scripts/generate-colors.js @@ -31,6 +31,7 @@ const availableColors = [ 'white', 'gray', 'black', + 'slate', ]; const semanticColors = { @@ -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')} `; } diff --git a/packages/craftcms-cp/src/components/action-item/action-item.ts b/packages/craftcms-cp/src/components/action-item/action-item.ts index 4a273da2c07..dac3569f228 100644 --- a/packages/craftcms-cp/src/components/action-item/action-item.ts +++ b/packages/craftcms-cp/src/components/action-item/action-item.ts @@ -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'; /** diff --git a/packages/craftcms-cp/src/components/badge/Badge.mdx b/packages/craftcms-cp/src/components/badge/Badge.mdx new file mode 100644 index 00000000000..86d0e54e33e --- /dev/null +++ b/packages/craftcms-cp/src/components/badge/Badge.mdx @@ -0,0 +1,194 @@ +import {Canvas, Meta} from '@storybook/addon-docs/blocks'; +import BadgeMeta, { + Default, + AllColors, + SemanticColors, + CustomPrefix, +} from './badge.stories'; + + + +# Badge + +`` is a small status pill — a color `` 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). + + + +## Usage + +```html +Live +``` + +The label is the default slot. By default the badge prepends a +`` tinted to match `fill`; the badge's own surface, border, and +text render in the quieter tone of the same color. + +## Properties + + + + + + + + + + + + + + + + + + + + +
PropertyAttributeTypeDefaultDescription
+ fill + + fill + + ColorValue + + gray + + The badge color — a value from the shared Color palette + (see Fill). Reflected to the fill{' '} + attribute. +
+ +## 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 ``, 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. + + + +### 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 +Success +``` + + + +## Slots + + + + + + + + + + + + + + + + + + + + + + +
SlotDescription
+ (default) + The label content, shown after the indicator.
+ prefix + + Leading content. Defaults to a <craft-indicator>{' '} + filled from fill; slot your own element to replace it. +
+ suffix + Trailing content, shown after the label.
+ +Slotting `prefix` overrides the default indicator — handy for an icon or a +custom marker: + + + +## CSS parts + + + + + + + + + + + + + + + + + + + + + + + + + + +
PartDescription
+ badge + The pill wrapper.
+ prefix + The leading slot, before the label.
+ indicator + The default indicator rendered in the prefix slot.
+ suffix + The trailing slot, after the label.
+ +## 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 +`` or ``). diff --git a/packages/craftcms-cp/src/components/badge/badge.stories.ts b/packages/craftcms-cp/src/components/badge/badge.stories.ts new file mode 100644 index 00000000000..c7930d6e191 --- /dev/null +++ b/packages/craftcms-cp/src/components/badge/badge.stories.ts @@ -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` + + ${capitalize(String(args.fill))} + + `, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +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'); }); });