Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/craftcms-ui/scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ async function generateVueWrappers() {

async function generateColors() {
spinner.start('Generating Colors');
createColors();
await createColors();
spinner.succeed();
return Promise.resolve();
}
Expand Down
77 changes: 32 additions & 45 deletions packages/craftcms-ui/scripts/generate-colors.js
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down Expand Up @@ -103,8 +83,8 @@ function colorScale(color) {
}
}

function buildColorableTokens() {
return availableColors
function buildColorableTokens(paletteColors) {
return paletteColors
.map((color) => {
const s = colorScale(color);
return [
Expand All @@ -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);
Expand Down Expand Up @@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const DataDrivenDisabled: Story = {
export const SlotBasedDisabled: Story = {
render: () =>
html`<craft-action-menu disabled>
<craft-button type="button" slot="invoker" appearance="secondary">
<craft-button type="button" slot="invoker" variant="fill">
Custom invoker
</craft-button>
<div slot="content">
Expand All @@ -140,7 +140,7 @@ export const SlotBasedDisabled: Story = {
export const DataDrivenWithCustomInvoker: Story = {
render: () =>
html`<craft-action-menu .actions="${dataDrivenActions}">
<craft-button type="button" slot="invoker" appearance="secondary">
<craft-button type="button" slot="invoker" variant="fill">
Custom invoker
</craft-button>
</craft-action-menu>`,
Expand All @@ -156,7 +156,7 @@ export const DataDrivenWithCustomInvoker: Story = {
export const Searchable: Story = {
render: () =>
html`<craft-action-menu searchable>
<craft-button type="button" slot="invoker" appearance="secondary">
<craft-button type="button" slot="invoker" variant="fill">
Add a field
</craft-button>

Expand Down Expand Up @@ -218,7 +218,7 @@ export const SearchableDataDriven: Story = {
export const SearchableWithHiddenItems: Story = {
render: () =>
html`<craft-action-menu searchable>
<craft-button type="button" slot="invoker" appearance="secondary">
<craft-button type="button" slot="invoker" variant="fill">
Add a field
</craft-button>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,29 +70,23 @@ export const RadioMode: Story = {
};

/**
* Radio mode works across all button appearances. The selected button retains
* its `active` styling regardless of the chosen appearance.
* Radio mode works across all button variants. The selected button retains
* its `active` styling regardless of the chosen variant.
*/
export const RadioModeAppearances: Story = {
name: 'Radio mode — appearances',
export const RadioModeVariants: Story = {
name: 'Radio mode — variants',
render: () => html`
<div style="display:flex;flex-direction:column;gap:1rem">
${(['fill', 'plain'] as const).map(
(appearance) => html`
${(['solid', 'fill', 'outline', 'dashed', 'plain'] as const).map(
(variant) => html`
<div style="display:flex;align-items:center;gap:1rem">
<span style="width:4rem;font-size:0.75rem;color:var(--c-text-muted)"
>${appearance}</span
>${variant}</span
>
<craft-button-group name="view-${appearance}" value="b">
<craft-button appearance="${appearance}" value="a"
>A</craft-button
>
<craft-button appearance="${appearance}" value="b"
>B</craft-button
>
<craft-button appearance="${appearance}" value="c"
>C</craft-button
>
<craft-button-group name="view-${variant}" value="b">
<craft-button variant="${variant}" value="a">A</craft-button>
<craft-button variant="${variant}" value="b">B</craft-button>
<craft-button variant="${variant}" value="c">C</craft-button>
</craft-button-group>
</div>
`
Expand Down Expand Up @@ -141,7 +135,7 @@ export const FormAssociated: Story = {
<craft-button value="table">Table</craft-button>
<craft-button value="thumbs">Thumbnails</craft-button>
</craft-button-group>
<craft-button type="submit" appearance="filled">Submit</craft-button>
<craft-button type="submit" variant="fill">Submit</craft-button>
</form>
`,
};
Expand Down
40 changes: 22 additions & 18 deletions packages/craftcms-ui/src/components/button/Docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,42 @@ A button represents a clickable element that performs an action when activated.

<Canvas of={ButtonStories.Default} />

## 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.

<Canvas of={ButtonStories.Accent} />
`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.

<Canvas of={ButtonStories.Inherit} />

## Sizes

<Canvas of={ButtonStories.Sizes} />

## Icon buttons
## Icons

<Canvas of={ButtonStories.Icon} />
<Canvas of={ButtonStories.Icons} />

## Loading state

<Canvas of={ButtonStories.Loading} />

## Links

<Canvas of={ButtonStories.Links} />
Loading
Loading