From 7d832bff6812e0980e140ca7b5d775486e02cef9 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Thu, 23 Jul 2026 13:46:02 -0500 Subject: [PATCH 1/8] Collapse craft-button to a single variant axis Replace the two-axis (variant + appearance) button API with one `variant` property: primary, danger, solid, fill (default), outline, dashed, plain, link, none. `primary`/`danger` are the colored, stable variants; the rest use the neutral palette. Add a boolean `inherit` property (orthogonal to variant) so neutral variants adopt the ambient colorable palette in a [data-color]/callout context, while primary/danger keep their own colors. Rewrites button.styles.ts around the single axis, and updates the stories and docs (combined the variant/appearance stories, added an Inherit demo). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/components/button/Docs.mdx | 40 +-- .../src/components/button/button.stories.ts | 134 ++++---- .../src/components/button/button.styles.ts | 306 ++++++++++-------- .../src/components/button/button.ts | 35 +- 4 files changed, 278 insertions(+), 237 deletions(-) diff --git a/packages/craftcms-ui/src/components/button/Docs.mdx b/packages/craftcms-ui/src/components/button/Docs.mdx index aba2d6ced23..72697740939 100644 --- a/packages/craftcms-ui/src/components/button/Docs.mdx +++ b/packages/craftcms-ui/src/components/button/Docs.mdx @@ -9,38 +9,42 @@ A button represents a clickable element that performs an action when activated. -## Appearance - -`appearance` controls how visually prominent the button is. Use it to signal importance relative to other buttons on the page. - -- `solid` — Solid colored background. The highest-emphasis style. Use for the single most important action in a given context. -- `outline` — Transparent background with a colored border. Medium emphasis. Use for secondary actions alongside a solid button. -- `plain` — No background or border. Lowest emphasis. Use for tertiary or destructive actions that shouldn't draw attention on their own. - ## Variant -`variant` controls the button's color, which communicates the _intent_ of the action — not how prominent it is. Appearance and variant are independent: any appearance can be combined with any variant. +`variant` is the single axis that sets a button's color and appearance together — communicating both the intent of the action and how prominent it is. -- `accent` — Blue. Use for the primary action on a page, such as Save or Confirm. -- `neutral` — Gray. The default. Use for everyday actions that don't carry a strong positive or negative meaning. -- `danger` — Red. Use for actions that delete, remove, or permanently change data. +- `primary` — Solid accent color. The highest-emphasis style; use for the single most important action in a context, such as Save or Confirm. +- `danger` — Solid red. Use for actions that delete, remove, or permanently change data. +- `solid` — Solid neutral (gray). A prominent action without a strong positive or negative meaning. +- `fill` — Soft neutral fill. The default; use for everyday actions. +- `outline` — Transparent background with a neutral border. Medium emphasis; use for secondary actions alongside a `primary` or `solid` button. +- `dashed` — Like `outline`, with a dashed border. +- `plain` — No background or border. Lowest emphasis; use for tertiary actions that shouldn't draw attention on their own. +- `link` — Renders as an inline text hyperlink. +- `none` — Completely unstyled; provides behavior only. - +`primary` and `danger` are colored and stay stable in any context; every other variant uses the neutral palette. -### Inherit +## Inherit -`variant="inherit"` is a special case for buttons placed inside [Colorable](?path=/docs/concepts-colorable--docs) containers. Instead of applying its own color, the button picks up the color from its parent container. +Add the boolean `inherit` property to a button placed inside a [Colorable](?path=/docs/concepts-colorable--docs) container (such as a `craft-callout` or any `[data-color]` element). Its neutral variants then adopt the container's color instead of the neutral palette, while still honoring the chosen appearance (`solid`, `fill`, `outline`, and so on). -Avoid using the standard color variants (`accent`, `neutral`, `danger`) inside Colorable components — the two color systems can conflict and produce unexpected results. +`primary` and `danger` ignore `inherit` and keep their own colors, so they remain stable inside colorable containers. + + ## Sizes -## Icon buttons +## Icons - + ## Loading state + +## Links + + diff --git a/packages/craftcms-ui/src/components/button/button.stories.ts b/packages/craftcms-ui/src/components/button/button.stories.ts index e102011313c..143db30510b 100644 --- a/packages/craftcms-ui/src/components/button/button.stories.ts +++ b/packages/craftcms-ui/src/components/button/button.stories.ts @@ -5,10 +5,9 @@ import {html} from 'lit'; import './button.js'; import '../icon/icon.js'; import '../chip/chip.js'; -import {ButtonVariant, ButtonAppearance} from '@src/components/button/button'; +import {ButtonVariant} from '@src/components/button/button'; const buttonVariants = Object.values(ButtonVariant); -const appearance = Object.values(ButtonAppearance); // More on how to set up stories at: https://storybook.js.org/docs/writing-stories const meta = { @@ -19,76 +18,79 @@ const meta = { }, args: { label: 'Button', - appearance: 'solid', loading: false, - variant: 'neutral', + variant: ButtonVariant.Fill, }, argTypes: { - appearance: { + variant: { control: {type: 'select'}, - options: appearance, + options: buttonVariants, }, loading: { control: {type: 'boolean'}, }, }, - render: (args) => html` -
+} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** + * Every variant. `primary` and `danger` are solid and colored; the rest are the + * neutral palette in their named appearance. + */ +export const Default: Story = { + render: () => html` +
${buttonVariants.map( (variant) => html` -
- ${variant ?? 'None'} solid - ${variant} outline${variant} + ` + )} +
+ `, +}; + +/** + * With the `inherit` property, the neutral variants adopt the ambient colorable + * palette (any `[data-color]` / colorable ancestor, e.g. a callout). `primary` + * and `danger` keep their own colors regardless. + */ +export const Inherit: Story = { + render: () => html` +
+ ${['accent', 'violet', 'success'].map( + (color) => html` +
+ ${[ + ButtonVariant.Solid, + ButtonVariant.Fill, + ButtonVariant.Outline, + ButtonVariant.Dashed, + ButtonVariant.Plain, + ButtonVariant.Link, + ].map( + (variant) => html` + ${variant} + ` + )} + primary - ${variant} plaindanger
` )} - - -
- Chip Buttons - Outline - Plain -
-
`, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args -export const Default: Story = { - args: {}, -}; - -export const Variants: Story = { - args: { - variant: 'accent', - }, - argTypes: { - variant: { - control: {type: 'select'}, - options: buttonVariants, - }, - }, - render: () => - html`${buttonVariants.map( - (variant) => html` - ${variant} - ` - )}`, }; export const Sizes: Story = { @@ -102,10 +104,18 @@ export const Sizes: Story = { `, }; -export const Icon: Story = { - args: {}, - render: (args) => html` -
+export const Icons: Story = { + render: () => html` +
+ Prefix icon + Suffix icon + + + Slotted prefix + + @@ -130,10 +140,10 @@ export const Links: Story = { render: () => html`
- ${appearance.map( - (a) => html` - ${a} link html` + ${variant} link ` )} diff --git a/packages/craftcms-ui/src/components/button/button.styles.ts b/packages/craftcms-ui/src/components/button/button.styles.ts index 98bcc723bb2..513cce176ca 100644 --- a/packages/craftcms-ui/src/components/button/button.styles.ts +++ b/packages/craftcms-ui/src/components/button/button.styles.ts @@ -40,6 +40,61 @@ export default css` ); } + /* + Color palette per variant. + The single 'variant' axis picks both a palette and an appearance. Here we map + the generic --c-color-* props to a palette; 'primary' uses accent, 'danger' + uses danger, everything else uses neutral, and 'inherit' sets nothing so it + adopts the parent's theme. + */ + /* + Neutral variants use the neutral palette by default. With [inherit] we skip + this override so the ambient --c-color-* (set by a colorable ancestor) flows + through instead. primary/danger set their palettes unconditionally below, so + they are unaffected by [inherit]. + */ + :host([variant='solid']:not([inherit])), + :host([variant='fill']:not([inherit])), + :host([variant='outline']:not([inherit])), + :host([variant='dashed']:not([inherit])), + :host([variant='plain']:not([inherit])), + :host([variant='link']:not([inherit])), + :host([variant='none']:not([inherit])) { + --c-color-fill-loud: var(--c-color-neutral-fill-loud); + --c-color-fill-normal: var(--c-color-neutral-fill-normal); + --c-color-fill-quiet: var(--c-color-neutral-fill-quiet); + --c-color-border-loud: var(--c-color-neutral-border-loud); + --c-color-border-normal: var(--c-color-neutral-border-normal); + --c-color-border-quiet: var(--c-color-neutral-border-quiet); + --c-color-on-loud: var(--c-color-neutral-on-loud); + --c-color-on-normal: var(--c-color-neutral-on-normal); + --c-color-on-quiet: var(--c-color-neutral-on-quiet); + } + + :host([variant='primary']) { + --c-color-fill-loud: var(--c-color-accent-fill-loud); + --c-color-fill-normal: var(--c-color-accent-fill-normal); + --c-color-fill-quiet: var(--c-color-accent-fill-quiet); + --c-color-border-loud: var(--c-color-accent-border-loud); + --c-color-border-normal: var(--c-color-accent-border-normal); + --c-color-border-quiet: var(--c-color-accent-border-quiet); + --c-color-on-loud: var(--c-color-accent-on-loud); + --c-color-on-normal: var(--c-color-accent-on-normal); + --c-color-on-quiet: var(--c-color-accent-on-quiet); + } + + :host([variant='danger']) { + --c-color-fill-loud: var(--c-color-danger-fill-loud); + --c-color-fill-normal: var(--c-color-danger-fill-normal); + --c-color-fill-quiet: var(--c-color-danger-fill-quiet); + --c-color-border-loud: var(--c-color-danger-border-loud); + --c-color-border-normal: var(--c-color-danger-border-normal); + --c-color-border-quiet: var(--c-color-danger-border-quiet); + --c-color-on-loud: var(--c-color-danger-on-loud); + --c-color-on-normal: var(--c-color-danger-on-normal); + --c-color-on-quiet: var(--c-color-danger-on-quiet); + } + :host(:focus:not([disabled])), :host(:focus-visible) { outline: var(--c-focus-outline-width) solid var(--_focus-outline-color); @@ -50,16 +105,6 @@ export default css` --_focus-outline-color: var(--c-color-focus-outline); } - @media (hover: hover) { - :host(:hover) { - background-color: hsl( - from var(--c-color-fill-loud, var(--c-button-default-fill)) h s - calc(l - 5) - ); - color: var(--c-color-on-loud); - } - } - :host(:not(:disabled):not(.loading):active), :host(.is-active:not(:disabled):not(.loading)) { color: var(--_active-color); @@ -67,10 +112,11 @@ export default css` box-shadow: inset 0 1px 3px var(--c-color-mix-active); } + /* Selected state (e.g. inside a radio button-group): show the loud fill. */ :host(:not(:disabled):not(.loading)[active]), :host(.is-active:not(:disabled):not(.loading)) { - color: var(--c-color-on-loud); background-color: var(--c-color-fill-loud); + color: var(--c-color-on-loud); border-color: var(--c-color-border-loud); } @@ -132,185 +178,177 @@ export default css` } /* - Appearances + Variants (appearance × palette) */ - /* Inline */ - :host([appearance~='inline']) { - display: inline; - appearance: none; - background-color: transparent; - border-color: currentColor; - color: inherit; - font: inherit; - padding: 0; - min-height: auto; - min-width: auto; - - &::before { - /* remove the sizer added by lion */ - display: none; - } - - .button-content { - padding: 0; - } + /* Solid — primary, danger, and solid are filled with the loud color. */ + :host([variant='primary']), + :host([variant='danger']), + :host([variant='solid']) { + background-color: var( + --c-color-fill-loud, + var(--c-color-neutral-fill-loud) + ); + border-color: transparent; + color: var(--c-color-on-loud, var(--c-color-neutral-on-loud)); } - :host([appearance='inline']:not(:disabled):not(.loading):hover) { - background-color: color-mix( - in oklab, - var(--c-color-fill-quiet, var(--c-button-default-fill)), - var(--c-color-mix-hover) + :host([variant='primary']:hover), + :host([variant='danger']:hover), + :host([variant='solid']:hover) { + background-color: hsl( + from var(--c-color-fill-loud, var(--c-color-neutral-fill-loud)) h s + calc(l - 5) ); - color: var(--c-color-on-quiet); + color: var(--c-color-on-loud, var(--c-color-neutral-on-loud)); } - :host([appearance='inline']:not(:disabled):not(.loading):active) { - color: var(--c-color-on-quiet, var(--c-color-neutral-on-quiet)); - background-color: color-mix( - in oklab, - var(--c-color-fill-quiet, var(--c-color-neutral-fill-quiet)), - var(--c-color-mix-active) + :host([variant='primary']:active), + :host([variant='danger']:active), + :host([variant='solid']:active) { + --_active-background-color: hsl( + from var(--c-color-fill-loud, var(--c-color-neutral-fill-loud)) h s + calc(l - 10) ); + --_active-color: var(--c-color-on-loud, var(--c-color-neutral-on-loud)); } - :host([appearance='inline'][active]) { + /* Fill (default) — neutral normal fill. */ + :host([variant='fill']) { + border-color: transparent; background-color: var( --c-color-fill-normal, var(--c-color-neutral-fill-normal) ); - border-color: var( - --c-color-border-normal, - var(--c-color-neutral-border-normal) - ); color: var(--c-color-on-normal, var(--c-color-neutral-on-normal)); - box-shadow: none; } - /* Plain & Outline (Shared) */ - :host([appearance~='plain']), - :host([appearance~='outline']) { - background-color: transparent; - color: var(--c-color-on-quiet); - } - - :host([appearance~='plain']:hover), - :host([appearance~='outline']:hover) { + :host([variant='fill']:hover) { background-color: hsl( - from var(--c-color-fill-quiet, var(--c-color-neutral-fill-quiet)) h s + from var(--c-color-fill-normal, var(--c-color-neutral-fill-normal)) h s calc(l - 5) ); + color: var(--c-color-on-normal, var(--c-color-neutral-on-normal)); } - :host([appearance~='plain']:active), - :host([appearance~='outline']:active) { + :host([variant='fill']:active) { --_active-background-color: hsl( - from var(--c-color-fill-quiet, var(--c-color-neutral-fill-quiet)) h s - calc(l - 8) + from var(--c-color-fill-normal, var(--c-color-neutral-fill-normal)) h s + calc(l - 10) ); - --_active-color: var(--c-color-on-quiet, var(--c-color-neutral-on-quiet)); - } - - :host([appearance~='plain'][active]), - :host([appearance~='outline'][active]) { - background-color: var(--c-color-fill-loud); - color: var(--c-color-on-loud); - border-color: var(--c-color-border-loud); + --_active-color: var(--c-color-on-normal, var(--c-color-neutral-on-normal)); } - /* Plain */ - :host([appearance~='plain']) { - border-color: transparent; - color: inherit; - - &:before { - display: none; - } + /* Outline, Dashed & Plain — transparent fill. */ + :host([variant='outline']), + :host([variant='dashed']), + :host([variant='plain']) { + background-color: transparent; + color: var(--c-color-on-quiet); } - :host([appearance='plain']:not(:disabled):not(.loading):hover) { + :host([variant='outline']:not(:disabled):not(.loading):hover), + :host([variant='dashed']:not(:disabled):not(.loading):hover), + :host([variant='plain']:not(:disabled):not(.loading):hover) { background-color: color-mix( in oklab, - var(--c-color-fill-quiet, var(--c-button-default-fill)), + var(--c-color-fill-quiet, var(--c-color-neutral-fill-quiet)), var(--c-color-mix-hover) ); color: var(--c-color-on-quiet); } - :host([appearance='plain']:not(:disabled):not(.loading):active) { - color: var(--c-color-on-quiet, var(--c-color-neutral-on-quiet)); - background-color: color-mix( + :host([variant='outline']:not(:disabled):not(.loading):active), + :host([variant='dashed']:not(:disabled):not(.loading):active), + :host([variant='plain']:not(:disabled):not(.loading):active) { + --_active-background-color: color-mix( in oklab, var(--c-color-fill-quiet, var(--c-color-neutral-fill-quiet)), var(--c-color-mix-active) ); + --_active-color: var(--c-color-on-quiet, var(--c-color-neutral-on-quiet)); } - :host([appearance='plain'][active]) { - background-color: var(--c-color-fill-normal); - border-color: var(--c-color-border-normal); - color: var(--c-color-on-normal); - box-shadow: none; + /* Outline & Dashed — visible border. */ + :host([variant='outline']), + :host([variant='dashed']) { + border-color: var(--c-color-border-loud); } - /* Outline */ - :host([appearance='outline']) { - border-color: var(--c-color-border-loud); + :host([variant='dashed']) { + border-style: dashed; } - /* Solid */ - :host([appearance~='solid']) { - background-color: var( - --c-color-fill-loud, - var(--c-color-neutral-fill-loud) - ); + /* Plain — no border. */ + :host([variant='plain']) { border-color: transparent; - color: var(--c-color-on-loud, var(--c-color-neutral-on-loud)); + + &::before { + display: none; + } } - :host([appearance='solid']:hover) { - background-color: hsl( - from var(--c-color-fill-loud, var(--c-color-neutral-fill-loud)) h s - calc(l - 5) - ); - color: var(--c-color-on-loud, var(--c-color-neutral-on-loud)); + /* Link — renders as a text hyperlink: no fill or border, underlined. */ + :host([variant='link']) { + display: inline; + appearance: none; + background-color: transparent; + border-color: transparent; + color: var(--c-color-fill-loud, var(--c-color-neutral-fill-loud)); + font: inherit; + padding: 0; + min-height: auto; + min-width: auto; + text-decoration: underline; + + &::before { + /* remove the sizer added by lion */ + display: none; + } + + .button-content { + padding: 0; + } } - :host([appearance='solid']:active) { - --_active-background-color: hsl( + :host([variant='link']:not(:disabled):not(.loading):hover) { + background-color: transparent; + color: hsl( from var(--c-color-fill-loud, var(--c-color-neutral-fill-loud)) h s calc(l - 10) ); - --_active-color: var(--c-color-on-loud, var(--c-color-neutral-on-loud)); + text-decoration: none; } - /* Fill */ - :host([appearance~='fill']) { - border-color: transparent; - background-color: var( - --c-color-fill-normal, - var(--c-color-neutral-fill-normal) - ); - border-color: transparent; - color: var(--c-color-on-normal, var(--c-color-neutral-on-normal)); + :host([variant='link']:not(:disabled):not(.loading):active) { + background-color: transparent; } - :host([appearance='fill']:hover) { - background-color: hsl( - from var(--c-color-fill-normal, var(--c-color-neutral-fill-normal)) h s - calc(l - 5) - ); - color: var(--c-color-on-normal, var(--c-color-neutral-on-normal)); + /* None — completely unstyled; provides behavior only. */ + :host([variant='none']) { + appearance: none; + background-color: transparent; + border-color: transparent; + border-width: 0; + color: inherit; + font: inherit; + padding: 0; + min-height: auto; + min-width: auto; + + &::before { + display: none; + } + + .button-content { + padding: 0; + } } - :host([appearance='fill'][active]), - :host([appearance='fill']:active) { - --_active-background-color: hsl( - from var(--c-color-fill-normal, var(--c-color-neutral-fill-normal)) h s - calc(l - 10) - ); - --_active-color: var(--c-color-on-normal, var(--c-color-neutral-on-normal)); + :host([variant='none']:hover), + :host([variant='none']:active) { + background-color: transparent; + color: inherit; } .button-content { @@ -340,12 +378,6 @@ export default css` border-radius: 0; } - craft-button-reset, - craft-button-submit { - /* Temporarily make it very obvious when these are used */ - outline: 10px solid var(--c-button-danger-border); - } - .a11y-error { position: relative; outline: 2px solid var(--c-color-danger-border-normal) !important; diff --git a/packages/craftcms-ui/src/components/button/button.ts b/packages/craftcms-ui/src/components/button/button.ts index 856d022a2eb..8631147053b 100644 --- a/packages/craftcms-ui/src/components/button/button.ts +++ b/packages/craftcms-ui/src/components/button/button.ts @@ -6,24 +6,20 @@ import '../spinner/spinner.js'; import '../icon/icon.js'; import {computeAccessibleName} from 'dom-accessibility-api'; import {classMap} from 'lit/directives/class-map.js'; -import variantsStyles from '@src/styles/variants.styles'; -export const ButtonAppearance = { +export const ButtonVariant = { + Primary: 'primary', + Danger: 'danger', Solid: 'solid', Fill: 'fill', Outline: 'outline', + Dashed: 'dashed', Plain: 'plain', -} as const; - -export const ButtonVariant = { - Accent: 'accent', - Neutral: 'neutral', - Danger: 'danger', + Link: 'link', + None: 'none', } as const; export type ButtonVariant = (typeof ButtonVariant)[keyof typeof ButtonVariant]; -export type ButtonAppearance = - (typeof ButtonAppearance)[keyof typeof ButtonAppearance]; /** * @summary Interactive element that triggers an action or event. @@ -44,7 +40,7 @@ export type ButtonAppearance = */ export default class CraftButton extends LionButtonSubmit { static override get styles() { - return [...super.styles, variantsStyles, styles]; + return [...super.styles, styles]; } override connectedCallback() { @@ -108,18 +104,17 @@ export default class CraftButton extends LionButtonSubmit { /** The computed accessible name */ @property({attribute: 'accessible-name'}) accessibleName: string; - /** Visual appearance of the button */ - @property({reflect: true}) appearance: ButtonAppearance = 'solid'; + /** + * The button's visual style. Defaults to "fill" (neutral fill). + */ + @property({reflect: true}) variant: ButtonVariant = ButtonVariant.Fill; /** - * Theme variant of the button. Defaults to "neutral" - * - * Accent: The primary action on a page - * Neutral: Used in most cases - * Danger: Indicates a dangerous action, when data will be removed or deleted - * Inherit: Useful for colorable elements, button will reflect the parent theme + * Adopt the ambient colorable palette (from a `[data-color]` / colorable + * ancestor) instead of the neutral palette. Only + * affects the neutral variants; `primary` and `danger` stay stable. */ - @property({reflect: true}) variant: ButtonVariant = 'neutral'; + @property({reflect: true, type: Boolean}) inherit: boolean = false; /** Size of the button. Defaults to "medium" */ @property({reflect: true}) size: 'zero' | 'small' | 'medium' | 'large' = From 1361fce9c28db0f35749c642688729b6cbf393c3 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Thu, 23 Jul 2026 13:46:19 -0500 Subject: [PATCH 2/8] Replace previous button variant/appearance references Adapt the package to the collapsed button API: - index.ts stops re-exporting the removed ButtonAppearance. - variants.styles.ts swaps the [variant='default'] block for [variant='accent'] and [variant='neutral'] (used by callout/indicator/action-item). - The tokens "Button Matrix" story and FilterHud now use the single variant axis (e.g. ButtonVariant.Fill / ButtonVariant.Primary). Note: the broader app consumer migration (variant/appearance usages across resources/js) is still pending and tracked separately. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/craftcms-ui/src/index.ts | 1 - .../src/stories/tokens/variants.stories.ts | 43 +++++-------------- .../craftcms-ui/src/styles/variants.styles.ts | 14 +++++- .../modules/elements/components/FilterHud.vue | 6 +-- 4 files changed, 25 insertions(+), 39 deletions(-) diff --git a/packages/craftcms-ui/src/index.ts b/packages/craftcms-ui/src/index.ts index dd43dfd4354..e175abf655a 100644 --- a/packages/craftcms-ui/src/index.ts +++ b/packages/craftcms-ui/src/index.ts @@ -14,7 +14,6 @@ export {default as CraftBreadcrumbs} from './components/breadcrumbs/breadcrumbs. export {default as CraftButtonGroup} from './components/button-group/button-group.js'; export { default as CraftButton, - ButtonAppearance, ButtonVariant, } from './components/button/button.js'; export {default as CraftCallout} from './components/callout/callout.js'; diff --git a/packages/craftcms-ui/src/stories/tokens/variants.stories.ts b/packages/craftcms-ui/src/stories/tokens/variants.stories.ts index 17e67e0b0a0..909b6efd29c 100644 --- a/packages/craftcms-ui/src/stories/tokens/variants.stories.ts +++ b/packages/craftcms-ui/src/stories/tokens/variants.stories.ts @@ -8,10 +8,9 @@ import '../../components/indicator/indicator.js'; import {appearances} from '@src/constants/appearances'; import {variants} from '@src/constants/variants'; -import {ButtonVariant, ButtonAppearance} from '@src/components/button/button'; +import {ButtonVariant} from '@src/components/button/button'; const buttonVariants = Object.values(ButtonVariant); -const buttonAppearances = Object.values(ButtonAppearance); const meta: Meta = { title: 'Tokens/Variants & Appearances', @@ -64,42 +63,20 @@ export const CalloutMatrix: Story = { }; /** - * Buttons use their own variant subset (accent, neutral, danger) - * and appearance subset (solid, outline, plain). + * Buttons use a single `variant` axis (primary, danger, solid, fill, outline, + * dashed, plain, link, none — plus inherit). */ export const ButtonMatrix: Story = { name: 'Button Matrix', render: () => html`
- - - - - ${buttonAppearances.map((a) => html``)} - - - - ${buttonVariants.map( - (variant) => html` - - - ${buttonAppearances.map( - (appearance) => html` - - ` - )} - - ` - )} - -
variant${a}
${variant} - - ${variant} - -
+
+ ${buttonVariants.map( + (variant) => html` + ${variant} + ` + )} +
`, }; diff --git a/packages/craftcms-ui/src/styles/variants.styles.ts b/packages/craftcms-ui/src/styles/variants.styles.ts index 906db69267b..fec9c94b451 100644 --- a/packages/craftcms-ui/src/styles/variants.styles.ts +++ b/packages/craftcms-ui/src/styles/variants.styles.ts @@ -1,7 +1,19 @@ import {css} from 'lit'; export default css` - :host([variant='default']) { + :host([variant='accent']) { + --c-color-fill-loud: var(--c-color-accent-fill-loud); + --c-color-fill-normal: var(--c-color-accent-fill-normal); + --c-color-fill-quiet: var(--c-color-accent-fill-quiet); + --c-color-border-loud: var(--c-color-accent-border-loud); + --c-color-border-normal: var(--c-color-accent-border-normal); + --c-color-border-quiet: var(--c-color-accent-border-quiet); + --c-color-on-loud: var(--c-color-accent-on-loud); + --c-color-on-normal: var(--c-color-accent-on-normal); + --c-color-on-quiet: var(--c-color-accent-on-quiet); + } + + :host([variant='neutral']) { --c-color-fill-loud: var(--c-color-neutral-fill-loud); --c-color-fill-normal: var(--c-color-neutral-fill-normal); --c-color-fill-quiet: var(--c-color-neutral-fill-quiet); diff --git a/resources/js/modules/elements/components/FilterHud.vue b/resources/js/modules/elements/components/FilterHud.vue index c664a33fdb1..412e3390298 100644 --- a/resources/js/modules/elements/components/FilterHud.vue +++ b/resources/js/modules/elements/components/FilterHud.vue @@ -3,7 +3,6 @@ t, appendBodyHtml, appendHeadHtml, - ButtonAppearance, ButtonVariant, } from '@craftcms/ui'; import {useHttp, usePage} from '@inertiajs/vue3'; @@ -108,14 +107,13 @@
{{ t('Cancel') }} {{ t('Apply') }}
From 0de4b1142c00d5b5195e57b5c3a2cedb111afe88 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Thu, 23 Jul 2026 13:46:31 -0500 Subject: [PATCH 3/8] Consolidate color definitions into a single source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit constants/colors.ts and scripts/generate-colors.js each hard-coded the color palette and semantic map, and had drifted (info: blue vs sky, warning: orange vs yellow; accent: red vs blue). Introduce constants/colors.data.ts as the single source of truth. colors.ts builds its Color map + ColorValue/ColorKey from it; generate-colors.js loads it via esbuild (it can't import TS directly). Regenerate colorable.css — the token output is unchanged except accent is corrected from a stale red to blue. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/craftcms-ui/scripts/build.js | 2 +- .../craftcms-ui/scripts/generate-colors.js | 77 ++++++++---------- .../craftcms-ui/src/constants/colors.data.ts | 57 ++++++++++++++ packages/craftcms-ui/src/constants/colors.ts | 78 +++++++++---------- .../src/styles/shared/colorable.css | 36 ++++----- 5 files changed, 146 insertions(+), 104 deletions(-) create mode 100644 packages/craftcms-ui/src/constants/colors.data.ts diff --git a/packages/craftcms-ui/scripts/build.js b/packages/craftcms-ui/scripts/build.js index 680cb1f3244..b21c760b1dd 100755 --- a/packages/craftcms-ui/scripts/build.js +++ b/packages/craftcms-ui/scripts/build.js @@ -106,7 +106,7 @@ async function generateVueWrappers() { async function generateColors() { spinner.start('Generating Colors'); - createColors(); + await createColors(); spinner.succeed(); return Promise.resolve(); } diff --git a/packages/craftcms-ui/scripts/generate-colors.js b/packages/craftcms-ui/scripts/generate-colors.js index 006f9f1dd8b..aa8df54f4cf 100644 --- a/packages/craftcms-ui/scripts/generate-colors.js +++ b/packages/craftcms-ui/scripts/generate-colors.js @@ -1,47 +1,27 @@ -import {writeFileSync} from 'fs'; +import {writeFileSync, readFileSync} from 'fs'; import {dirname, resolve} from 'path'; import {fileURLToPath} from 'url'; +import {transformSync} from 'esbuild'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(__dirname, '..'); const OUT_FILE = resolve(ROOT, 'src/styles/shared/colorable.css'); +const DATA_FILE = resolve(ROOT, 'src/constants/colors.data.ts'); /* -This is the same as the colors in constants/colors but we can't import -typescript into this JS file so we had to duplicate for now. +The canonical color data lives in src/constants/colors.data.ts (shared with +constants/colors.ts). This script can't import TypeScript directly, so we +transpile it with esbuild and import the result. */ -const availableColors = [ - 'red', - 'orange', - 'amber', - 'yellow', - 'lime', - 'green', - 'emerald', - 'teal', - 'cyan', - 'sky', - 'blue', - 'indigo', - 'violet', - 'purple', - 'fuchsia', - 'pink', - 'rose', - 'white', - 'gray', - 'black', - 'slate', -]; - -const semanticColors = { - neutral: 'slate', - accent: 'red', - info: 'blue', - success: 'emerald', - warning: 'yellow', - danger: 'red', -}; +async function loadColorData() { + const {code} = transformSync(readFileSync(DATA_FILE, 'utf8'), { + loader: 'ts', + format: 'esm', + }); + return import( + `data:text/javascript;base64,${Buffer.from(code).toString('base64')}` + ); +} const colorIncrements = { fill: { @@ -103,8 +83,8 @@ function colorScale(color) { } } -function buildColorableTokens() { - return availableColors +function buildColorableTokens(paletteColors) { + return paletteColors .map((color) => { const s = colorScale(color); return [ @@ -123,7 +103,7 @@ function buildColorableTokens() { .join('\n\n'); } -function buildSemanticTokens() { +function buildSemanticTokens(semanticColors) { let declarations = []; for (const [meaning, color] of Object.entries(semanticColors)) { const s = colorScale(color); @@ -161,22 +141,29 @@ function buildStyleBlock(color) { }`; } -function generateStyles(colors) { +function generateStyles(paletteColors, semanticColors) { return `/* Auto-generated by scripts/generate-colors.js — do not edit manually */ :root { -${buildColorableTokens()} -${buildSemanticTokens()} +${buildColorableTokens(paletteColors)} +${buildSemanticTokens(semanticColors)} } -${[...availableColors, ...Object.keys(semanticColors)].map((c) => buildStyleBlock(c)).join('\n')} +${[...paletteColors, ...Object.keys(semanticColors)] + .map((c) => buildStyleBlock(c)) + .join('\n')} `; } -export default function main() { - const css = generateStyles(availableColors); +export default async function main() { + const {paletteColors, semanticColors} = await loadColorData(); + const css = generateStyles(paletteColors, semanticColors); writeFileSync(OUT_FILE, css); console.log(`Generated ${OUT_FILE}`); } -main(); +// Run when invoked directly (node scripts/generate-colors.js), but not when +// imported by build.js (which awaits the default export instead). +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/packages/craftcms-ui/src/constants/colors.data.ts b/packages/craftcms-ui/src/constants/colors.data.ts new file mode 100644 index 00000000000..63b88ab4e5d --- /dev/null +++ b/packages/craftcms-ui/src/constants/colors.data.ts @@ -0,0 +1,57 @@ +/** + * Canonical color data — the single source of truth for colors in the design + * system. + * + * Consumed by: + * - `constants/colors.ts` (runtime `Color` map + types), and + * - `scripts/generate-colors.js` (generates `styles/shared/colorable.css`), + * which transpiles this file with esbuild since it can't import TS directly. + * + * Keep it dependency-free so it stays trivially transpilable. + */ + +/** The raw color palette (each has a full `--color--*` scale). */ +export const paletteColors = [ + 'red', + 'orange', + 'amber', + 'yellow', + 'lime', + 'green', + 'emerald', + 'teal', + 'cyan', + 'sky', + 'blue', + 'indigo', + 'violet', + 'purple', + 'fuchsia', + 'pink', + 'rose', + 'white', + 'gray', + 'black', + 'slate', +] as const; + +/** Semantic meaning → palette color. */ +export const semanticColors = { + neutral: 'slate', + accent: 'blue', + info: 'sky', + success: 'emerald', + warning: 'yellow', + danger: 'red', +} as const; + +/** Status name → palette color. */ +export const statusColors = { + pending: 'orange', + off: 'red', + suspended: 'red', + expired: 'red', + disabled: 'gray', + inactive: 'gray', + on: 'emerald', +} as const; diff --git a/packages/craftcms-ui/src/constants/colors.ts b/packages/craftcms-ui/src/constants/colors.ts index 30ecdc51187..813a01c22e1 100644 --- a/packages/craftcms-ui/src/constants/colors.ts +++ b/packages/craftcms-ui/src/constants/colors.ts @@ -1,44 +1,42 @@ -export const Color = { - Red: 'red', - Orange: 'orange', - Amber: 'amber', - Yellow: 'yellow', - Lime: 'lime', - Green: 'green', - Emerald: 'emerald', - Teal: 'teal', - Cyan: 'cyan', - Sky: 'sky', - Blue: 'blue', - Indigo: 'indigo', - Violet: 'violet', - Purple: 'purple', - Fuchsia: 'fuchsia', - Pink: 'pink', - Rose: 'rose', - White: 'white', - Gray: 'gray', - Black: 'black', +import {paletteColors, semanticColors, statusColors} from './colors.data'; + +type PaletteColor = (typeof paletteColors)[number]; - // Semantic - Neutral: 'slate', - Accent: 'red', - Success: 'emerald', - Warning: 'orange', - Danger: 'red', - Info: 'blue', +/** + * Named color map (label → palette color), combining the raw palette, semantic + * meanings, and status names from the shared {@link ./colors.data} source. + */ +type NamedColors = { + readonly [K in PaletteColor as Capitalize]: K; +} & { + readonly [K in keyof typeof semanticColors as Capitalize]: (typeof semanticColors)[K]; +} & { + readonly [K in keyof typeof statusColors as Capitalize]: (typeof statusColors)[K]; +}; - // Status - Pending: 'orange', - Off: 'red', - Suspended: 'red', - Expired: 'red', - Disabled: 'gray', - Inactive: 'gray', - On: 'emerald', -} as const; +const capitalize = (value: string): string => + value.charAt(0).toUpperCase() + value.slice(1); + +export const Color = { + ...Object.fromEntries( + paletteColors.map((color) => [capitalize(color), color]) + ), + ...Object.fromEntries( + Object.entries(semanticColors).map(([name, color]) => [ + capitalize(name), + color, + ]) + ), + ...Object.fromEntries( + Object.entries(statusColors).map(([name, color]) => [ + capitalize(name), + color, + ]) + ), +} as NamedColors; -export const colors = Object.values(Color); +/** All valid color values (the palette). */ +export const colors: PaletteColor[] = [...paletteColors]; -export type ColorKey = keyof typeof Color; -export type ColorValue = (typeof Color)[keyof typeof Color]; +export type ColorKey = keyof NamedColors; +export type ColorValue = PaletteColor; diff --git a/packages/craftcms-ui/src/styles/shared/colorable.css b/packages/craftcms-ui/src/styles/shared/colorable.css index f3a9073c8d0..c97c886df11 100644 --- a/packages/craftcms-ui/src/styles/shared/colorable.css +++ b/packages/craftcms-ui/src/styles/shared/colorable.css @@ -243,26 +243,26 @@ --c-color-neutral-on-loud: var(--color-slate-50); /* Semantics colors - accent */ - --c-color-accent-fill-quiet: var(--color-red-50); - --c-color-accent-fill-normal: var(--color-red-100); - --c-color-accent-fill-loud: var(--color-red-600); - --c-color-accent-border-quiet: var(--color-red-400); - --c-color-accent-border-normal: var(--color-red-600); - --c-color-accent-border-loud: var(--color-red-800); - --c-color-accent-on-quiet: var(--color-red-800); - --c-color-accent-on-normal: var(--color-red-950); - --c-color-accent-on-loud: var(--color-red-50); + --c-color-accent-fill-quiet: var(--color-blue-50); + --c-color-accent-fill-normal: var(--color-blue-100); + --c-color-accent-fill-loud: var(--color-blue-600); + --c-color-accent-border-quiet: var(--color-blue-400); + --c-color-accent-border-normal: var(--color-blue-600); + --c-color-accent-border-loud: var(--color-blue-800); + --c-color-accent-on-quiet: var(--color-blue-800); + --c-color-accent-on-normal: var(--color-blue-950); + --c-color-accent-on-loud: var(--color-blue-50); /* Semantics colors - info */ - --c-color-info-fill-quiet: var(--color-blue-50); - --c-color-info-fill-normal: var(--color-blue-100); - --c-color-info-fill-loud: var(--color-blue-600); - --c-color-info-border-quiet: var(--color-blue-400); - --c-color-info-border-normal: var(--color-blue-600); - --c-color-info-border-loud: var(--color-blue-800); - --c-color-info-on-quiet: var(--color-blue-800); - --c-color-info-on-normal: var(--color-blue-950); - --c-color-info-on-loud: var(--color-blue-50); + --c-color-info-fill-quiet: var(--color-sky-50); + --c-color-info-fill-normal: var(--color-sky-100); + --c-color-info-fill-loud: var(--color-sky-600); + --c-color-info-border-quiet: var(--color-sky-400); + --c-color-info-border-normal: var(--color-sky-600); + --c-color-info-border-loud: var(--color-sky-800); + --c-color-info-on-quiet: var(--color-sky-800); + --c-color-info-on-normal: var(--color-sky-950); + --c-color-info-on-loud: var(--color-sky-50); /* Semantics colors - success */ --c-color-success-fill-quiet: var(--color-emerald-50); From 842c21021b56a2d183320ec2320aba47fba686b2 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Thu, 23 Jul 2026 16:26:18 -0500 Subject: [PATCH 4/8] Update button instance to new variants --- .../src/components/button/button.styles.ts | 92 ++++++++++--------- .../src/components/button/button.ts | 1 + .../js/common/components/FormActions.vue | 10 +- .../js/common/components/SlideoutButton.vue | 3 +- resources/js/common/components/Tooltip.vue | 4 +- resources/js/common/components/UserMenu.vue | 2 +- resources/js/common/layouts/AppLayout.vue | 5 +- .../admin-table/components/DeleteButton.vue | 3 +- .../auth/components/login/login-form.ts | 2 +- .../modules/editable-table/editable-table.ts | 3 +- .../components/EntryTypeSelect.vue | 3 +- .../field-layout-designer.ts | 4 +- .../js/modules/link-field/craft-link-field.ts | 2 +- .../permissions/components/PermissionList.vue | 4 +- .../components/PluginLicenseInput.vue | 4 +- .../components/PluginLicenseIssues.vue | 3 +- .../components/DeleteSectionButton.vue | 3 +- .../sites/components/DeleteSiteButton.vue | 3 +- .../user/components/UserGroupSelect.vue | 13 ++- resources/js/pages/settings/Email.vue | 4 +- resources/js/pages/settings/routes/Index.vue | 8 +- .../templates/_includes/disclosuremenu.twig | 4 +- .../_includes/forms/editableTable.twig | 3 +- src/Condition/BaseCondition.php | 5 +- src/Cp/Components/Button.php | 44 ++++++++- src/Cp/Enums/ButtonVariant.php | 27 ++++++ src/Cp/FormFields.php | 6 +- src/Cp/Html/ElementHtml.php | 4 +- src/Cp/Html/PreviewHtml.php | 2 +- src/Field/ButtonGroup.php | 2 +- src/Field/Icon.php | 3 +- src/Field/Link.php | 4 +- src/Support/Facades/HtmlSanitizers.php | 2 +- src/Support/Facades/Template.php | 2 +- tests/Unit/Cp/Components/ButtonTest.php | 17 ++-- .../Twig/Templates/ButtonTemplateTest.php | 4 +- 36 files changed, 184 insertions(+), 121 deletions(-) create mode 100644 src/Cp/Enums/ButtonVariant.php diff --git a/packages/craftcms-ui/src/components/button/button.styles.ts b/packages/craftcms-ui/src/components/button/button.styles.ts index 513cce176ca..99eccc64031 100644 --- a/packages/craftcms-ui/src/components/button/button.styles.ts +++ b/packages/craftcms-ui/src/components/button/button.styles.ts @@ -1,4 +1,5 @@ import {css} from 'lit'; + export default css` :host { /* Necessary to use variables here to override the default active style specificity */ @@ -53,13 +54,13 @@ export default css` through instead. primary/danger set their palettes unconditionally below, so they are unaffected by [inherit]. */ - :host([variant='solid']:not([inherit])), - :host([variant='fill']:not([inherit])), - :host([variant='outline']:not([inherit])), - :host([variant='dashed']:not([inherit])), - :host([variant='plain']:not([inherit])), - :host([variant='link']:not([inherit])), - :host([variant='none']:not([inherit])) { + :host([variant~='solid']:not([inherit])), + :host([variant~='fill']:not([inherit])), + :host([variant~='outline']:not([inherit])), + :host([variant~='dashed']:not([inherit])), + :host([variant~='plain']:not([inherit])), + :host([variant~='link']:not([inherit])), + :host([variant~='none']:not([inherit])) { --c-color-fill-loud: var(--c-color-neutral-fill-loud); --c-color-fill-normal: var(--c-color-neutral-fill-normal); --c-color-fill-quiet: var(--c-color-neutral-fill-quiet); @@ -71,7 +72,7 @@ export default css` --c-color-on-quiet: var(--c-color-neutral-on-quiet); } - :host([variant='primary']) { + :host([variant~='primary']) { --c-color-fill-loud: var(--c-color-accent-fill-loud); --c-color-fill-normal: var(--c-color-accent-fill-normal); --c-color-fill-quiet: var(--c-color-accent-fill-quiet); @@ -83,7 +84,8 @@ export default css` --c-color-on-quiet: var(--c-color-accent-on-quiet); } - :host([variant='danger']) { + :host([variant~='danger-plain']), + :host([variant~='danger']) { --c-color-fill-loud: var(--c-color-danger-fill-loud); --c-color-fill-normal: var(--c-color-danger-fill-normal); --c-color-fill-quiet: var(--c-color-danger-fill-quiet); @@ -182,9 +184,9 @@ export default css` */ /* Solid — primary, danger, and solid are filled with the loud color. */ - :host([variant='primary']), - :host([variant='danger']), - :host([variant='solid']) { + :host([variant~='primary']), + :host([variant~='danger']), + :host([variant~='solid']) { background-color: var( --c-color-fill-loud, var(--c-color-neutral-fill-loud) @@ -193,9 +195,9 @@ export default css` color: var(--c-color-on-loud, var(--c-color-neutral-on-loud)); } - :host([variant='primary']:hover), - :host([variant='danger']:hover), - :host([variant='solid']:hover) { + :host([variant~='primary']:hover), + :host([variant~='danger']:hover), + :host([variant~='solid']:hover) { background-color: hsl( from var(--c-color-fill-loud, var(--c-color-neutral-fill-loud)) h s calc(l - 5) @@ -203,9 +205,9 @@ export default css` color: var(--c-color-on-loud, var(--c-color-neutral-on-loud)); } - :host([variant='primary']:active), - :host([variant='danger']:active), - :host([variant='solid']:active) { + :host([variant~='primary']:active), + :host([variant~='danger']:active), + :host([variant~='solid']:active) { --_active-background-color: hsl( from var(--c-color-fill-loud, var(--c-color-neutral-fill-loud)) h s calc(l - 10) @@ -214,7 +216,7 @@ export default css` } /* Fill (default) — neutral normal fill. */ - :host([variant='fill']) { + :host([variant~='fill']) { border-color: transparent; background-color: var( --c-color-fill-normal, @@ -223,7 +225,7 @@ export default css` color: var(--c-color-on-normal, var(--c-color-neutral-on-normal)); } - :host([variant='fill']:hover) { + :host([variant~='fill']:hover) { background-color: hsl( from var(--c-color-fill-normal, var(--c-color-neutral-fill-normal)) h s calc(l - 5) @@ -231,7 +233,7 @@ export default css` color: var(--c-color-on-normal, var(--c-color-neutral-on-normal)); } - :host([variant='fill']:active) { + :host([variant~='fill']:active) { --_active-background-color: hsl( from var(--c-color-fill-normal, var(--c-color-neutral-fill-normal)) h s calc(l - 10) @@ -240,16 +242,18 @@ export default css` } /* Outline, Dashed & Plain — transparent fill. */ - :host([variant='outline']), - :host([variant='dashed']), - :host([variant='plain']) { + :host([variant~='outline']), + :host([variant~='dashed']), + :host([variant~='danger-plain']), + :host([variant~='plain']) { background-color: transparent; color: var(--c-color-on-quiet); } - :host([variant='outline']:not(:disabled):not(.loading):hover), - :host([variant='dashed']:not(:disabled):not(.loading):hover), - :host([variant='plain']:not(:disabled):not(.loading):hover) { + :host([variant~='outline']:not(:disabled):not(.loading):hover), + :host([variant~='dashed']:not(:disabled):not(.loading):hover), + :host([variant~='danger-plain']:not(:disabled):not(.loading):hover), + :host([variant~='plain']:not(:disabled):not(.loading):hover) { background-color: color-mix( in oklab, var(--c-color-fill-quiet, var(--c-color-neutral-fill-quiet)), @@ -258,9 +262,10 @@ export default css` color: var(--c-color-on-quiet); } - :host([variant='outline']:not(:disabled):not(.loading):active), - :host([variant='dashed']:not(:disabled):not(.loading):active), - :host([variant='plain']:not(:disabled):not(.loading):active) { + :host([variant~='outline']:not(:disabled):not(.loading):active), + :host([variant~='dashed']:not(:disabled):not(.loading):active), + :host([variant~='danger-plain']:not(:disabled):not(.loading):active), + :host([variant~='plain']:not(:disabled):not(.loading):active) { --_active-background-color: color-mix( in oklab, var(--c-color-fill-quiet, var(--c-color-neutral-fill-quiet)), @@ -270,17 +275,18 @@ export default css` } /* Outline & Dashed — visible border. */ - :host([variant='outline']), - :host([variant='dashed']) { + :host([variant~='outline']), + :host([variant~='dashed']) { border-color: var(--c-color-border-loud); } - :host([variant='dashed']) { - border-style: dashed; + :host([variant~='dashed']) { + --c-button-border-style: dashed; } /* Plain — no border. */ - :host([variant='plain']) { + :host([variant~='danger-plain']), + :host([variant~='plain']) { border-color: transparent; &::before { @@ -289,7 +295,7 @@ export default css` } /* Link — renders as a text hyperlink: no fill or border, underlined. */ - :host([variant='link']) { + :host([variant~='link']) { display: inline; appearance: none; background-color: transparent; @@ -311,7 +317,7 @@ export default css` } } - :host([variant='link']:not(:disabled):not(.loading):hover) { + :host([variant~='link']:not(:disabled):not(.loading):hover) { background-color: transparent; color: hsl( from var(--c-color-fill-loud, var(--c-color-neutral-fill-loud)) h s @@ -320,12 +326,12 @@ export default css` text-decoration: none; } - :host([variant='link']:not(:disabled):not(.loading):active) { + :host([variant~='link']:not(:disabled):not(.loading):active) { background-color: transparent; } /* None — completely unstyled; provides behavior only. */ - :host([variant='none']) { + :host([variant~='none']) { appearance: none; background-color: transparent; border-color: transparent; @@ -345,8 +351,8 @@ export default css` } } - :host([variant='none']:hover), - :host([variant='none']:active) { + :host([variant~='none']:hover), + :host([variant~='none']:active) { background-color: transparent; color: inherit; } @@ -374,10 +380,6 @@ export default css` justify-content: end; } - craft-button-group craft-button { - border-radius: 0; - } - .a11y-error { position: relative; outline: 2px solid var(--c-color-danger-border-normal) !important; diff --git a/packages/craftcms-ui/src/components/button/button.ts b/packages/craftcms-ui/src/components/button/button.ts index 8631147053b..101b1c5ac1f 100644 --- a/packages/craftcms-ui/src/components/button/button.ts +++ b/packages/craftcms-ui/src/components/button/button.ts @@ -10,6 +10,7 @@ import {classMap} from 'lit/directives/class-map.js'; export const ButtonVariant = { Primary: 'primary', Danger: 'danger', + DangerPlain: 'danger-plain', Solid: 'solid', Fill: 'fill', Outline: 'outline', diff --git a/resources/js/common/components/FormActions.vue b/resources/js/common/components/FormActions.vue index 59c4fbe5027..5941ff502c4 100644 --- a/resources/js/common/components/FormActions.vue +++ b/resources/js/common/components/FormActions.vue @@ -1,5 +1,5 @@