Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
130b1a4
Add craft-field web component
brianjhanson Jul 9, 2026
f530b26
Add CP UI component system
brianjhanson Jul 9, 2026
51174c1
Render FormFields and lightswitch.twig through UI components
brianjhanson Jul 9, 2026
2d74885
Route lightswitch.twig through the PHP config mapper
brianjhanson Jul 9, 2026
0d584cc
Port button and buttonGroup to UI components
brianjhanson Jul 9, 2026
a2320c2
Port checkbox to a UI component
brianjhanson Jul 9, 2026
5d3ccd3
Always render an explicit type on craft-button
brianjhanson Jul 9, 2026
d24e682
Deprecate mismatched legacy config keys in the component mappers
brianjhanson Jul 9, 2026
5acfabb
Merge branch 'feature/inertia-field-edit' of github.com:craftcms/cms …
brianjhanson Jul 9, 2026
6d9de97
Clean things up a bit
brianjhanson Jul 9, 2026
400557a
Checkboxes and radios
brianjhanson Jul 10, 2026
1c861f9
Fix field width
brianjhanson Jul 10, 2026
8ed0b90
BaseOptions cleanup
brianjhanson Jul 10, 2026
3c49ba0
Implement radio group
brianjhanson Jul 10, 2026
241861b
Give action menu search capabilities
brianjhanson Jul 13, 2026
ef3a737
Fix AppLayout bug
brianjhanson Jul 13, 2026
69ab137
garnish: add nearestSibling, closestRegistered, and toHtmlElement utils
brianjhanson Jul 14, 2026
56c63ef
craft-reorder-button: add horizontal orientation
brianjhanson Jul 14, 2026
6c6159b
craft-action-menu: add keyboard-navigation tests
brianjhanson Jul 14, 2026
c6c2a8f
Render chip and card action menus as craft-action-menu
brianjhanson Jul 14, 2026
91f0a0a
Port ComponentSelectInput to <craft-component-select>
brianjhanson Jul 14, 2026
5d63a4a
Port GroupedEntryTypeManager to <craft-entry-type-manager>
brianjhanson Jul 14, 2026
548c023
Fix chip list spacing for iconless chips
brianjhanson Jul 14, 2026
c7bcbb1
Render legacy field-settings islands in Inertia
brianjhanson Jul 14, 2026
f7df03e
docs: document the CP JS module-port pattern
brianjhanson Jul 14, 2026
865ef03
listbox: use toHtmlElement from garnish compat
brianjhanson Jul 14, 2026
6f97075
craft-disclosure: extend LionCollapsible
brianjhanson Jul 14, 2026
c199c3a
Convert field-settings Advanced toggles to craft-disclosure
brianjhanson Jul 14, 2026
68df108
Sync checkbox host/input state for the legacy All handler
brianjhanson Jul 14, 2026
fd9762c
Restore multi-select to craft-component-select
brianjhanson Jul 14, 2026
5e3bdc9
Add cross-group chip drag to entry-type manager
brianjhanson Jul 14, 2026
c91ee9d
Port entry-type override editor to web-component path
brianjhanson Jul 14, 2026
3d95872
Fix icons
brianjhanson Jul 14, 2026
88353df
Improve spacing
brianjhanson Jul 14, 2026
0801091
Migrate entryTypeSelect off legacy Craft.EntryTypeSelectInput
brianjhanson Jul 14, 2026
f432f00
Give web-component properties explicit kebab-case attributes
brianjhanson Jul 15, 2026
c756064
Add PHP<->web-component drift-detection test
brianjhanson Jul 15, 2026
104e5c0
Remove extra `FormFields::buttonHtml` layer
brianjhanson Jul 15, 2026
af11eaf
Remove extra FormFields::*Html layer for component-backed fields
brianjhanson Jul 15, 2026
4902725
Merge branch '6.x' into feature/cp-ui-components
brianjhanson Jul 15, 2026
f9af6a4
Fix phpstan
brianjhanson Jul 15, 2026
5d58722
Remove console.log
brianjhanson Jul 15, 2026
227852c
Replace `FormFields::*FromConfig` calls within core
brianjhanson Jul 15, 2026
df18d1a
Add deprecation warnings mostly to keep track of things
brianjhanson Jul 15, 2026
a2450db
Clean house a bit
brianjhanson Jul 15, 2026
e9ff05d
Fix typo
brianjhanson Jul 15, 2026
49a6428
Better button styles
brianjhanson Jul 15, 2026
d71b9ce
Accept float values
brianjhanson Jul 15, 2026
e996c09
Fix typo
brianjhanson Jul 15, 2026
c77bbff
Update yii2-adapter tests
brianjhanson Jul 15, 2026
f59b290
Fix globby ... again?
brianjhanson Jul 15, 2026
1f8b8af
Generate CEM before tests run
brianjhanson Jul 16, 2026
6f781a9
Merge remote-tracking branch 'origin/6.x' into feature/cp-ui-components
brianjhanson Jul 20, 2026
4e2d9f2
Cleanup icon button selector
brianjhanson Jul 20, 2026
e3395ef
Merge branch '6.x' of github.com:craftcms/cms into feature/cp-ui-comp…
brianjhanson Jul 20, 2026
4cee494
Final cleanup
brianjhanson Jul 20, 2026
f35060c
Make tagName concrete
brianjhanson Jul 20, 2026
4c0add7
`toContainTag` helper and less fragile tests
brianjhanson Jul 20, 2026
0e01ef1
Back to abstract
brianjhanson Jul 20, 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
7 changes: 7 additions & 0 deletions .github/workflows/js-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ jobs:
if: steps.cache.outputs.cache-hit != 'true'
run: npm run build:ui

# `build:cp` does not produce the Custom Elements Manifest (only the
# Storybook pre-hooks do), so generate it explicitly — it lands in the
# packages/craftcms-cp/dist cache path above for the downstream jobs.
- name: Build Custom Elements Manifest
if: steps.cache.outputs.cache-hit != 'true'
run: cd ./packages/craftcms-ui && npm run build:manifest

- name: Generate TypeScript types
if: steps.cache.outputs.cache-hit != 'true'
run: npm run generate:types
Expand Down
17 changes: 17 additions & 0 deletions .github/workflows/laravel-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,23 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

# ComponentManifestDriftTest needs the Custom Elements Manifest, which is
# gitignored (packages/craftcms-ui/dist). The cache key hashes everything
# the analyzer reads, so a hit is never stale; the cache itself is shared
# repo-wide, so whichever workflow generates it first serves the rest. On
# a miss the pinned npx run (~seconds) needs no npm install.
- name: Restore Custom Elements Manifest
id: cem
uses: actions/cache@v4
with:
path: packages/craftcms-ui/dist/custom-elements.json
key: cem-${{ hashFiles('packages/craftcms-ui/src/components/**', 'packages/craftcms-ui/custom-elements-manifest.config.mjs', 'packages/craftcms-ui/package.json', 'packages/craftcms-ui/tsconfig.json') }}

- name: Generate Custom Elements Manifest
if: steps.cem.outputs.cache-hit != 'true'
working-directory: packages/craftcms-ui
run: npx --yes @custom-elements-manifest/analyzer@0.11.0 analyze

- name: Run tests
uses: ./.github/actions/run-tests
with:
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ partially migrated; full migration means converting the inner form to a Vue comp
**Custom elements** (anything with a hyphen in the tag name) are treated as native web components by the Vue compiler —
they pass through to the browser without Vue trying to resolve them as Vue components.

When porting behavior out of the legacy jQuery bundle (`packages/craftcms-legacy/cp/src/js/*.js`) into modern TypeScript, follow the shared module pattern documented in `resources/js/modules/README.md` — a logic class (`<name>.ts` on `@craftcms/garnish` `Base`), a `ControllerElement` custom element (`<name>.ce.ts`), an instance-registry `support.ts` WeakMap, and an `index.ts` shim that registers the element and assigns the legacy `window.Craft.*` global. Note the source-vs-`dist` gotcha in that README: after editing `packages/craftcms-garnish/src` (or `@craftcms/cp`), rebuild the package's `dist` or `npm run typecheck` won't see the change.

## Adapter Work

`yii2-adapter` is compatibility code, not the implementation path for new core behavior. If you need to add adapter classes, follow its Composer autoload mapping. Do not put general adapter classes in `yii2-adapter/lib/`; that area is for vendored or library-style code.
Expand Down
23 changes: 23 additions & 0 deletions packages/craftcms-garnish/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,29 @@ then the menu hides. `DisclosureMenu.getInstance(triggerOrContainer)` is the nat
replacement for the legacy `$el.data('disclosureMenu')`. A legacy
`new Garnish.DisclosureMenu($trigger, settings)` call ports across unchanged.

#### Item selection — `Select`

`Select` is the selection interface over a set of sibling items: click to select,
shift-click to extend a contiguous range, and ctrl/⌘-click to toggle individual
items (the ctrl/shift roles swap under `checkboxMode`). Selected items get the
`selectedClass` (`sel` by default); arrow keys move the selection two-dimensionally
by measuring item geometry, so it works for vertical lists and wrapping grids alike.
It pairs naturally with `DragSort` for drag-a-multi-selection lists (see
`<craft-component-select>`).

```ts
import {Select} from '@craftcms/garnish';

const select = new Select({multi: true, filter: (t) => !(t as Element)?.closest('button')});
select.addItems(list.querySelectorAll('li'));
select.on('selectionChange', () => console.log(select.getSelectedItems()));
```

The constructor mirrors the legacy shape — `new Select(container, items?, settings?)`,
with `new Select(settings)` / `new Select(container, settings)` param shifts — so a
legacy `new Garnish.Select({...})` call ports across unchanged. `getSelectedItems()`
returns a native `HTMLElement[]` (not a jQuery collection).

**No-jQuery guarantee:** importing from `@craftcms/garnish` (the `.` entry) never
pulls in jQuery, never reads `window.jQuery`/`$`, and never assigns
`window.Garnish`. Those behaviors live exclusively in the `compat` entry.
Expand Down
55 changes: 55 additions & 0 deletions packages/craftcms-garnish/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,61 @@ are recomputed per insertion; lists `> 200` items use a viewport filter. Unlike

---

## `Select`

`class Select<S extends SelectSettings = SelectSettings> extends Base<S>` — the
selection interface over a set of sibling items: click to select, shift-click to extend
a contiguous range, ctrl/⌘-click to toggle individual items (ctrl/shift roles swap under
`checkboxMode`). Selected items get the `selectedClass`; arrow keys move the selection
two-dimensionally by measuring item geometry, so it spans vertical lists and wrapping
grids. Commonly paired with `DragSort` to drag a multi-selection as a group.

**Constructor:** `new Select(container?, items?, settings?)` — with `new Select(settings)`
and `new Select(container, settings)` param-shifts (a plain-object arg is treated as the
settings).

### Statics

| Member | Type | Description |
| --- | --- | --- |
| `Select.defaults` | `SelectSettings` | Default settings (below). |

### Settings (`SelectSettings`)

| Key | Default | Description |
| --- | --- | --- |
| `selectedClass` | `'sel'` | Class added to selected items. |
| `checkboxClass` | `'checkbox'` | Class of a checkbox affordance inside an item; gets `aria-checked` toggled. |
| `multi` | `false` | Allow more than one item selected at once. |
| `allowEmpty` | `true` | Allow the selection to be emptied (container click / last-item deselect). |
| `vertical` / `horizontal` | `false` | Constrain arrow-key nav to a single column / row. |
| `handle` | `null` | What within each item receives the pointer listeners: selector / fn / element(s) / `null` (the item). |
| `filter` | `null` | Gate before selecting: a selector string or `(target) => boolean` predicate. |
| `checkboxMode` | `false` | Whether a checkbox affordance (not ctrl/shift) drives selection. |
| `makeFocusable` | `false` | Give the focused item a roving `tabindex` (keyboard-navigable list). |
| `waitForDoubleClicks` | `false` | Defer single-click (de)selection briefly so a double-click can pre-empt it. |
| `onSelectionChange` | no-op | Fired (RAF-deferred) whenever the selection changes. |

### Methods & properties

| Member | Signature | Description |
| --- | --- | --- |
| `$items` / `$selectedItems` | `HTMLElement[]` | Tracked items / the selected subset (native arrays). |
| `addItems` / `removeItems` | `(items) => void` | Track / untrack items (binds/unbinds their listeners). |
| `selectItem` / `selectRange` / `selectAll` | `(item?, …) => void` | Select one / a contiguous range / everything (range & all are `multi`-only). |
| `deselectItem` / `deselectAll` / `deselectOthers` | `(item?) => void` | Deselect one / all / all-but-one. |
| `toggleItem` | `(item, preventScroll?) => void` | Toggle a single item (honoring `allowEmpty`). |
| `isSelected` | `(item) => boolean` | Whether an item is selected. |
| `getSelectedItems` | `() => HTMLElement[]` | The selected items, as a fresh array. |
| `totalSelected` | `get => number` | How many are selected. |
| `resetItemOrder` | `() => void` | Re-sort `$items` / `$selectedItems` into current DOM order (after a reorder). |
| `focusItem` / `setFocusableItem` | `(item, …) => void` | Move DOM focus / set the roving-`tabindex` item. |

**Events:** `selectionChange` (RAF-deferred), `focusItem` (`{item}`), plus `Base`'s
`destroy`.

---

## `UiLayerManager`

`class UiLayerManager extends Base` — manages the stack of UI layers (document,
Expand Down
16 changes: 16 additions & 0 deletions packages/craftcms-garnish/src/compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ export function toJq(value: unknown): JQueryCollection {
*
* @param value - The (possibly jQuery) value.
* @returns The native element, or the value unchanged.
*
* @TODO replace instances of this with `toHtmlElement`
*/
export function unwrapJq(value: unknown): unknown {
if (value == null) {
Expand All @@ -147,6 +149,20 @@ export function unwrapJq(value: unknown): unknown {
return value;
}

/**
* Unwrap a jQuery collection (truthy `.jquery`) to its first native element,
* pass a native `Element` through, and return `null` for anything else.
*
* This has some overlap with `unwrapJq` but the types are better here.
*/
export function toHtmlElement(value: unknown): HTMLElement | null {
if (isJquery(value)) {
return value.length && value[0] instanceof HTMLElement ? value[0] : null;
}

return value instanceof HTMLElement ? value : null;
}

/* ------------------------------------------------------------------------- *
* compatify(ModernClass) — the legacy `.extend()` / `this.base()` shim
* ------------------------------------------------------------------------- */
Expand Down
4 changes: 4 additions & 0 deletions packages/craftcms-garnish/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {Drag, type DragSettings} from './drag/drag';
import {DragDrop, type DragDropSettings} from './drag/drag-drop';
import {DragSort, type DragSortSettings} from './drag/drag-sort';
import {DragMove} from './drag-move';
import {Select, type SelectSettings} from './select';
import {ResizeHandle} from './icons/resize-handle';
import {garnishClassBus, globals, win, doc, bod} from './globals';
import type {Callback, Constructor, GarnishBaseSettings} from './types';
Expand Down Expand Up @@ -77,6 +78,8 @@ export {Drag, type DragSettings};
export {DragDrop, type DragDropSettings};
export {DragSort, type DragSortSettings};
export {DragMove};
export {Select, type SelectSettings};
export type {SelectHandle, SelectFilter} from './select';
export {ResizeHandle};
export {win, doc, bod};

Expand Down Expand Up @@ -184,6 +187,7 @@ export const Garnish = {
DragDrop,
DragSort,
DragMove,
Select,
/** @deprecated Use UiLayerManager instead. */
ShortcutManager: UiLayerManager,

Expand Down
Loading
Loading