diff --git a/packages/craftcms-cp/.storybook/preview.css b/packages/craftcms-cp/.storybook/preview.css index 4809e00ea04..790bafc37d9 100644 --- a/packages/craftcms-cp/.storybook/preview.css +++ b/packages/craftcms-cp/.storybook/preview.css @@ -70,3 +70,20 @@ font-size: 12px; color: rgba(0, 0, 0, 0.5); } + +table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; + text-align: left; +} + +table th, +table td { + border: 1px solid rgba(0, 0, 0, 0.2); + padding: 0.5em; +} + +table th { + background-color: rgba(0, 0, 0, 0.05); +} diff --git a/packages/craftcms-cp/scripts/generate-colors.js b/packages/craftcms-cp/scripts/generate-colors.js index ce8f737df05..98f80432ee6 100644 --- a/packages/craftcms-cp/scripts/generate-colors.js +++ b/packages/craftcms-cp/scripts/generate-colors.js @@ -169,7 +169,7 @@ ${buildColorableTokens()} ${buildSemanticTokens()} } -${[...availableColors, ...semanticColors].map((c) => buildStyleBlock(c)).join('\n')} +${[...availableColors, ...Object.values(semanticColors)].map((c) => buildStyleBlock(c)).join('\n')} `; } diff --git a/packages/craftcms-cp/src/actions/index.ts b/packages/craftcms-cp/src/actions/index.ts index 7f5a8fd29aa..1e90fe236f7 100644 --- a/packages/craftcms-cp/src/actions/index.ts +++ b/packages/craftcms-cp/src/actions/index.ts @@ -1,4 +1,4 @@ -import type {VariantKey} from '@src/types'; +import type {VariantKey} from '@src/constants/variants'; export type BaseAction = | {type: 'clipboard'; value: string} 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 f381c386510..4a273da2c07 100644 --- a/packages/craftcms-cp/src/components/action-item/action-item.ts +++ b/packages/craftcms-cp/src/components/action-item/action-item.ts @@ -1,22 +1,13 @@ import {html, LitElement, nothing} from 'lit'; import {property, state} from 'lit/decorators.js'; import styles from './action-item.styles.js'; -import { - type AsyncState, - AsyncStates, - Variant, - type VariantKey, -} from '@src/types'; +import {type AsyncState, AsyncStates} from '@src/types'; 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'; /** * @summary Either a link or button typically used in a menu. diff --git a/packages/craftcms-cp/src/components/callout/callout.stories.ts b/packages/craftcms-cp/src/components/callout/callout.stories.ts index bde500768f6..861647c8eaf 100644 --- a/packages/craftcms-cp/src/components/callout/callout.stories.ts +++ b/packages/craftcms-cp/src/components/callout/callout.stories.ts @@ -3,10 +3,8 @@ import type {Meta, StoryObj} from '@storybook/web-components-vite'; import {html} from 'lit'; import './callout.js'; -import {Appearance, Variant} from '@src/types'; - -const variants = Object.values(Variant); -const appearances = Object.values(Appearance); +import {appearances} from '@src/constants/appearances.js'; +import {variants} from '@src/constants/variants.js'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories const meta = { diff --git a/packages/craftcms-cp/src/components/callout/callout.ts b/packages/craftcms-cp/src/components/callout/callout.ts index 24fe343e05f..c1436886617 100644 --- a/packages/craftcms-cp/src/components/callout/callout.ts +++ b/packages/craftcms-cp/src/components/callout/callout.ts @@ -2,12 +2,8 @@ import {type CSSResultGroup, html, LitElement, nothing} from 'lit'; import {property} from 'lit/decorators.js'; import styles from './callout.styles.js'; import '../icon/icon.js'; -import { - Appearance, - type AppearanceKey, - Variant, - type VariantKey, -} from '@src/types/index.js'; +import {Appearance, type AppearanceKey} from '@src/constants/appearances'; +import {Variant, type VariantKey} from '@src/constants/variants'; import variantsStyles from '@src/styles/variants.styles.js'; export default class CraftCallout extends LitElement { diff --git a/packages/craftcms-cp/src/components/indicator/Indicator.mdx b/packages/craftcms-cp/src/components/indicator/Indicator.mdx new file mode 100644 index 00000000000..3bdf20d3bcf --- /dev/null +++ b/packages/craftcms-cp/src/components/indicator/Indicator.mdx @@ -0,0 +1,186 @@ +import {Canvas, Meta} from '@storybook/addon-docs/blocks'; +import IndicatorMeta, {ArbitraryColor, ArbitrarySize, Default,} from './indicator.stories'; + + + +# Indicator + +`` is a small dot used to visually represent the status of an +object — for example, the live/draft state of an entry or a "has updates" marker +in a list. + + + +## Usage + +```html + +``` + +The element renders a single `role="img"` dot. It has no slot — all of its +appearance is driven by attributes. + +## Properties + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyAttributeTypeDefaultDescription
+ fill + + fill + + string + + var(--c-color-fill-loud) + + The dot color. Accepts a semantic keyword (see below) or any CSS color. +
+ appearance + + appearance + + 'filled' | 'outlined' | 'filled-outlined' + + 'filled-outlined' + + How the dot is drawn — see Appearance. +
+ size + + size + + 'md' | 'lg' + + 'md' + + md ≈ 0.6em, lg ≈ 1em. Scales with the + surrounding font size. +
+ label + + label + + string | null + + null + + Accessible name, exposed as aria-label. Provide it for + non-decorative dots. +
+ +## Fill + +`fill` sets the dot's color. A recognized **status variant** (`default`, +`success`, `warning`, `danger`, `info`) or **palette swatch** (`red`, `orange`, +`amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, +`indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`, `gray`, `white`, +`black`) resolves to the matching `--c-color--fill-loud` design token. Any +other value — a hex code, `rgb()`, a gradient, or a custom property — is used +verbatim. + +Prefer the variant and swatch names so indicators stay consistent with the rest +of the palette; reach for an arbitrary value only when a color falls outside it. + + + +## Appearance + +`appearance` controls how the dot is drawn: + + + + + + + + + + + + + + + + + + + + + + +
ValueRenders
+ filled-outlined (default) + Filled dot with a subtle dark outline.
+ filled + Filled dot with no outline.
+ outlined + + Hollow dot — a 2px ring in the fill color over a transparent center. +
+ +## Sizing + +`size` switches between two preset sizes. Because the dimensions are defined in +`em`, the dot scales with the font size of its container — set `font-size` on a +wrapper to fine-tune it. + + + +## Accessibility + +- 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. + +## Styling + +At render time the resolved color and size are written to the `--fill` and +`--size` custom properties on the inner dot, so there's nothing to override +there directly. Because the dot is sized in `em`, set `font-size` on the host to +scale it: + +```css +craft-indicator { + /* the dot is sized in em — change the font size to scale it */ + font-size: 1.25rem; +} +``` diff --git a/packages/craftcms-cp/src/components/indicator/indicator.stories.ts b/packages/craftcms-cp/src/components/indicator/indicator.stories.ts index d6f105950ec..98492c554f6 100644 --- a/packages/craftcms-cp/src/components/indicator/indicator.stories.ts +++ b/packages/craftcms-cp/src/components/indicator/indicator.stories.ts @@ -1,32 +1,101 @@ import type {Meta, StoryObj} from '@storybook/web-components-vite'; -import {html} from 'lit'; +import {html, nothing} from 'lit'; import './indicator.js'; -import {Variant} from '@src/types'; +import type CraftIndicator from './indicator.js'; +import {Variant} from '@src/constants/variants'; + +const appearances = ['filled-outlined', 'filled', 'outlined']; + +/** + * Renders the indicator in each appearance, with a label, so every story shares + * the same markup. + */ +const renderIndicators = ( + label: string, + attrs: {fill?: string; size?: CraftIndicator['size']} = {} +) => html` + + ${label} + ${appearances.map( + (appearance) => html` + + + + ` + )} + +`; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories const meta = { title: 'Components/Indicator', component: 'craft-indicator', args: {}, - render: function () { - return html` -
- ${Object.values(Variant).map( - (variant) => - html`` + render: () => html` + + + + ${appearances.map((appearance) => html``)} + + + ${Object.values(Variant).map((variant) => + renderIndicators(variant, {fill: variant}) )} - - - `; - }, -} satisfies Meta; + +
${appearance}
+ `, +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args export const Default: Story = { args: {}, }; + +const arbitraryValues = [ + '#2c61de', + 'rgba(30, 40, 40, 0.2)', + 'linear-gradient(red, blue)', +]; + +export const ArbitraryColor: Story = { + render: () => html` + + + + ${appearances.map((appearance) => html``)} + + + ${arbitraryValues.map((value) => + renderIndicators(value, {fill: value}) + )} + +
${appearance}
+ `, +}; + +const sizes: Array = ['md', 'lg']; + +export const ArbitrarySize: Story = { + render: () => html` + + + + ${appearances.map((appearance) => html``)} + + + ${sizes.map((size) => + renderIndicators(size === 'md' ? 'md (default)' : size, {size}) + )} + +
${appearance}
+ `, +}; diff --git a/packages/craftcms-cp/src/components/indicator/indicator.ts b/packages/craftcms-cp/src/components/indicator/indicator.ts index 944806e22cf..7aac33d676b 100644 --- a/packages/craftcms-cp/src/components/indicator/indicator.ts +++ b/packages/craftcms-cp/src/components/indicator/indicator.ts @@ -1,49 +1,98 @@ import {css, html, LitElement} from 'lit'; import {property} from 'lit/decorators.js'; -import {Variant, type VariantKey} from '@src/types'; import {classMap} from 'lit/directives/class-map.js'; import variantsStyles from '@src/styles/variants.styles'; +import {colors} from '@src/constants/colors'; +import {variants} from '@src/constants/variants'; +/** + * @summary Indicators are used to visually represent the status of an object. + * Most of the time, you won't want to use the component directly but instead + * should use one of the status components. + * + * @since 1.0 + */ export default class CraftIndicator extends LitElement { static override styles = [ variantsStyles, css` .indicator { + --_fill: var(--fill, var(--c-color-fill-loud)); + --_size: var(--size, 0.5em); display: inline-flex; aspect-ratio: 1; - width: var(--c-indicator-size, 0.5em); + width: var(--_size); border-radius: var(--c-radius-full); - color: var(--c-color-on-loud); - background-color: var(--c-color-fill-loud); - border: 1px solid var(--c-color-border-loud); + background: var(--_fill); + border: 1px solid var(--_fill); } - .indicator--empty { - background-color: var(--c-color-neutral-fill-quiet); - border: 1px solid var(--c-color-neutral-border-normal); + /* Appearances */ + :host([appearance~='filled-outlined']) .indicator { + background: var(--_fill); + border: 1px solid rgba(0, 0, 0, 0.5); + } + + :host([appearance~='filled']) .indicator { + background: var(--_fill); + border-color: transparent; + } + + :host([appearance~='outlined']) .indicator { + background: transparent; + border: 2px solid var(--_fill); } `, ]; + @property() + size: 'md' | 'lg' = 'md'; + + /** @phpType {Color|string} */ @property({reflect: true}) - variant: VariantKey | 'empty' = Variant.Default; + fill: string = 'var(--c-color-fill-loud)'; @property() label: string | null = null; + @property({reflect: true}) + appearance: 'filled' | 'outlined' | 'filled-outlined' = 'filled-outlined'; + + getFill() { + // If the fill is known swatch + if ((colors as string[]).includes(this.fill)) { + return `var(--c-color-${this.fill}-fill-loud)`; + } + + // If it's a known variant + if ((variants as string[]).includes(this.fill)) { + return `var(--c-color-${this.fill}-fill-loud)`; + } + + return this.fill; + } + + getSize() { + switch (this.size) { + case 'md': + return '0.6em'; + case 'lg': + return '1em'; + default: + return this.size; + } + } + protected override render(): unknown { return html` - - `; + >`; } } diff --git a/packages/craftcms-cp/src/constants/appearances.ts b/packages/craftcms-cp/src/constants/appearances.ts new file mode 100644 index 00000000000..f60853a3d4a --- /dev/null +++ b/packages/craftcms-cp/src/constants/appearances.ts @@ -0,0 +1,12 @@ +export const Appearance = { + Accent: 'accent', + OutlineFill: 'outline-fill', + Fill: 'fill', + Outline: 'outline', + Plain: 'plain', +} as const; + +export const appearances = Object.values(Appearance); + +export type AppearanceKey = (typeof Appearance)[keyof typeof Appearance]; +export type AppearanceValue = typeof appearances; diff --git a/packages/craftcms-cp/src/constants/colors.ts b/packages/craftcms-cp/src/constants/colors.ts index 5069cf43d93..eab6854cccc 100644 --- a/packages/craftcms-cp/src/constants/colors.ts +++ b/packages/craftcms-cp/src/constants/colors.ts @@ -20,3 +20,8 @@ export const Color = { Gray: 'gray', Black: 'black', } as const; + +export const colors = Object.values(Color); + +export type ColorKey = (typeof Color)[keyof typeof Color]; +export type ColorValue = typeof colors; diff --git a/packages/craftcms-cp/src/constants/variants.ts b/packages/craftcms-cp/src/constants/variants.ts new file mode 100644 index 00000000000..5451a0e691b --- /dev/null +++ b/packages/craftcms-cp/src/constants/variants.ts @@ -0,0 +1,12 @@ +export const Variant = { + Default: 'default', + Success: 'success', + Warning: 'warning', + Danger: 'danger', + Info: 'info', +} as const; + +export const variants = Object.values(Variant); + +export type VariantKey = (typeof Variant)[keyof typeof Variant]; +export type VariantValue = typeof variants; diff --git a/packages/craftcms-cp/src/index.ts b/packages/craftcms-cp/src/index.ts index fc68049f859..cf4b2bab405 100644 --- a/packages/craftcms-cp/src/index.ts +++ b/packages/craftcms-cp/src/index.ts @@ -93,4 +93,9 @@ export {default as hostStyles} from './styles/host.styles.js'; export {default as variantStyles} from './styles/variants.styles.js'; export {default as visuallyHiddenStyles} from './styles/visually-hidden.styles.js'; +// Constants +export * from './constants/variants'; +export * from './constants/appearances'; +export * from './constants/colors'; + configureIcons(); diff --git a/packages/craftcms-cp/src/stories/tokens/variants.stories.ts b/packages/craftcms-cp/src/stories/tokens/variants.stories.ts index 77381a11ee1..942ebfe2c30 100644 --- a/packages/craftcms-cp/src/stories/tokens/variants.stories.ts +++ b/packages/craftcms-cp/src/stories/tokens/variants.stories.ts @@ -6,10 +6,8 @@ import '../../components/callout/callout.js'; import '../../components/button/button.js'; import '../../components/indicator/indicator.js'; -import {Appearance, Variant} from '@src/types'; - -const variants = Object.values(Variant); -const appearances = Object.values(Appearance); +import {appearances} from '@src/constants/appearances'; +import {variants} from '@src/constants/variants'; const buttonVariants = ['primary', 'default', 'danger'] as const; const buttonAppearances = ['accent', 'filled', 'dashed', 'plain'] as const; diff --git a/packages/craftcms-cp/src/styles/shared/colorable.css b/packages/craftcms-cp/src/styles/shared/colorable.css index 9618fc87b22..b373cb8faa1 100644 --- a/packages/craftcms-cp/src/styles/shared/colorable.css +++ b/packages/craftcms-cp/src/styles/shared/colorable.css @@ -298,24 +298,7 @@ --c-color-danger-on-loud: var(--color-red-50); } -.c-colorable, -[data-color] { - --c-color-fill-quiet: var(--c-color-neutral-fill-quiet); - --c-color-fill-normal: var(--c-color-neutral-fill-normal); - --c-color-fill-loud: var(--c-color-neutral-fill-loud); - --c-color-border-quiet: var(--c-color-neutral-border-quiet); - --c-color-border-normal: var(--c-color-neutral-border-normal); - --c-color-border-loud: var(--c-color-neutral-border-loud); - --c-color-on-quiet: var(--c-color-neutral-on-quiet); - --c-color-on-normal: var(--c-color-neutral-on-normal); - --c-color-on-loud: var(--c-color-neutral-on-loud); - - background-color: var(--c-color-fill-quiet); - border-color: var(--c-color-border-quiet); - color: var(--c-color-on-quiet); -} - -.c-colorable--red, +.cp-color-red, [data-color='red'] { --c-color-fill-quiet: var(--c-color-red-fill-quiet); --c-color-border-quiet: var(--c-color-red-border-quiet); @@ -555,87 +538,87 @@ --c-color-border-loud: var(--c-color-black-border-loud); --c-color-on-loud: var(--c-color-black-on-loud); } -.cp-color-neutral, -[data-color='neutral'] { - --c-color-fill-quiet: var(--c-color-neutral-fill-quiet); - --c-color-border-quiet: var(--c-color-neutral-border-quiet); - --c-color-on-quiet: var(--c-color-neutral-on-quiet); - --c-color-fill-normal: var(--c-color-neutral-fill-normal); - --c-color-border-normal: var(--c-color-neutral-border-normal); - --c-color-on-normal: var(--c-color-neutral-on-normal); - --c-color-fill-loud: var(--c-color-neutral-fill-loud); - --c-color-border-loud: var(--c-color-neutral-border-loud); - --c-color-on-loud: var(--c-color-neutral-on-loud); +.cp-color-slate, +[data-color='slate'] { + --c-color-fill-quiet: var(--c-color-slate-fill-quiet); + --c-color-border-quiet: var(--c-color-slate-border-quiet); + --c-color-on-quiet: var(--c-color-slate-on-quiet); + --c-color-fill-normal: var(--c-color-slate-fill-normal); + --c-color-border-normal: var(--c-color-slate-border-normal); + --c-color-on-normal: var(--c-color-slate-on-normal); + --c-color-fill-loud: var(--c-color-slate-fill-loud); + --c-color-border-loud: var(--c-color-slate-border-loud); + --c-color-on-loud: var(--c-color-slate-on-loud); } -.cp-color-brand, -[data-color='brand'] { - --c-color-fill-quiet: var(--c-color-brand-fill-quiet); - --c-color-border-quiet: var(--c-color-brand-border-quiet); - --c-color-on-quiet: var(--c-color-brand-on-quiet); - --c-color-fill-normal: var(--c-color-brand-fill-normal); - --c-color-border-normal: var(--c-color-brand-border-normal); - --c-color-on-normal: var(--c-color-brand-on-normal); - --c-color-fill-loud: var(--c-color-brand-fill-loud); - --c-color-border-loud: var(--c-color-brand-border-loud); - --c-color-on-loud: var(--c-color-brand-on-loud); +.cp-color-red, +[data-color='red'] { + --c-color-fill-quiet: var(--c-color-red-fill-quiet); + --c-color-border-quiet: var(--c-color-red-border-quiet); + --c-color-on-quiet: var(--c-color-red-on-quiet); + --c-color-fill-normal: var(--c-color-red-fill-normal); + --c-color-border-normal: var(--c-color-red-border-normal); + --c-color-on-normal: var(--c-color-red-on-normal); + --c-color-fill-loud: var(--c-color-red-fill-loud); + --c-color-border-loud: var(--c-color-red-border-loud); + --c-color-on-loud: var(--c-color-red-on-loud); } -.cp-color-accent, -[data-color='accent'] { - --c-color-fill-quiet: var(--c-color-accent-fill-quiet); - --c-color-border-quiet: var(--c-color-accent-border-quiet); - --c-color-on-quiet: var(--c-color-accent-on-quiet); - --c-color-fill-normal: var(--c-color-accent-fill-normal); - --c-color-border-normal: var(--c-color-accent-border-normal); - --c-color-on-normal: var(--c-color-accent-on-normal); - --c-color-fill-loud: var(--c-color-accent-fill-loud); - --c-color-border-loud: var(--c-color-accent-border-loud); - --c-color-on-loud: var(--c-color-accent-on-loud); +.cp-color-blue, +[data-color='blue'] { + --c-color-fill-quiet: var(--c-color-blue-fill-quiet); + --c-color-border-quiet: var(--c-color-blue-border-quiet); + --c-color-on-quiet: var(--c-color-blue-on-quiet); + --c-color-fill-normal: var(--c-color-blue-fill-normal); + --c-color-border-normal: var(--c-color-blue-border-normal); + --c-color-on-normal: var(--c-color-blue-on-normal); + --c-color-fill-loud: var(--c-color-blue-fill-loud); + --c-color-border-loud: var(--c-color-blue-border-loud); + --c-color-on-loud: var(--c-color-blue-on-loud); } -.cp-color-info, -[data-color='info'] { - --c-color-fill-quiet: var(--c-color-info-fill-quiet); - --c-color-border-quiet: var(--c-color-info-border-quiet); - --c-color-on-quiet: var(--c-color-info-on-quiet); - --c-color-fill-normal: var(--c-color-info-fill-normal); - --c-color-border-normal: var(--c-color-info-border-normal); - --c-color-on-normal: var(--c-color-info-on-normal); - --c-color-fill-loud: var(--c-color-info-fill-loud); - --c-color-border-loud: var(--c-color-info-border-loud); - --c-color-on-loud: var(--c-color-info-on-loud); +.cp-color-blue, +[data-color='blue'] { + --c-color-fill-quiet: var(--c-color-blue-fill-quiet); + --c-color-border-quiet: var(--c-color-blue-border-quiet); + --c-color-on-quiet: var(--c-color-blue-on-quiet); + --c-color-fill-normal: var(--c-color-blue-fill-normal); + --c-color-border-normal: var(--c-color-blue-border-normal); + --c-color-on-normal: var(--c-color-blue-on-normal); + --c-color-fill-loud: var(--c-color-blue-fill-loud); + --c-color-border-loud: var(--c-color-blue-border-loud); + --c-color-on-loud: var(--c-color-blue-on-loud); } -.cp-color-success, -[data-color='success'] { - --c-color-fill-quiet: var(--c-color-success-fill-quiet); - --c-color-border-quiet: var(--c-color-success-border-quiet); - --c-color-on-quiet: var(--c-color-success-on-quiet); - --c-color-fill-normal: var(--c-color-success-fill-normal); - --c-color-border-normal: var(--c-color-success-border-normal); - --c-color-on-normal: var(--c-color-success-on-normal); - --c-color-fill-loud: var(--c-color-success-fill-loud); - --c-color-border-loud: var(--c-color-success-border-loud); - --c-color-on-loud: var(--c-color-success-on-loud); +.cp-color-emerald, +[data-color='emerald'] { + --c-color-fill-quiet: var(--c-color-emerald-fill-quiet); + --c-color-border-quiet: var(--c-color-emerald-border-quiet); + --c-color-on-quiet: var(--c-color-emerald-on-quiet); + --c-color-fill-normal: var(--c-color-emerald-fill-normal); + --c-color-border-normal: var(--c-color-emerald-border-normal); + --c-color-on-normal: var(--c-color-emerald-on-normal); + --c-color-fill-loud: var(--c-color-emerald-fill-loud); + --c-color-border-loud: var(--c-color-emerald-border-loud); + --c-color-on-loud: var(--c-color-emerald-on-loud); } -.cp-color-warning, -[data-color='warning'] { - --c-color-fill-quiet: var(--c-color-warning-fill-quiet); - --c-color-border-quiet: var(--c-color-warning-border-quiet); - --c-color-on-quiet: var(--c-color-warning-on-quiet); - --c-color-fill-normal: var(--c-color-warning-fill-normal); - --c-color-border-normal: var(--c-color-warning-border-normal); - --c-color-on-normal: var(--c-color-warning-on-normal); - --c-color-fill-loud: var(--c-color-warning-fill-loud); - --c-color-border-loud: var(--c-color-warning-border-loud); - --c-color-on-loud: var(--c-color-warning-on-loud); +.cp-color-orange, +[data-color='orange'] { + --c-color-fill-quiet: var(--c-color-orange-fill-quiet); + --c-color-border-quiet: var(--c-color-orange-border-quiet); + --c-color-on-quiet: var(--c-color-orange-on-quiet); + --c-color-fill-normal: var(--c-color-orange-fill-normal); + --c-color-border-normal: var(--c-color-orange-border-normal); + --c-color-on-normal: var(--c-color-orange-on-normal); + --c-color-fill-loud: var(--c-color-orange-fill-loud); + --c-color-border-loud: var(--c-color-orange-border-loud); + --c-color-on-loud: var(--c-color-orange-on-loud); } -.cp-color-danger, -[data-color='danger'] { - --c-color-fill-quiet: var(--c-color-danger-fill-quiet); - --c-color-border-quiet: var(--c-color-danger-border-quiet); - --c-color-on-quiet: var(--c-color-danger-on-quiet); - --c-color-fill-normal: var(--c-color-danger-fill-normal); - --c-color-border-normal: var(--c-color-danger-border-normal); - --c-color-on-normal: var(--c-color-danger-on-normal); - --c-color-fill-loud: var(--c-color-danger-fill-loud); - --c-color-border-loud: var(--c-color-danger-border-loud); - --c-color-on-loud: var(--c-color-danger-on-loud); +.cp-color-red, +[data-color='red'] { + --c-color-fill-quiet: var(--c-color-red-fill-quiet); + --c-color-border-quiet: var(--c-color-red-border-quiet); + --c-color-on-quiet: var(--c-color-red-on-quiet); + --c-color-fill-normal: var(--c-color-red-fill-normal); + --c-color-border-normal: var(--c-color-red-border-normal); + --c-color-on-normal: var(--c-color-red-on-normal); + --c-color-fill-loud: var(--c-color-red-fill-loud); + --c-color-border-loud: var(--c-color-red-border-loud); + --c-color-on-loud: var(--c-color-red-on-loud); } diff --git a/packages/craftcms-cp/src/types/index.ts b/packages/craftcms-cp/src/types/index.ts index 4f0b49b5331..f8866fcbcc4 100644 --- a/packages/craftcms-cp/src/types/index.ts +++ b/packages/craftcms-cp/src/types/index.ts @@ -1,23 +1,3 @@ -export const Variant = { - Default: 'default', - Success: 'success', - Warning: 'warning', - Danger: 'danger', - Info: 'info', -} as const; - -export type VariantKey = (typeof Variant)[keyof typeof Variant]; - -export const Appearance = { - Accent: 'accent', - OutlineFill: 'outline-fill', - Fill: 'fill', - Outline: 'outline', - Plain: 'plain', -} as const; - -export type AppearanceKey = (typeof Appearance)[keyof typeof Appearance]; - export interface DateObject { date: string; timezone_type: string; diff --git a/src/Cp/Html/StatusHtml.php b/src/Cp/Html/StatusHtml.php index 849d4a1e3bc..6f50cfc8af8 100644 --- a/src/Cp/Html/StatusHtml.php +++ b/src/Cp/Html/StatusHtml.php @@ -7,6 +7,7 @@ use CraftCms\Cms\Component\Contracts\Statusable; use CraftCms\Cms\Cp\Icons; use CraftCms\Cms\Shared\Enums\Color; +use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Html; use Illuminate\Container\Attributes\Singleton; @@ -17,11 +18,7 @@ { public function statusIndicatorHtml(string $status, array $attributes = []): ?string { - $attributes += [ - 'color' => null, - 'label' => ucfirst($status), - 'class' => $status, - ]; + $label = Arr::get($attributes, 'label', ucfirst($status)); if ($status === 'draft') { return Html::tag('span', '', [ @@ -31,30 +28,23 @@ public function statusIndicatorHtml(string $status, array $attributes = []): ?st 'aria' => [ 'label' => sprintf('%s %s', t('Status:'), - $attributes['label'] ?? t('Draft'), + $label ?? t('Draft'), ), ], ]); } - if ($attributes['color'] instanceof Color) { - $attributes['color'] = $attributes['color']->value; - } + $color = Arr::get($attributes, 'color') ?? Color::tryFromStatus($status) ?? Color::Gray; + $attributes = []; - $options = [ - 'class' => array_filter([ - 'status', - $attributes['class'], - $attributes['color'], - ]), - ]; - - if ($attributes['label'] !== null) { - $options['role'] = 'img'; - $options['aria']['label'] = sprintf('%s %s', t('Status:'), $attributes['label']); + if ($color instanceof Color) { + $color = $color->value; } - return Html::tag('span', '', $options); + $attributes['label'] = $label ? sprintf('%s %s', t('Status:'), $label) : null; + $attributes['fill'] = $color; + + return Html::tag('craft-indicator', '', $attributes); } public function componentStatusIndicatorHtml(Statusable $component): ?string diff --git a/tests/Feature/Cp/ElementHtmlTest.php b/tests/Feature/Cp/ElementHtmlTest.php index dc39633fbfc..9da550ca87e 100644 --- a/tests/Feature/Cp/ElementHtmlTest.php +++ b/tests/Feature/Cp/ElementHtmlTest.php @@ -30,8 +30,8 @@ expect($fieldHtml)->toContain('removable') ->and($fieldHtml)->toContain('name="myFieldName[]"') - ->and($indexHtml)->toContain('and($indexHtml)->toContain('and($this->elementHtml->elementChipHtml($user, ['showStatus' => false]))->not->toContain('and($indexHtml)->toContain('thumb') ->and($this->elementHtml->elementChipHtml($user, ['showThumb' => false]))->not->toContain('thumb'); diff --git a/tests/Unit/Cp/StatusHtmlTest.php b/tests/Unit/Cp/StatusHtmlTest.php index 82b22cbcb1a..6659d29294a 100644 --- a/tests/Unit/Cp/StatusHtmlTest.php +++ b/tests/Unit/Cp/StatusHtmlTest.php @@ -19,8 +19,8 @@ 'label' => 'Enabled', ]); - expect($html)->toContain('class="status enabled"') - ->and($html)->toContain('aria-label="Status: Enabled"'); + expect($html)->toContain('fill="teal"') + ->and($html)->toContain('label="Status: Enabled"'); }); }); @@ -41,7 +41,8 @@ public function getStatus(): string $html = app(StatusHtml::class)->componentStatusIndicatorHtml($component); - expect($html)->toContain('status pending yellow'); + expect($html)->toContain('fill="yellow"') + ->and($html)->toContain('label="Status: Pending"'); }); it('renders component status label and edited status label', function () { diff --git a/yii2-adapter/tests/unit/helpers/CpHelperTest.php b/yii2-adapter/tests/unit/helpers/CpHelperTest.php index b13a744ac07..5d8767aa874 100644 --- a/yii2-adapter/tests/unit/helpers/CpHelperTest.php +++ b/yii2-adapter/tests/unit/helpers/CpHelperTest.php @@ -1,6 +1,8 @@ + * * @since 3.6.0 */ class CpHelperTest extends TestCase { - /** - * @var UnitTester - */ protected UnitTester $tester; public function _fixtures(): array @@ -37,10 +37,7 @@ public function _fixtures(): array ]; } - /** - * - */ - public function testElementChipHtml(): void + public function test_element_chip_html(): void { /** @var User $user */ $user = User::findOne(1); @@ -57,8 +54,8 @@ public function testElementChipHtml(): void self::assertStringContainsString('name="myFieldName[]"', $fieldHtml); // status - self::assertStringContainsString(' false])); + self::assertStringContainsString(' false])); // thumb self::assertStringContainsString('thumb', $indexHtml); @@ -84,10 +81,7 @@ public function testElementChipHtml(): void $user->trashed = false; } - /** - * - */ - public function testElementHtml(): void + public function test_element_html(): void { /** @var User $user */ $user = User::findOne(1); @@ -101,8 +95,8 @@ public function testElementHtml(): void self::assertStringContainsString('name="myFieldName[]"', $fieldHtml); // status - self::assertStringContainsString('trashed = false; } - /** - * - */ - public function testFieldHtml(): void + public function test_field_html(): void { self::assertStringContainsString('
', Cp::fieldHtml('')); self::assertStringContainsString('', Cp::fieldHtml('', ['label' => 'Label', 'id' => 'id'])); - self::assertStringNotContainsString('', ['label' => '__blank__', ])); + self::assertStringNotContainsString('', ['label' => '__blank__'])); // invalid site ID $this->tester->expectThrowable(InvalidArgumentException::class, function() { Cp::fieldHtml('', ['siteId' => -1]); @@ -170,18 +161,12 @@ public function testFieldHtml(): void /** * @dataProvider fieldMethodsDataProvider - * @param string $needle - * @param string $method - * @param array $config */ - public function testFieldMethods(string $needle, string $method, array $config = []): void + public function test_field_methods(string $needle, string $method, array $config = []): void { self::assertStringContainsString($needle, call_user_func([Cp::class, $method], $config)); } - /** - * @return array - */ public static function fieldMethodsDataProvider(): array { return [ @@ -189,16 +174,16 @@ public static function fieldMethodsDataProvider(): array ['color-input', 'colorFieldHtml'], [ 'editable', 'editableTableFieldHtml', [ - 'name' => 'test', - ], + 'name' => 'test', + ], ], ['lightswitch', 'lightswitchFieldHtml'], ['
', 'textFieldHtml', [ - 'unit' => 'Test unit', - ], + 'unit' => 'Test unit', + ], ], ['