Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
8d1c0d5
More chip refinement
brianjhanson Aug 27, 2026
aa7a435
Regenerate the custom elements manifest while Storybook runs
brianjhanson Aug 28, 2026
637e3c1
Give craft-chip's selectable checkbox an accessible name
brianjhanson Aug 28, 2026
e9522b6
Document craft-callout, and limit padding to the spacing scale
brianjhanson Aug 28, 2026
58758a9
Document craft-card
brianjhanson Aug 28, 2026
b043d34
Make craft-button's accessible name internal
brianjhanson Aug 28, 2026
5531099
Document craft-action-item
brianjhanson Aug 28, 2026
315c33c
Document craft-input-date-time
brianjhanson Aug 28, 2026
9fe96d0
Fix the nested field in craft-input-date-time's In A Field story
brianjhanson Aug 28, 2026
3215641
Fix the Date Only and Time Only stories showing both inputs
brianjhanson Aug 28, 2026
a17d751
Restore the In A Field story for craft-input-date-time
brianjhanson Aug 28, 2026
7e25825
Guard against docs pages referencing stories that do not exist
brianjhanson Aug 28, 2026
44c865d
Document craft-input, craft-input-date, and craft-input-time
brianjhanson Aug 29, 2026
5d27c61
Document the rest of the craft-input-* family
brianjhanson Aug 31, 2026
69fcf71
Extend craft-input from the password and color inputs
brianjhanson Aug 31, 2026
b311a29
Document craft-icon
brianjhanson Aug 31, 2026
baf6757
Document craft-info-icon
brianjhanson Aug 31, 2026
1e7b384
Document craft-spinner
brianjhanson Aug 31, 2026
468454f
Document craft-avatar
brianjhanson Aug 31, 2026
ca2a20b
Document craft-thumbnail
brianjhanson Aug 31, 2026
84c480f
Document craft-thumbnail-loader
brianjhanson Aug 31, 2026
eb39255
Document craft-status
brianjhanson Aug 31, 2026
eae289e
Document craft-visually-hidden
brianjhanson Aug 31, 2026
c1db691
Document craft-nav-list
brianjhanson Aug 31, 2026
edb758a
Document craft-field-group
brianjhanson Aug 31, 2026
c11d4af
Group the form controls under Form Controls
brianjhanson Aug 31, 2026
20b5daf
Document craft-checkbox
brianjhanson Aug 31, 2026
730c352
Document craft-checkbox-group
brianjhanson Aug 31, 2026
8f77df2
Document craft-checkbox-indeterminate
brianjhanson Aug 31, 2026
f5b0907
Document craft-radio
brianjhanson Aug 31, 2026
dd28f50
Document craft-radio-group
brianjhanson Aug 31, 2026
b342f2f
Document craft-switch
brianjhanson Aug 31, 2026
89acabf
Document craft-switch-button
brianjhanson Aug 31, 2026
bd4cc6e
Document craft-select
brianjhanson Aug 31, 2026
af002fd
Document craft-select-rich
brianjhanson Aug 31, 2026
93e56a2
Document craft-option
brianjhanson Aug 31, 2026
a486117
Document craft-select-color
brianjhanson Aug 31, 2026
f906ab4
Document craft-combobox
brianjhanson Aug 31, 2026
5b4346e
Document craft-field
brianjhanson Aug 31, 2026
2328726
Document craft-pane
brianjhanson Aug 31, 2026
bc5d102
Document craft-breadcrumbs
brianjhanson Aug 31, 2026
7da4110
Document craft-breadcrumb-item
brianjhanson Aug 31, 2026
ca3f494
Document craft-nav-item
brianjhanson Aug 31, 2026
4b8b87b
Document craft-tabs
brianjhanson Aug 31, 2026
06dbe28
Document craft-tab
brianjhanson Aug 31, 2026
7990a0c
Document craft-empty
brianjhanson Aug 31, 2026
8bc0abe
Document craft-badge-indicator
brianjhanson Aug 31, 2026
60c942f
Document craft-progress
brianjhanson Aug 31, 2026
a0ee265
Document craft-progress-bar
brianjhanson Aug 31, 2026
213a3e7
Document craft-dialog
brianjhanson Aug 31, 2026
107c0c1
Document craft-popover
brianjhanson Aug 31, 2026
2d03720
Document craft-disclosure
brianjhanson Aug 31, 2026
06f96d7
Document craft-action-menu
brianjhanson Aug 31, 2026
8bb33e9
Document craft-slide-picker
brianjhanson Sep 1, 2026
78d75f0
Document craft-slide-rule
brianjhanson Sep 1, 2026
12ba503
Document craft-reorder-button
brianjhanson Sep 1, 2026
201904c
Document craft-truncate
brianjhanson Sep 1, 2026
a599078
Document craft-text-expander
brianjhanson Sep 1, 2026
26b4f4b
Document craft-shortcut
brianjhanson Sep 1, 2026
2b118b9
Document craft-copy-button
brianjhanson Sep 1, 2026
d83189f
Document craft-copy-attribute
brianjhanson Sep 1, 2026
d68a32e
Document craft-button-group
brianjhanson Sep 1, 2026
fce13ab
Document craft-textarea
brianjhanson Sep 1, 2026
d2d438e
Document craft-permission-tree
brianjhanson Sep 1, 2026
f5352ae
Document craft-missing-component
brianjhanson Sep 1, 2026
0e692b2
Check that docs pages import stories that exist
brianjhanson Sep 1, 2026
3c602f1
Move craft-indicator's API docs into its annotations
brianjhanson Sep 1, 2026
8871a64
Collapse the duplicated attribute and light-DOM plumbing
brianjhanson Sep 2, 2026
79f6296
Guard the manifest against undocumented and placeholder API
brianjhanson Sep 2, 2026
08d35f3
Generate every docs page's API table from the manifest
brianjhanson Sep 2, 2026
c749c1f
Align three naming inconsistencies in the component API
brianjhanson Sep 2, 2026
d036ffd
Let craft-pane's title choose its heading level
brianjhanson Sep 2, 2026
e94ac25
Correct craft-chip's size docs and stop craft-truncate double-rendering
brianjhanson Sep 2, 2026
cfbf406
Give the docs site a front door
brianjhanson Sep 2, 2026
9b470a4
Fix the two accessibility defects the docs had only described
brianjhanson Sep 2, 2026
c51f078
Give craft-card the padding and parts craft-pane has
brianjhanson Sep 2, 2026
aa02471
Derive story args from the manifest: components
brianjhanson Sep 2, 2026
9973fd1
Derive story args from the manifest: choice and select controls
brianjhanson Sep 2, 2026
0c1b9bb
Derive story args from the manifest: the input family
brianjhanson Sep 2, 2026
8c0d6b5
Derive story args from the manifest: the remaining components
brianjhanson Sep 2, 2026
fc578f2
Test the presentational components
brianjhanson Sep 2, 2026
d5440b6
Test the progress, badge, and navigation components
brianjhanson Sep 2, 2026
daee6d8
Test the text controls and the copy components
brianjhanson Sep 2, 2026
53d05ef
Test the remaining components
brianjhanson Sep 2, 2026
b67bb60
Stop the tabs overflow story failing on Lion's overlay wrapper
brianjhanson Sep 2, 2026
5188d96
Make the slide controls form-associated
brianjhanson Sep 3, 2026
521e295
Prefix the component events, following Web Awesome
brianjhanson Sep 3, 2026
275e67d
Sit chips on the raised surface
brianjhanson Sep 4, 2026
8203fc6
Give the Lion controls a native event surface
brianjhanson Sep 4, 2026
b1af07b
Document the factory, utilities, services, and Vue wrappers
brianjhanson Sep 4, 2026
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
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions packages/craftcms-legacy/cp/src/js/AssetImageEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -930,15 +930,17 @@ Craft.AssetImageEditor = Garnish.Modal.extend(
});

// Straighten slider — the <craft-slide-rule> web component (replaces the
// legacy Craft.SlideRuleInput), driven via its start/change/end events.
// legacy Craft.SlideRuleInput). The grid follows the drag gesture, and
// the image follows `input`, which fires continuously through the drag
// (`change` only fires once, on release).
this.straighteningInput = document.getElementById('slide-rule');
this.straighteningInput.addEventListener('start', () => {
this.straighteningInput.addEventListener('craft-drag-start', () => {
this._showGrid();
});
this.straighteningInput.addEventListener('change', () => {
this.straighteningInput.addEventListener('input', () => {
this.straighten(this.straighteningInput);
});
this.straighteningInput.addEventListener('end', () => {
this.straighteningInput.addEventListener('craft-drag-end', () => {
this._hideGrid();
this._cleanupFocalPointAfterStraighten();
});
Expand Down
4 changes: 2 additions & 2 deletions packages/craftcms-legacy/cp/src/js/CraftGlobalSidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ class CraftGlobalSidebar extends HTMLElement {
this.trigger = this.querySelector('#sidebar-trigger');

if (this.trigger) {
this.trigger.addEventListener('open', this.expand.bind(this));
this.trigger.addEventListener('close', this.collapse.bind(this));
this.trigger.addEventListener('craft-show', this.expand.bind(this));
this.trigger.addEventListener('craft-hide', this.collapse.bind(this));
}
}

Expand Down
17 changes: 3 additions & 14 deletions packages/craftcms-legacy/cpcompat/src/cp-compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -566,15 +566,8 @@
this.setValue(value, false);

var self = this;
this.$container.on('value-change', function (event) {
if (
event &&
event.originalEvent &&
event.originalEvent.detail &&
typeof event.originalEvent.detail.value === 'number'
) {
self.setValue(event.originalEvent.detail.value);
}
this.$container.on('change', function () {
self.setValue(self.$container[0].value);
});
}

Expand Down Expand Up @@ -604,11 +597,7 @@
el.removeAttribute('described-by');
}

if (this.settings.readOnly) {
el.setAttribute('read-only', '');
} else {
el.removeAttribute('read-only');
}
el.readonly = !!this.settings.readOnly;

this.$buttons = this.$container.find('.slide-picker__segment');

Expand Down
4 changes: 2 additions & 2 deletions packages/craftcms-legacy/dashboard/src/Dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -774,8 +774,8 @@ import './dashboard.scss';

refreshColspanPicker();

this.colspanPicker.addEventListener('value-change', ({detail}) => {
const colspan = detail.value;
this.colspanPicker.addEventListener('change', () => {
const colspan = this.colspanPicker.value;

// Update the widget and grid
this.setColspan(colspan);
Expand Down
20 changes: 20 additions & 0 deletions packages/craftcms-ui/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,29 @@ const preview: Preview = {
},
},

docs: {
// Show each story's source in a "Code" panel alongside Controls, so the
// markup is available from the story view and not only from the docs
// page. Stories that pin `docs.source.code` supply that value here too.
codePanel: true,
},

options: {
storySort: {
method: 'alphabetical',
// Everything else sorts alphabetically beneath these. Getting Started
// leads because a first-time reader lands on the sidebar, and Tokens
// trails because it explains what the component pages reference.
order: [
'Getting Started',
['Introduction', 'Installation', 'Theming'],
'Components',
'Form Controls',
['Choice Controls', 'Select Controls', 'Text Controls'],
'JavaScript API',
['Factory', 'Utilities', 'Services', 'Reactive Controllers'],
'Tokens',
],
},
},

Expand Down
93 changes: 93 additions & 0 deletions packages/craftcms-ui/custom-elements-manifest.config.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
import {getTsProgram, typeParserPlugin} from '@wc-toolkit/type-parser';

/**
* The type parser warns once for every type it declines to expand: DOM
* interfaces reached through a property (`Element`, `HTMLCanvasElement`), the
* component classes themselves, and anything past its depth or property
* limits. Expanding those was never the point — only the union aliases behind
* `size`, `variant`, and `appearance` reach the Storybook controls — so the
* bail-out notices are dropped here.
*
* The filter is deliberately narrow. Real parser warnings, such as a bad
* `tsconfig.json`, use a different prefix and still print. The parser calls
* `console.warn(colorFormat, message)`, so the message is the second argument.
*/
const SKIPPED_TYPE_NOTICE = '[type-parser] - Skipped parsing type';
const consoleWarn = console.warn;
console.warn = (...args) => {
if (args.some((arg) => typeof arg === 'string' && arg.includes(SKIPPED_TYPE_NOTICE))) {
return;
}
consoleWarn(...args);
};

/**
* A leaked TypeScript AST node (e.g. a `SourceFile`, which holds a circular
* `parent` pointer) is a TS node, not a manifest value. The CEM manifest schema
Expand Down Expand Up @@ -47,7 +70,21 @@ export default {
globs: ['src/components/**/*.ts'],
exclude: ['**/*.stories.ts', '**/*.styles.ts', '**/*.test.ts'],
outdir: 'dist',
// The type parser needs a real TypeScript program so it can resolve type
// aliases (`SizeValue`) back to their union members ('small' | 'medium' |
// 'large'). Without this the manifest only records the alias name, and
// Storybook renders a text box instead of a select.
overrideModuleCreation: ({ts, globs}) => {
const program = getTsProgram(ts, globs, 'tsconfig.json');
return program
.getSourceFiles()
.filter((sf) => globs.find((glob) => sf.fileName.includes(glob)));
},
plugins: [
// Expand type aliases into `parsedTypes`, which `.storybook/preview.ts`
// reads via `setStorybookHelpersConfig({typeRef: 'parsedTypes'})`.
typeParserPlugin({propertyName: 'parsedTypes'}),

// Add a plugin to prevent inheritance tree analysis errors
{
name: 'skip-external-inheritance',
Expand All @@ -73,6 +110,62 @@ export default {
});
},
},
// The manifest is the package's public API description: Storybook's
// property tables and editors' IntelliSense are generated from it. The
// analyzer records every class member it finds, so without this the tables
// list internals — `craft-permission-tree` advertised a `#treeId`, and 45
// elements between them exposed 408 members no consumer can call.
//
// Dropped: TypeScript `private`/`protected`, JS `#private` fields, and the
// `_`/`__` prefixes Lion and this package use for the same thing. Anything
// meant to be public and prefixed should be renamed rather than exempted.
{
name: 'drop-internal-members',
packageLinkPhase({customElementsManifest}) {
const isInternal = (member) =>
member.privacy === 'private' ||
member.privacy === 'protected' ||
member.name?.startsWith('#') ||
member.name?.startsWith('_');

customElementsManifest?.modules?.forEach((module) => {
module?.declarations?.forEach((declaration) => {
if (declaration.members) {
declaration.members = declaration.members.filter(
(member) => !isInternal(member)
);
}
});
});
},
},

// The analyzer also infers events from `dispatchEvent()` calls, reading the
// first argument literally — so a helper that dispatches
// `new CustomEvent(type, …)` is recorded as an event named `type`. That is
// an artifact of the call site, not API, and it reaches the docs tables.
//
// Once a class declares its events with `@fires`, that list is taken as the
// whole story and inferred extras are dropped. A class with no `@fires` is
// left alone, so nothing is hidden from the guard that requires every
// remaining event to be described.
{
name: 'declared-events-win',
packageLinkPhase({customElementsManifest}) {
customElementsManifest?.modules?.forEach((module) => {
module?.declarations?.forEach((declaration) => {
const events = declaration.events;

if (!events?.some((event) => event.description)) {
return;
}

declaration.events = events.filter((event) => event.description);
});
});
},
},

// Strip any leaked TS AST nodes so the analyzer can serialize the manifest.
// analyzer@0.11.0 (under Node 24) leaks `SourceFile` nodes into the manifest,
// and their circular `parent` pointers crash the final `JSON.stringify`. Runs
Expand Down
7 changes: 6 additions & 1 deletion packages/craftcms-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"web-components"
],
"scripts": {
"pretest": "npm run build:manifest",
"test": "vp test run",
"test:dev": "vp test",
"test:coverage": "vp test run --coverage",
Expand All @@ -26,7 +27,9 @@
"check:exports": "attw --pack .",
"check:types": "tsc --noEmit",
"prestorybook": "npm run build:manifest",
"storybook": "storybook dev -p 6006",
"watch:manifest": "npm run build:manifest -- --watch",
"storybook": "run-p -l watch:manifest storybook:dev",
"storybook:dev": "storybook dev -p 6006",
"prebuild:storybook": "npm run build:manifest",
"build:storybook": "storybook build",
"build:manifest": "custom-elements-manifest analyze --litelement --outdir dist",
Expand Down Expand Up @@ -99,8 +102,10 @@
"@types/node": "^25.6.0",
"@vitest/coverage-v8": "4.1.10",
"@wc-toolkit/storybook-helpers": "^10.3.0",
"@wc-toolkit/type-parser": "^1.3.1",
"del": "^8.0.1",
"dom-accessibility-api": "^0.7.1",
"element-internals-polyfill": "^3.0.2",
"esbuild": "^0.28.0",
"globby": "^16.2.0",
"happy-dom": "^20.9.0",
Expand Down
109 changes: 109 additions & 0 deletions packages/craftcms-ui/src/components/action-item/ActionItem.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {ArgTypes, Canvas, Meta} from '@storybook/addon-docs/blocks';
import * as ActionItemStories from './action-item.stories';

<Meta of={ActionItemStories} name="Docs" />

# Action Item

`<craft-action-item>` is a single entry in a menu.
It renders as a button, or as a link when `href` is set, and carries an optional leading icon, a label, and an optional trailing shortcut or suffix.

<Canvas of={ActionItemStories.Default} sourceState="shown" />

Action items are usually supplied to [Action Menu](?path=/docs/components-action-menu--docs) rather than placed by hand — in data-driven mode it builds them from an `actions` array.
Reach for the element directly when you are slotting your own menu content.

An item has no width of its own; it fills whatever menu contains it.
The examples below sit in a menu-sized container so the layout reads correctly.

## Content

`icon` renders artwork before the label, and the `icon` slot replaces it when you need something the icon set does not cover:

<Canvas of={ActionItemStories.WithIcon} sourceState="shown" />

The `suffix` slot holds trailing content, before any shortcut:

<Canvas of={ActionItemStories.WithSuffix} sourceState="shown" />

A `shortcut` is shown at the end of the item.
It is display only — the item renders the keys but does not bind them, so the shortcut has to be wired up separately:

<Canvas of={ActionItemStories.WithShortcut} sourceState="shown" />

Pass an object to name the modifiers:

<Canvas of={ActionItemStories.WithComplexShortcut} sourceState="shown" />

## States

`active` marks the entry a menu opens onto, or the option already in effect.
It is reflected, so it can be styled from outside the component:

<Canvas of={ActionItemStories.Active} sourceState="shown" />

`disabled` dims the item and stops it being activated:

<Canvas of={ActionItemStories.Disabled} sourceState="shown" />

`variant` colors the item.
Reach for `danger` on a destructive entry, so it reads as one before it is clicked:

<Canvas of={ActionItemStories.Variants} sourceState="shown" />

## Buttons, Links, and Checkboxes

By default an item is a `<button>`.
Setting `href` makes it an `<a>` instead, which is what you want for anything that navigates — it gets the browser's own link affordances, including opening in a new tab:

<Canvas of={ActionItemStories.Link} sourceState="shown" />

`type="checkbox"` reserves room for a checkmark before the icon, so a list of options stays aligned whether or not each one is checked.
The `checkmark` slot replaces the default check:

<Canvas of={ActionItemStories.Checkable} sourceState="shown" />

## Running an Action

Set `action` to an action descriptor, or a URL string, and the item runs it when activated.
It reports progress in place: a spinner while an HTTP action is in flight, then a check or a cross, before returning to its label.

```html
<craft-action-item icon="trash" variant="danger"> Delete </craft-action-item>
```

```js
item.action = {
type: 'http',
method: 'POST',
url: '/actions/entries/delete',
// Prompts before the request goes out.
confirm: 'Delete this entry?',
};
item.feedback = {success: {message: 'Deleted'}, error: {message: 'Failed'}};
```

Confirmation belongs to the action descriptor, not the item.
`feedback` supplies the messages shown in place of the label, and `feedback-duration` controls how long they stay.

Each transition emits `craft-state-change`, whose `detail` carries the new `state`, the action's `actionType`, and any feedback data — useful when a menu needs to react to an item's progress.

## Theming

An item inherits the palette of any `data-color` ancestor, so a menu can tint its entries without overriding tokens:

<Canvas of={ActionItemStories.Colors} sourceState="shown" />

## Accessibility

The item renders a real `<button>` or `<a>`, so it is focusable and activates on Enter and Space without any help.
Focus is delegated into the shadow root, which means `item.focus()` lands on the control rather than the host — that is how the parent menu's arrow-key navigation works.

Two things are on you:

- **Give an icon-only item a label.** The default slot is the accessible name. An item with nothing but an `icon` is nameless.
- **Do not put controls in the `suffix` slot.** The slot sits inside the item's own button, so an interactive suffix nests a control inside a control — a shape assistive technology cannot present, and one that swallows the click meant for the item. The item marks the suffix in red when it finds one, the same way [Button](?path=/docs/components-button--docs) marks itself when it has no accessible name.

## Properties

<ArgTypes of={ActionItemStories} />
Loading