From 197281bb08dd4e3e1c5faae18da56c13878eae48 Mon Sep 17 00:00:00 2001
From: Brian Hanson
Date: Wed, 1 Apr 2026 15:16:46 -0500
Subject: [PATCH 1/5] `.c-colorable` -> `.cp-colorable` for consistency
---
.../craftcms-cp/scripts/generate-colors.js | 4 +-
.../src/styles/shared/colorable.css | 60 +++++++++----------
2 files changed, 32 insertions(+), 32 deletions(-)
diff --git a/packages/craftcms-cp/scripts/generate-colors.js b/packages/craftcms-cp/scripts/generate-colors.js
index 5533c2423f7..995c709da7c 100644
--- a/packages/craftcms-cp/scripts/generate-colors.js
+++ b/packages/craftcms-cp/scripts/generate-colors.js
@@ -136,7 +136,7 @@ function buildTokens(colors, scaleFn) {
}
function buildStyleBlock(color) {
- return `.c-colorable--${color},
+ return `.cp-colorable--${color},
[data-color='${color}'] {
--c-color-fill-quiet: var(--c-color-${color}-fill-quiet);
--c-color-border-quiet: var(--c-color-${color}-border-quiet);
@@ -161,7 +161,7 @@ ${buildTokens(colors, lightScale)}
${buildTokens(colors, darkScale)}
}
-.c-colorable,
+.cp-colorable,
[data-color] {
--c-color-fill-quiet: var(--c-color-neutral-fill-quiet);
--c-color-fill-normal: var(--c-color-neutral-fill-normal);
diff --git a/packages/craftcms-cp/src/styles/shared/colorable.css b/packages/craftcms-cp/src/styles/shared/colorable.css
index 7e82e67bb61..e703dc37a71 100644
--- a/packages/craftcms-cp/src/styles/shared/colorable.css
+++ b/packages/craftcms-cp/src/styles/shared/colorable.css
@@ -1,4 +1,4 @@
-/* Auto-generated by scripts/generate-colors.ts — do not edit manually */
+/* Auto-generated by scripts/generate-colors.js — do not edit manually */
:root {
/* red */
@@ -215,8 +215,8 @@
--c-color-black-fill-normal: var(--color-gray-900);
--c-color-black-fill-loud: var(--color-gray-900);
--c-color-black-border-quiet: var(--color-gray-800);
- --c-color-black-border-normal: undefined;
- --c-color-black-border-loud: undefined;
+ --c-color-black-border-normal: var(--color-gray-800);
+ --c-color-black-border-loud: var(--color-gray-800);
--c-color-black-on-quiet: var(--color-gray-100);
--c-color-black-on-normal: var(--color-gray-100);
--c-color-black-on-loud: var(--color-gray-100);
@@ -444,24 +444,24 @@
--c-color-black-on-loud: var(--color-gray-300);
}
-.c-colorable,
+.cp-colorable,
[data-color] {
--c-color-fill-quiet: var(--c-color-neutral-fill-quiet);
- --c-color-fill-normal: var(--c-color-neutral-fill-quiet);
- --c-color-fill-loud: var(--c-color-neutral-fill-quiet);
+ --c-color-fill-normal: var(--c-color-neutral-fill-normal);
+ --c-color-fill-loud: var(--c-color-neutral-fill-loud);
--c-color-border-quiet: var(--c-color-neutral-border-quiet);
- --c-color-border-normal: var(--c-color-neutral-border-quiet);
- --c-color-border-loud: var(--c-color-neutral-border-quiet);
+ --c-color-border-normal: var(--c-color-neutral-border-normal);
+ --c-color-border-loud: var(--c-color-neutral-border-loud);
--c-color-on-quiet: var(--c-color-neutral-on-quiet);
- --c-color-on-normal: var(--c-color-neutral-on-quiet);
- --c-color-on-loud: var(--c-color-neutral-on-quiet);
+ --c-color-on-normal: var(--c-color-neutral-on-normal);
+ --c-color-on-loud: var(--c-color-neutral-on-loud);
background-color: var(--c-color-fill-quiet);
border-color: var(--c-color-border-quiet);
color: var(--c-color-on-quiet);
}
-.c-colorable--red,
+.cp-colorable--red,
[data-color='red'] {
--c-color-fill-quiet: var(--c-color-red-fill-quiet);
--c-color-border-quiet: var(--c-color-red-border-quiet);
@@ -473,7 +473,7 @@
--c-color-border-loud: var(--c-color-red-border-loud);
--c-color-on-loud: var(--c-color-red-on-loud);
}
-.c-colorable--orange,
+.cp-colorable--orange,
[data-color='orange'] {
--c-color-fill-quiet: var(--c-color-orange-fill-quiet);
--c-color-border-quiet: var(--c-color-orange-border-quiet);
@@ -485,7 +485,7 @@
--c-color-border-loud: var(--c-color-orange-border-loud);
--c-color-on-loud: var(--c-color-orange-on-loud);
}
-.c-colorable--amber,
+.cp-colorable--amber,
[data-color='amber'] {
--c-color-fill-quiet: var(--c-color-amber-fill-quiet);
--c-color-border-quiet: var(--c-color-amber-border-quiet);
@@ -497,7 +497,7 @@
--c-color-border-loud: var(--c-color-amber-border-loud);
--c-color-on-loud: var(--c-color-amber-on-loud);
}
-.c-colorable--yellow,
+.cp-colorable--yellow,
[data-color='yellow'] {
--c-color-fill-quiet: var(--c-color-yellow-fill-quiet);
--c-color-border-quiet: var(--c-color-yellow-border-quiet);
@@ -509,7 +509,7 @@
--c-color-border-loud: var(--c-color-yellow-border-loud);
--c-color-on-loud: var(--c-color-yellow-on-loud);
}
-.c-colorable--lime,
+.cp-colorable--lime,
[data-color='lime'] {
--c-color-fill-quiet: var(--c-color-lime-fill-quiet);
--c-color-border-quiet: var(--c-color-lime-border-quiet);
@@ -521,7 +521,7 @@
--c-color-border-loud: var(--c-color-lime-border-loud);
--c-color-on-loud: var(--c-color-lime-on-loud);
}
-.c-colorable--green,
+.cp-colorable--green,
[data-color='green'] {
--c-color-fill-quiet: var(--c-color-green-fill-quiet);
--c-color-border-quiet: var(--c-color-green-border-quiet);
@@ -533,7 +533,7 @@
--c-color-border-loud: var(--c-color-green-border-loud);
--c-color-on-loud: var(--c-color-green-on-loud);
}
-.c-colorable--emerald,
+.cp-colorable--emerald,
[data-color='emerald'] {
--c-color-fill-quiet: var(--c-color-emerald-fill-quiet);
--c-color-border-quiet: var(--c-color-emerald-border-quiet);
@@ -545,7 +545,7 @@
--c-color-border-loud: var(--c-color-emerald-border-loud);
--c-color-on-loud: var(--c-color-emerald-on-loud);
}
-.c-colorable--teal,
+.cp-colorable--teal,
[data-color='teal'] {
--c-color-fill-quiet: var(--c-color-teal-fill-quiet);
--c-color-border-quiet: var(--c-color-teal-border-quiet);
@@ -557,7 +557,7 @@
--c-color-border-loud: var(--c-color-teal-border-loud);
--c-color-on-loud: var(--c-color-teal-on-loud);
}
-.c-colorable--cyan,
+.cp-colorable--cyan,
[data-color='cyan'] {
--c-color-fill-quiet: var(--c-color-cyan-fill-quiet);
--c-color-border-quiet: var(--c-color-cyan-border-quiet);
@@ -569,7 +569,7 @@
--c-color-border-loud: var(--c-color-cyan-border-loud);
--c-color-on-loud: var(--c-color-cyan-on-loud);
}
-.c-colorable--sky,
+.cp-colorable--sky,
[data-color='sky'] {
--c-color-fill-quiet: var(--c-color-sky-fill-quiet);
--c-color-border-quiet: var(--c-color-sky-border-quiet);
@@ -581,7 +581,7 @@
--c-color-border-loud: var(--c-color-sky-border-loud);
--c-color-on-loud: var(--c-color-sky-on-loud);
}
-.c-colorable--blue,
+.cp-colorable--blue,
[data-color='blue'] {
--c-color-fill-quiet: var(--c-color-blue-fill-quiet);
--c-color-border-quiet: var(--c-color-blue-border-quiet);
@@ -593,7 +593,7 @@
--c-color-border-loud: var(--c-color-blue-border-loud);
--c-color-on-loud: var(--c-color-blue-on-loud);
}
-.c-colorable--indigo,
+.cp-colorable--indigo,
[data-color='indigo'] {
--c-color-fill-quiet: var(--c-color-indigo-fill-quiet);
--c-color-border-quiet: var(--c-color-indigo-border-quiet);
@@ -605,7 +605,7 @@
--c-color-border-loud: var(--c-color-indigo-border-loud);
--c-color-on-loud: var(--c-color-indigo-on-loud);
}
-.c-colorable--violet,
+.cp-colorable--violet,
[data-color='violet'] {
--c-color-fill-quiet: var(--c-color-violet-fill-quiet);
--c-color-border-quiet: var(--c-color-violet-border-quiet);
@@ -617,7 +617,7 @@
--c-color-border-loud: var(--c-color-violet-border-loud);
--c-color-on-loud: var(--c-color-violet-on-loud);
}
-.c-colorable--purple,
+.cp-colorable--purple,
[data-color='purple'] {
--c-color-fill-quiet: var(--c-color-purple-fill-quiet);
--c-color-border-quiet: var(--c-color-purple-border-quiet);
@@ -629,7 +629,7 @@
--c-color-border-loud: var(--c-color-purple-border-loud);
--c-color-on-loud: var(--c-color-purple-on-loud);
}
-.c-colorable--fuchsia,
+.cp-colorable--fuchsia,
[data-color='fuchsia'] {
--c-color-fill-quiet: var(--c-color-fuchsia-fill-quiet);
--c-color-border-quiet: var(--c-color-fuchsia-border-quiet);
@@ -641,7 +641,7 @@
--c-color-border-loud: var(--c-color-fuchsia-border-loud);
--c-color-on-loud: var(--c-color-fuchsia-on-loud);
}
-.c-colorable--pink,
+.cp-colorable--pink,
[data-color='pink'] {
--c-color-fill-quiet: var(--c-color-pink-fill-quiet);
--c-color-border-quiet: var(--c-color-pink-border-quiet);
@@ -653,7 +653,7 @@
--c-color-border-loud: var(--c-color-pink-border-loud);
--c-color-on-loud: var(--c-color-pink-on-loud);
}
-.c-colorable--rose,
+.cp-colorable--rose,
[data-color='rose'] {
--c-color-fill-quiet: var(--c-color-rose-fill-quiet);
--c-color-border-quiet: var(--c-color-rose-border-quiet);
@@ -665,7 +665,7 @@
--c-color-border-loud: var(--c-color-rose-border-loud);
--c-color-on-loud: var(--c-color-rose-on-loud);
}
-.c-colorable--white,
+.cp-colorable--white,
[data-color='white'] {
--c-color-fill-quiet: var(--c-color-white-fill-quiet);
--c-color-border-quiet: var(--c-color-white-border-quiet);
@@ -677,7 +677,7 @@
--c-color-border-loud: var(--c-color-white-border-loud);
--c-color-on-loud: var(--c-color-white-on-loud);
}
-.c-colorable--gray,
+.cp-colorable--gray,
[data-color='gray'] {
--c-color-fill-quiet: var(--c-color-gray-fill-quiet);
--c-color-border-quiet: var(--c-color-gray-border-quiet);
@@ -689,7 +689,7 @@
--c-color-border-loud: var(--c-color-gray-border-loud);
--c-color-on-loud: var(--c-color-gray-on-loud);
}
-.c-colorable--black,
+.cp-colorable--black,
[data-color='black'] {
--c-color-fill-quiet: var(--c-color-black-fill-quiet);
--c-color-border-quiet: var(--c-color-black-border-quiet);
From b1fd75feea3cc9a572bd954bb790d5f5ecbdd560 Mon Sep 17 00:00:00 2001
From: Brian Hanson
Date: Wed, 1 Apr 2026 15:17:28 -0500
Subject: [PATCH 2/5] Inject `headHtml` and `bodyHtml` when present
---
resources/js/composables/useAppendHtml.ts | 26 +++++++++++++++++++++++
resources/js/layout/AppLayout.vue | 4 ++++
2 files changed, 30 insertions(+)
create mode 100644 resources/js/composables/useAppendHtml.ts
diff --git a/resources/js/composables/useAppendHtml.ts b/resources/js/composables/useAppendHtml.ts
new file mode 100644
index 00000000000..af3799c484f
--- /dev/null
+++ b/resources/js/composables/useAppendHtml.ts
@@ -0,0 +1,26 @@
+import {watch} from 'vue';
+import {
+ appendBodyHtml,
+ appendHeadHtml,
+} from '../../../packages/craftcms-cp/src';
+import {usePage} from '@inertiajs/vue3';
+
+export function useAppendHtml() {
+ const page = usePage<{
+ headHtml?: string;
+ bodyHtml?: string;
+ }>();
+
+ watch(
+ () => [page.props.headHtml, page.props.bodyHtml],
+ async ([headHtml, bodyHtml]) => {
+ if (headHtml) {
+ await appendHeadHtml(headHtml);
+ }
+
+ if (bodyHtml) {
+ await appendBodyHtml(bodyHtml);
+ }
+ }
+ );
+}
diff --git a/resources/js/layout/AppLayout.vue b/resources/js/layout/AppLayout.vue
index 5a0e7b2780f..f83d4927339 100644
--- a/resources/js/layout/AppLayout.vue
+++ b/resources/js/layout/AppLayout.vue
@@ -9,6 +9,7 @@
import Breadcrumbs from '@/components/Breadcrumbs.vue';
import {useAnnouncer} from '@/composables/useAnnouncer';
import LiveRegion from '@/components/LiveRegion.vue';
+ import {useAppendHtml} from '@/composables/useAppendHtml';
withDefaults(
defineProps<{
@@ -38,6 +39,9 @@
watch(successFlash, (newMessage) => announce(newMessage));
watch(errorFlash, (newMessage) => announce(newMessage));
+ // Inject headHtml and bodyHtml when present
+ useAppendHtml();
+
const state = reactive<{
sidebar: {
mode: 'docked' | 'floating';
From 1d3e7db312e779861d1c461a4dbf47701bb05ee2 Mon Sep 17 00:00:00 2001
From: Brian Hanson
Date: Wed, 1 Apr 2026 15:46:52 -0500
Subject: [PATCH 3/5] Rendering working chips
---
.../src/components/chip/chip.styles.ts | 4 +-
.../craftcms-cp/src/styles/shared/base.css | 13 +-
.../js/components/form/ComponentSelect.vue | 217 ++++++++++++
.../js/components/form/EntryTypeSelect.vue | 309 ++----------------
.../js/pages/SettingsSectionsEditPage.vue | 5 +-
.../templates/_includes/disclosuremenu.twig | 33 +-
src/Cp/Html/ElementHtml.php | 76 +++--
src/Entry/Data/EntryType.php | 2 +-
.../Settings/SectionsController.php | 3 +-
src/Shared/Enums/Color.php | 4 +-
yii2-adapter/.stylelintrc.json | 7 +-
.../legacy/web/assets/cp/src/css/_cp.scss | 15 -
.../legacy/web/assets/cp/src/css/craft.scss | 33 +-
13 files changed, 327 insertions(+), 394 deletions(-)
create mode 100644 resources/js/components/form/ComponentSelect.vue
diff --git a/packages/craftcms-cp/src/components/chip/chip.styles.ts b/packages/craftcms-cp/src/components/chip/chip.styles.ts
index 0b23608dd77..7e44dfd6faa 100644
--- a/packages/craftcms-cp/src/components/chip/chip.styles.ts
+++ b/packages/craftcms-cp/src/components/chip/chip.styles.ts
@@ -37,13 +37,13 @@ export default css`
.cp-chip[size='small'],
.cp-chip--small {
- padding-block: 0;
+ padding-block: var(--c-spacing-xs);
min-height: var(--c-size-control-sm);
}
.cp-chip[size='medium'],
.cp-chip--medium {
- padding-block: 0;
+ padding-block: var(--c-spacing-sm);
min-height: var(--c-size-control-md);
}
diff --git a/packages/craftcms-cp/src/styles/shared/base.css b/packages/craftcms-cp/src/styles/shared/base.css
index fea2a124570..fdf8d305b3b 100644
--- a/packages/craftcms-cp/src/styles/shared/base.css
+++ b/packages/craftcms-cp/src/styles/shared/base.css
@@ -40,12 +40,17 @@ ul {
list-style: none;
}
-.code {
- font-size: 0.9em;
+.cp-code {
+ font-size: 0.75em;
+ font-family: var(--c-font-mono);
display: inline-flex;
padding: 0 var(--c-spacing-sm);
- border: 1px solid rgba(0, 0, 0, 0.2);
- background-color: rgba(0, 0, 0, 0.05);
+ color: var(--c-color-on-quiet);
+ border: 1px solid var(--c-color-border-quiet);
+ background-color: color-mix(
+ var(--c-color-fill-loud) 10%,
+ var(--c-color-fill-quiet)
+ );
border-radius: var(--c-radius-sm);
}
diff --git a/resources/js/components/form/ComponentSelect.vue b/resources/js/components/form/ComponentSelect.vue
new file mode 100644
index 00000000000..72324fc6be8
--- /dev/null
+++ b/resources/js/components/form/ComponentSelect.vue
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+ {{ component.name }}
+
+
{{ component.handle }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('Choose') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ type.name }}
+
{{ type.handle }}
+
+
+
+
+
+
+
+
+ {{ t('Create') }}
+
+
+
+
+
+
diff --git a/resources/js/components/form/EntryTypeSelect.vue b/resources/js/components/form/EntryTypeSelect.vue
index 2498ff8fd51..c78560ad24a 100644
--- a/resources/js/components/form/EntryTypeSelect.vue
+++ b/resources/js/components/form/EntryTypeSelect.vue
@@ -1,295 +1,32 @@
-
-
-
-
-
- {{ overrides[entryType.id]?.name ?? entryType.name }}
-
-
{{ overrides[entryType.id]?.handle ?? entryType.handle }}
-
-
-
-
+
+
+
-
-
-
-
-
-
- {{ t('Choose') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ entryType.name }}
-
{{ entryType.handle }}
-
-
-
-
-
-
-
-
- {{ t('Create') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
diff --git a/resources/js/pages/SettingsSectionsEditPage.vue b/resources/js/pages/SettingsSectionsEditPage.vue
index 70e05db0f2b..18f78635a63 100644
--- a/resources/js/pages/SettingsSectionsEditPage.vue
+++ b/resources/js/pages/SettingsSectionsEditPage.vue
@@ -297,10 +297,7 @@
)
}}
-
+
diff --git a/resources/templates/_includes/disclosuremenu.twig b/resources/templates/_includes/disclosuremenu.twig
index 63c9d2fb744..3a4008921ab 100644
--- a/resources/templates/_includes/disclosuremenu.twig
+++ b/resources/templates/_includes/disclosuremenu.twig
@@ -33,8 +33,9 @@
}|filter|keys,
}|merge(item.liAttributes ?? {}) %}
{% set selected = item.selected ?? false %}
- {% tag (type == 'button' ? 'button' : 'a') with {
+ {% tag 'craft-action-item' with {
id: id,
+ icon: item.icon ?? null,
class: {
'menu-item': true,
sel: selected,
@@ -54,15 +55,6 @@
}|filter,
}|merge(item.attributes ?? {}, recursive=true) %}
{%- apply spaceless %}
- {% if item.icon ?? false %}
- {{ tag('span', {
- class: [
- 'icon',
- _self.color(item.color ?? null),
- ]|filter,
- html: iconSvg(item.icon),
- }) }}
- {% endif %}
{% if item.status ?? false -%}
{{ statusIndicator(item.status) }}
{%- endif -%}
@@ -85,7 +77,7 @@
{% endtag %}
{% if type == 'link' %}
{% js %}
- $('#{{ id|namespaceInputId }}').on('keydown', (ev) => {
+ $(document.body).on('keydown', '#{{ id|namespaceInputId }}' (ev) => {
if (ev.keyCode === Garnish.SPACE_KEY) {
ev.currentTarget.click();
}
@@ -93,14 +85,16 @@
{% endjs %}
{% endif %}
{% js %}
- $('#{{ id|namespaceInputId }}').on('activate', () => {
+ $(document.body).on('activate', '#{{ id|namespaceInputId }}', () => {
setTimeout(() => {
+ console.log('{{menuId|namespaceInputId}}');
$('#{{ menuId|namespaceInputId }}').data('disclosureMenu').hide();
}, 1);
});
{% endjs %}
{% endmacro %}
+
{% if withButton %}
{% if html ?? false %}
{{ html|raw }}
@@ -108,16 +102,15 @@
{{ label }}
{% endif %}
- {% tag 'button' with {
- class: ['btn', 'menubtn'],
+ {% tag 'craft-button' with {
type: 'button',
+ size: 'small',
+ icon: true,
+ appearance: 'plain',
+ slot: 'invoker',
aria: {
- controls: id,
label: hiddenLabel ?? null
},
- data: {
- 'disclosure-trigger': true,
- },
disabled: disabled ?? false,
}|merge(buttonAttributes ?? {}, recursive=true) %}
{%- apply spaceless %}
@@ -140,7 +133,8 @@
{% tag 'div' with {
id: id,
- class: (class ?? [])|explodeClass|merge(['menu', 'menu--disclosure']),
+ class: (class ?? [])|explodeClass,
+ slot: 'content',
data: {
'with-search-input': withSearchInput,
},
@@ -203,3 +197,4 @@
{% if ulStarted %}{{ ''|raw }}{% endif %}
{% endblock %}
{% endtag %}
+
diff --git a/src/Cp/Html/ElementHtml.php b/src/Cp/Html/ElementHtml.php
index cf06b529616..acebdb75760 100644
--- a/src/Cp/Html/ElementHtml.php
+++ b/src/Cp/Html/ElementHtml.php
@@ -39,6 +39,8 @@
{
public const string CHIP_SIZE_SMALL = 'small';
+ public const string CHIP_SIZE_MEDIUM = 'medium';
+
public const string CHIP_SIZE_LARGE = 'large';
public function __construct(
@@ -82,17 +84,12 @@ public function chipHtml(Chippable $component, array $config = []): string
$attributes = Arr::merge([
'id' => $config['id'],
+ 'size' => $config['size'],
'class' => [
- 'chip',
- $config['size'],
+ 'cp-colorable',
+ 'cp-colorable--'.$color?->value ?? 'white',
...Html::explodeClass($config['class']),
],
- 'style' => array_filter([
- '--custom-border-color' => $color?->cssVar(200),
- '--custom-bg-color' => $color?->cssVar(50),
- '--custom-text-color' => $color?->cssVar(900),
- '--custom-sel-bg-color' => $color?->cssVar(900),
- ]),
'data' => array_filter([
'type' => $component::class,
'id' => $component->getId(),
@@ -112,34 +109,40 @@ public function chipHtml(Chippable $component, array $config = []): string
]),
], $config['attributes']);
- $html = Html::beginTag('div', $attributes);
+ $html = Html::beginTag('craft-chip', $attributes);
+
+ $prefixHtml = '';
+ if ($config['showThumb'] || $config['selectable'] || $config['showStatus']) {
+ $prefixHtml = Html::beginTag('div', ['slot' => 'prefix']);
+ }
if ($config['showThumb']) {
if ($component instanceof Thumbable) {
$thumbSize = $config['size'] === self::CHIP_SIZE_SMALL ? 30 : 120;
- $html .= $component->getThumbHtml($thumbSize) ?? '';
+ $prefixHtml .= $component->getThumbHtml($thumbSize) ?? '';
} else {
/** @var Chippable&Iconic $component */
$icon = $component->getIcon();
if ($icon || $icon === '0') {
- $html .= Html::tag('div', Icons::svg($icon), [
- 'class' => array_filter(['thumb', 'cp-icon', $color?->value]),
+ $prefixHtml .= Html::tag('craft-icon', '', [
+ 'name' => $icon,
]);
}
}
}
- $html .= Html::beginTag('div', ['class' => 'chip-content']);
-
if ($config['selectable']) {
- $html .= $this->componentCheckboxHtml(sprintf('%s-label', $config['id']));
+ $prefixHtml .= $this->componentCheckboxHtml(sprintf('%s-label', $config['id']));
}
if ($config['showStatus']) {
/** @var Chippable&Statusable $component */
- $html .= $this->statusHtml->componentStatusIndicatorHtml($component) ?? '';
+ $prefixHtml .= $this->statusHtml->componentStatusIndicatorHtml($component) ?? '';
}
+ $prefixHtml .= Html::endTag('div');
+ $html .= $prefixHtml;
+
if (isset($config['labelHtml'])) {
$html .= $config['labelHtml'];
} elseif ($config['showLabel']) {
@@ -161,14 +164,14 @@ public function chipHtml(Chippable $component, array $config = []): string
}
}
- $labelHtml = Html::tag('div', $labelHtml);
+ $labelHtml = Html::tag('div', $labelHtml, ['class' => 'flex gap-1 items-center']);
if ($config['showHandle']) {
/** @var Chippable&Grippable $component */
$handle = $component->getHandle();
if ($handle) {
$labelHtml .= Html::tag('div', Html::encode($handle), [
- 'class' => ['smalltext', 'light', 'code'],
+ 'class' => ['cp-code'],
]);
}
}
@@ -178,15 +181,12 @@ public function chipHtml(Chippable $component, array $config = []): string
if (! empty($indicators)) {
$labelHtml .= Html::beginTag('div', ['class' => 'indicators']).
implode('', array_map(function (array $indicator) {
- $color = $indicator['iconColor'] ?? null;
- if ($color instanceof Color) {
- $color = $color->value;
- }
-
- return Html::tag('div', Icons::svg($indicator['icon']), [
- 'class' => array_filter(['cp-icon', 'puny', $color]),
- 'title' => $indicator['label'],
- 'aria' => ['label' => $indicator['label']],
+ $color = Color::tryFrom($indicator['iconColor']);
+
+ return Html::tag('craft-icon', '', [
+ 'name' => $indicator['icon'],
+ 'style' => $color ? ['color' => $color->cssVar(600)] : null,
+ 'label' => $indicator['label'],
]);
}, $indicators)).
Html::endTag('div');
@@ -194,11 +194,11 @@ public function chipHtml(Chippable $component, array $config = []): string
}
$html .= Html::tag('div', $labelHtml, [
'id' => sprintf('%s-label', $config['id']),
- 'class' => 'chip-label',
+ 'class' => 'grid gap-1 justify-items-start',
]);
}
- $html .= Html::beginTag('div', ['class' => 'chip-actions']);
+ $html .= Html::beginTag('div', ['slot' => 'suffix']);
if ($config['showActionMenu']) {
/** @var Chippable&Actionable $component */
$html .= $this->componentActionMenu($component);
@@ -217,14 +217,16 @@ public function chipHtml(Chippable $component, array $config = []): string
],
]);
}
- $html .= Html::endTag('div'); // .chip-actions
+ $html .= Html::endTag('div'); // slot=suffix
if ($config['inputName'] !== null) {
$inputValue = $config['inputValue'] ?? $component->getId();
$html .= Html::hiddenInput($config['inputName'], (string) $inputValue);
} // .element
- return $html.(Html::endTag('div').Html::endTag('div'));
+ $html .= Html::endTag('craft-chip');
+
+ return $html;
}
public function elementChipHtml(ElementInterface $element, array $config = []): string
@@ -676,15 +678,11 @@ function () use ($component, $withEdit): string {
}
return $this->menuHtml->disclosureMenu($actionMenuItems, [
- 'hiddenLabel' => t('Actions'),
+ 'buttonHtml' => Html::tag('craft-icon', '', ['name' => 'ellipsis', 'label' => t('Actions')]),
'buttonAttributes' => [
- 'class' => array_keys(array_filter([
- 'action-btn' => true,
- 'small' => true,
- 'hidden' => empty($actionMenuItems),
- ])),
- 'removeClass' => 'menubtn',
- 'data' => ['icon' => 'ellipsis'],
+ 'icon' => true,
+ 'size' => 'small',
+ 'appearance' => 'plain',
],
'omitIfEmpty' => false,
]);
diff --git a/src/Entry/Data/EntryType.php b/src/Entry/Data/EntryType.php
index 5abef586586..ef1578377bb 100644
--- a/src/Entry/Data/EntryType.php
+++ b/src/Entry/Data/EntryType.php
@@ -182,7 +182,7 @@ public function getActionMenuItems(): array
]];
HtmlStack::jsWithVars(fn ($id, $params) => << {
+$(document.body).on('click', '#' + $id, () => {
new Craft.CpScreenSlideout('entry-types/edit', {
params: $params,
})
diff --git a/src/Http/Controllers/Settings/SectionsController.php b/src/Http/Controllers/Settings/SectionsController.php
index cdcbf9c5fd4..f6aed56bdf2 100644
--- a/src/Http/Controllers/Settings/SectionsController.php
+++ b/src/Http/Controllers/Settings/SectionsController.php
@@ -11,6 +11,7 @@
use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\Enums\PropagationMethod;
use CraftCms\Cms\Entry\EntryTypes;
+use CraftCms\Cms\Entry\Resources\EntryTypeResource;
use CraftCms\Cms\Http\RespondsWithFlash;
use CraftCms\Cms\Http\Responses\CpScreenResponse;
use CraftCms\Cms\Section\Data\Section as SectionData;
@@ -137,7 +138,7 @@ private function sectionProps(SectionData $section, Sites $sites, bool $brandNew
'section' => SectionResource::make($section),
'homepageUri' => Element::HOMEPAGE_URI,
'brandNew' => $brandNew,
- 'entryTypes' => $this->entryTypes->getAllEntryTypes(),
+ 'entryTypes' => EntryTypeResource::collection($this->entryTypes->getAllEntryTypes()),
'typeOptions' => SectionType::asOptions(),
'propagationOptions' => PropagationMethod::asOptions(),
'placementOptions' => DefaultPlacement::asOptions(),
diff --git a/src/Shared/Enums/Color.php b/src/Shared/Enums/Color.php
index b5cb9ae0f25..1ee3520f111 100644
--- a/src/Shared/Enums/Color.php
+++ b/src/Shared/Enums/Color.php
@@ -55,8 +55,8 @@ public function cssVar(int $shade): string
}
return match ($this) {
- self::White, self::Gray, self::Black => sprintf('var(--%s)', $this->value),
- default => sprintf('var(--%s-%s)', $this->value, str_pad((string) $shade, 3, '0', STR_PAD_LEFT)),
+ self::White, self::Gray, self::Black => sprintf('var(--color-%s)', $this->value),
+ default => sprintf('var(--color-%s-%s)', $this->value, str_pad((string) $shade, 3, '0', STR_PAD_LEFT)),
};
}
}
diff --git a/yii2-adapter/.stylelintrc.json b/yii2-adapter/.stylelintrc.json
index b3c44260f12..aeb14ad2519 100644
--- a/yii2-adapter/.stylelintrc.json
+++ b/yii2-adapter/.stylelintrc.json
@@ -8,12 +8,7 @@
"declaration-empty-line-before": null,
"no-descending-specificity": null,
"no-duplicate-selectors": null,
- "no-invalid-position-at-import-rule": [
- true,
- {
- "ignoreAtRules": ["tailwind", "use"]
- }
- ],
+ "no-invalid-position-at-import-rule": null,
"selector-class-pattern": null,
"liberty/use-logical-spec": [
"always",
diff --git a/yii2-adapter/legacy/web/assets/cp/src/css/_cp.scss b/yii2-adapter/legacy/web/assets/cp/src/css/_cp.scss
index 6041a22c3d0..6141833e550 100644
--- a/yii2-adapter/legacy/web/assets/cp/src/css/_cp.scss
+++ b/yii2-adapter/legacy/web/assets/cp/src/css/_cp.scss
@@ -1577,21 +1577,6 @@ li.breadcrumb-toggle-wrapper {
}
}
-/* grids */
-.grid {
- position: relative;
- min-height: 1px; // Required for Grid.js to run
-
- &::after {
- @include mixins.clearafter;
- }
-
- & > .item {
- display: none;
- box-sizing: border-box;
- }
-}
-
%type-heading-small {
text-transform: uppercase;
color: var(--medium-text-color);
diff --git a/yii2-adapter/legacy/web/assets/cp/src/css/craft.scss b/yii2-adapter/legacy/web/assets/cp/src/css/craft.scss
index 43174a9bbfe..291dcd27706 100644
--- a/yii2-adapter/legacy/web/assets/cp/src/css/craft.scss
+++ b/yii2-adapter/legacy/web/assets/cp/src/css/craft.scss
@@ -4,18 +4,21 @@
// @use 'tokens';
// @use 'variables';
@use 'compat';
-@use 'main';
-@use 'cp';
-@use 'range';
-@use 'global-sidebar';
-@use 'craft-disclosure';
-@use 'craft-spinner';
-@use 'craft-tooltip';
-@use 'preview';
-@use 'login';
-@use 'entry-type-select';
-@use 'fld';
-@use 'grouped-entry-type-select';
-@use 'image_editor';
-@use 'shame';
-@use 'debug_toolbar';
+
+@scope (.cp-legacy-reset, .slideout-container, .menu--disclosure) {
+ @import 'main';
+ @import 'cp';
+ @import 'range';
+ @import 'global-sidebar';
+ @import 'craft-disclosure';
+ @import 'craft-spinner';
+ @import 'craft-tooltip';
+ @import 'preview';
+ @import 'login';
+ @import 'entry-type-select';
+ @import 'fld';
+ @import 'grouped-entry-type-select';
+ @import 'image_editor';
+ @import 'shame';
+ @import 'debug_toolbar';
+}
From bc2bf152f7f85340c23bd61d62d3e682d97fb205 Mon Sep 17 00:00:00 2001
From: Brian Hanson
Date: Thu, 2 Apr 2026 11:53:00 -0500
Subject: [PATCH 4/5] Mostly working ComponentSelect component
---
.../craftcms-cp/scripts/generate-colors.js | 6 +-
.../src/components/button/button.styles.ts | 4 +-
.../src/components/tooltip/tooltip.stories.ts | 4 +-
.../src/components/tooltip/tooltip.ts | 39 +++---
.../craftcms-cp/src/styles/shared/tokens.css | 12 +-
.../{ => ComponentSelect}/ComponentSelect.vue | 0
.../{ => ComponentSelect}/EntryTypeSelect.vue | 20 ++-
.../js/pages/SettingsSectionsEditPage.vue | 14 +-
resources/views/components/icon.blade.php | 3 +
resources/views/components/tooltip.blade.php | 5 +
routes/cp.php | 3 +
src/Cp/Html/ElementHtml.php | 9 +-
src/Entry/Resources/EntryTypeResource.php | 8 +-
.../Settings/SectionsController.php | 12 +-
src/View/Components/Concerns/HasId.php | 22 +++
src/View/Components/Icon.php | 20 +++
src/View/Components/Tooltip.php | 84 +++++++++++
src/View/Components/ViewComponent.php | 131 ++++++++++++++++++
18 files changed, 348 insertions(+), 48 deletions(-)
rename resources/js/components/form/{ => ComponentSelect}/ComponentSelect.vue (100%)
rename resources/js/components/form/{ => ComponentSelect}/EntryTypeSelect.vue (64%)
create mode 100644 resources/views/components/icon.blade.php
create mode 100644 resources/views/components/tooltip.blade.php
create mode 100644 src/View/Components/Concerns/HasId.php
create mode 100644 src/View/Components/Icon.php
create mode 100644 src/View/Components/Tooltip.php
create mode 100644 src/View/Components/ViewComponent.php
diff --git a/packages/craftcms-cp/scripts/generate-colors.js b/packages/craftcms-cp/scripts/generate-colors.js
index 995c709da7c..2f488a554c1 100644
--- a/packages/craftcms-cp/scripts/generate-colors.js
+++ b/packages/craftcms-cp/scripts/generate-colors.js
@@ -55,9 +55,9 @@ function lightScale(color) {
borderQuiet: 'var(--color-gray-800)',
borderNormal: 'var(--color-gray-800)',
borderLoud: 'var(--color-gray-800)',
- onQuiet: 'var(--color-gray-100)',
- onNormal: 'var(--color-gray-100)',
- onLoud: 'var(--color-gray-100)',
+ onQuiet: 'var(--color-gray-50)',
+ onNormal: 'var(--color-gray-50)',
+ onLoud: 'var(--color-gray-50)',
};
default:
return {
diff --git a/packages/craftcms-cp/src/components/button/button.styles.ts b/packages/craftcms-cp/src/components/button/button.styles.ts
index 52ec9d5c69c..7c6995207e4 100644
--- a/packages/craftcms-cp/src/components/button/button.styles.ts
+++ b/packages/craftcms-cp/src/components/button/button.styles.ts
@@ -36,7 +36,7 @@ export default css`
:host(:hover) {
background-color: color-mix(
in oklab,
- var(--c-color-fill-loud, var(--c-button-default-fill)),
+ var(--c-color-fill-loud, var(--c-color-neutral-fill-loud)),
var(--c-color-mix-hover)
);
color: var(--c-color-on-loud);
@@ -123,7 +123,7 @@ export default css`
:host([appearance~='plain']: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);
diff --git a/packages/craftcms-cp/src/components/tooltip/tooltip.stories.ts b/packages/craftcms-cp/src/components/tooltip/tooltip.stories.ts
index 600bfe471f1..556f01f38a8 100644
--- a/packages/craftcms-cp/src/components/tooltip/tooltip.stories.ts
+++ b/packages/craftcms-cp/src/components/tooltip/tooltip.stories.ts
@@ -24,11 +24,11 @@ const meta = {
}
return html`
- ${args.content}${args.content}
Hover me
`;
diff --git a/packages/craftcms-cp/src/components/tooltip/tooltip.ts b/packages/craftcms-cp/src/components/tooltip/tooltip.ts
index c25fe343fd0..ae10108caac 100644
--- a/packages/craftcms-cp/src/components/tooltip/tooltip.ts
+++ b/packages/craftcms-cp/src/components/tooltip/tooltip.ts
@@ -12,37 +12,30 @@ export default class CraftTooltip extends WaTooltip {
return [
WaTooltip.styles,
css`
- wa-popup {
- --wa-z-index-tooltip: var(--c-tooltip-z-index, 1000);
- --wa-tooltip-background-color: var(
- --c-tooltip-fill,
- var(--c-surface-overlay)
- );
- --wa-tooltip-border-color: var(
- --c-tooltip-border,
- var(--c-color-neutral-border-quiet)
- );
- --wa-tooltip-content-color: var(--c-tooltip-text, currentColor);
+ :host {
+ --wa-tooltip-background-color: var(--c-color-black-fill-loud);
+ --wa-tooltip-border-color: var(--c-color-black-border-loud);
+ --wa-tooltip-content-color: var(--c-color-black-on-loud);
--wa-tooltip-padding: var(
--c-tooltip-padding,
calc(4rem / 16) calc(8rem / 16)
);
--wa-tooltip-arrow-size: var(--c-tooltip-arrow-size, 5px);
--wa-tooltip-font-family: inherit;
- --wa-tooltip-font-size: var(
- --c-tooltip-font-size,
- var(--c-text-base)
- );
- --wa-tooltip-font-weight: var(--c-tooltip-font-weight, 400);
- --wa-tooltip-line-height: var(--c-tooltip-line-height, 1.3);
- --wa-tooltip-border-radius: var(
- --c-tooltip-border-radius,
- var(--c-radius-sm)
- );
- font-weight: 400;
- color: var(--c-tooltip-text, currentColor);
+ --wa-tooltip-font-size: var(--c-text-base);
+ --wa-tooltip-font-weight: 400;
+ --wa-tooltip-line-height: 1.3;
+ --wa-tooltip-border-radius: var(--c-radius-sm);
+ }
+
+ &::part(base) {
box-shadow: var(--c-shadow-md);
}
+
+ .body {
+ color: var(--wa-tooltip-content-color);
+ font-weight: var(--wa-tooltip-font-weight);
+ }
`,
];
}
diff --git a/packages/craftcms-cp/src/styles/shared/tokens.css b/packages/craftcms-cp/src/styles/shared/tokens.css
index 39c24cb3697..177e3735436 100644
--- a/packages/craftcms-cp/src/styles/shared/tokens.css
+++ b/packages/craftcms-cp/src/styles/shared/tokens.css
@@ -283,13 +283,19 @@
/**
Web Awesome
*/
+
+ /* Colors */
+ --wa-color-surface-border: var(--c-color-neutral-border-quiet);
+ --wa-color-surface-raised: var(--c-surface-raised);
+
+ /* Shadow */
+ --wa-shadow-l: var(--c-shadow-lg);
+
+ /* Panels */
--wa-panel-border-style: solid;
--wa-panel-border-width: 1px;
- --wa-color-surface-border: var(--c-color-neutral-border-quiet);
--wa-panel-border-color: var(--c-color-neutral-border-quiet);
--wa-panel-border-radius: var(--c-radius-md);
- --wa-color-surface-raised: var(--c-surface-raised);
- --wa-shadow-l: var(--c-shadow-lg);
}
[data-theme='dark'] {
diff --git a/resources/js/components/form/ComponentSelect.vue b/resources/js/components/form/ComponentSelect/ComponentSelect.vue
similarity index 100%
rename from resources/js/components/form/ComponentSelect.vue
rename to resources/js/components/form/ComponentSelect/ComponentSelect.vue
diff --git a/resources/js/components/form/EntryTypeSelect.vue b/resources/js/components/form/ComponentSelect/EntryTypeSelect.vue
similarity index 64%
rename from resources/js/components/form/EntryTypeSelect.vue
rename to resources/js/components/form/ComponentSelect/EntryTypeSelect.vue
index c78560ad24a..6ca2d99a71f 100644
--- a/resources/js/components/form/EntryTypeSelect.vue
+++ b/resources/js/components/form/ComponentSelect/EntryTypeSelect.vue
@@ -2,7 +2,7 @@
import ComponentSelect, {
type ComponentSelectEmits,
type ComponentSelectProps,
- } from '@/components/form/ComponentSelect.vue';
+ } from '@/components/form/ComponentSelect/ComponentSelect.vue';
import DynamicHtmlRenderer from '@/components/DynamicHtmlRenderer.vue';
export interface EntryTypeSelectProps extends ComponentSelectProps {
@@ -11,8 +11,24 @@
const emit = defineEmits();
const props = withDefaults(defineProps(), {
+ modelValue: false,
+ id: () => `element-type-select`,
+ options: () => [],
+ limit: null,
+ showHandles: false,
+ showIndicators: false,
+ showDescription: false,
+ sortable: true,
+ showActionMenus: true,
+ hyperLinks: false,
+ createAction: null,
+ disabled: false,
+ registerJs: true,
+ renderDefaultInput: true,
+ selectable: true,
+
+ // Entry types specific
allowOverrides: false,
- showIndicators: true,
});
diff --git a/resources/js/pages/SettingsSectionsEditPage.vue b/resources/js/pages/SettingsSectionsEditPage.vue
index 18f78635a63..0569be2a319 100644
--- a/resources/js/pages/SettingsSectionsEditPage.vue
+++ b/resources/js/pages/SettingsSectionsEditPage.vue
@@ -11,7 +11,7 @@
import CraftInputHandle from '@craftcms/cp/vue/CraftInputHandle.vue';
import CraftSwitch from '@craftcms/cp/vue/CraftSwitch.vue';
import CraftSelect from '@craftcms/cp/vue/CraftSelect.vue';
- import EntryTypeSelect from '@/components/form/EntryTypeSelect.vue';
+ import EntryTypeSelect from '@/components/form/ComponentSelect/EntryTypeSelect.vue';
import {useInputGenerator} from '@/composables/useInputGenerator';
import type {
SectionResource,
@@ -297,7 +297,17 @@
)
}}
-
+
diff --git a/resources/views/components/icon.blade.php b/resources/views/components/icon.blade.php
new file mode 100644
index 00000000000..02bce69bb3b
--- /dev/null
+++ b/resources/views/components/icon.blade.php
@@ -0,0 +1,3 @@
+merge([
+ 'name' => $name
+])}}>
diff --git a/resources/views/components/tooltip.blade.php b/resources/views/components/tooltip.blade.php
new file mode 100644
index 00000000000..e76632e590b
--- /dev/null
+++ b/resources/views/components/tooltip.blade.php
@@ -0,0 +1,5 @@
+
+ {!! $getContent() !!}
+
+
+{!! $getButton() !!}
diff --git a/routes/cp.php b/routes/cp.php
index 8bd2e69fa5b..8a8cc759168 100644
--- a/routes/cp.php
+++ b/routes/cp.php
@@ -33,6 +33,7 @@
use CraftCms\Cms\Http\Controllers\Settings\UserGroupsController;
use CraftCms\Cms\Http\Controllers\Settings\UserSettingsController;
use CraftCms\Cms\Http\Controllers\Settings\VolumesController;
+use CraftCms\Cms\Http\Controllers\UiController;
use CraftCms\Cms\Http\Controllers\Updates\UpdaterController;
use CraftCms\Cms\Http\Controllers\Users\AddressesController;
use CraftCms\Cms\Http\Controllers\Users\PasskeysController;
@@ -254,4 +255,6 @@
});
Route::post('updates', [UpdaterController::class, 'index']);
+
+ Route::get('ui/{type}/{id}/{component}', [UiController::class, 'render']);
});
diff --git a/src/Cp/Html/ElementHtml.php b/src/Cp/Html/ElementHtml.php
index acebdb75760..f5a4a19ef11 100644
--- a/src/Cp/Html/ElementHtml.php
+++ b/src/Cp/Html/ElementHtml.php
@@ -28,6 +28,7 @@
use CraftCms\Cms\Support\Facades\HtmlStack;
use CraftCms\Cms\Support\Facades\InputNamespace;
use CraftCms\Cms\Support\Html;
+use CraftCms\Cms\View\Components\Tooltip;
use Illuminate\Container\Attributes\Singleton;
use Illuminate\Support\Facades\Auth;
use yii\base\InvalidConfigException;
@@ -158,9 +159,10 @@ public function chipHtml(Chippable $component, array $config = []): string
/** @var Chippable&Describable $component */
$description = $component->getDescription();
if ($description) {
- $labelHtml .= Html::tag('span',
- $this->contentHtml->parseMarkdown(Html::encode($description)),
- ['class' => 'info']);
+ $labelHtml .= Tooltip::make()
+ ->id('description-tooltip-'.$config['id'])
+ ->content($description)
+ ->toHtml();
}
}
@@ -683,6 +685,7 @@ function () use ($component, $withEdit): string {
'icon' => true,
'size' => 'small',
'appearance' => 'plain',
+ 'variant' => 'inherit',
],
'omitIfEmpty' => false,
]);
diff --git a/src/Entry/Resources/EntryTypeResource.php b/src/Entry/Resources/EntryTypeResource.php
index e68b7fd93b3..b34ebcad7f4 100644
--- a/src/Entry/Resources/EntryTypeResource.php
+++ b/src/Entry/Resources/EntryTypeResource.php
@@ -18,13 +18,7 @@ public function toArray(Request $request): array
$elementHtml = app(ElementHtml::class);
return parent::toArray($request) + [
- 'chipHtml' => $elementHtml->chipHtml($this->resource, [
- 'showHandle' => true,
- // 'checkbox' => $this->resource->selectable,
- 'showActionMenu' => true,
- 'showIndicators' => true,
- 'showDescription' => true,
- ]),
+ 'chipHtml' => $elementHtml->chipHtml($this->resource, $request->chipConfig),
];
}
}
diff --git a/src/Http/Controllers/Settings/SectionsController.php b/src/Http/Controllers/Settings/SectionsController.php
index f6aed56bdf2..4cb213e02a8 100644
--- a/src/Http/Controllers/Settings/SectionsController.php
+++ b/src/Http/Controllers/Settings/SectionsController.php
@@ -102,11 +102,21 @@ public function create(Sites $sites): CpScreenResponse
->inertiaPage('SettingsSectionsEditPage', $this->sectionProps($section, $sites, brandNew: true));
}
- public function edit(Sections $sections, Sites $sites, SectionModel $section): CpScreenResponse
+ public function edit(Request $request, Sections $sections, Sites $sites, SectionModel $section): CpScreenResponse
{
$sectionData = $sections->getSectionById($section->id);
abort_if(is_null($sectionData), 404, 'Section not found');
+ // Configure our entry type chips
+ $request->merge([
+ 'chipConfig' => [
+ 'showHandle' => true,
+ 'showActionMenu' => true,
+ 'showIndicators' => true,
+ 'showDescription' => true,
+ ],
+ ]);
+
return new CpScreenResponse()
->title(trim($sectionData->name) ?: t('Edit Section'))
->addCrumb(t('Settings'), 'settings')
diff --git a/src/View/Components/Concerns/HasId.php b/src/View/Components/Concerns/HasId.php
new file mode 100644
index 00000000000..fa7d71976d2
--- /dev/null
+++ b/src/View/Components/Concerns/HasId.php
@@ -0,0 +1,22 @@
+id = $id;
+
+ return $this;
+ }
+
+ public function getId(): string
+ {
+ return $this->id;
+ }
+}
diff --git a/src/View/Components/Icon.php b/src/View/Components/Icon.php
new file mode 100644
index 00000000000..66b12c0a117
--- /dev/null
+++ b/src/View/Components/Icon.php
@@ -0,0 +1,20 @@
+name = $name;
+
+ return $this;
+ }
+}
diff --git a/src/View/Components/Tooltip.php b/src/View/Components/Tooltip.php
new file mode 100644
index 00000000000..c2c1a9aab2c
--- /dev/null
+++ b/src/View/Components/Tooltip.php
@@ -0,0 +1,84 @@
+placement = $value;
+
+ return $this;
+ }
+
+ public function button(?string $value = null): static
+ {
+ $this->button = $value;
+
+ return $this;
+ }
+
+ public function content(?string $value = null): static
+ {
+ $this->content = $value;
+
+ return $this;
+ }
+
+ public function getContent(): string
+ {
+ return app(ContentHtml::class)->parseMarkdown(Html::encode($this->content));
+ }
+
+ private function getDefaultIcon(): string
+ {
+ return Html::tag('craft-icon', '', [
+ 'name' => 'circle-info',
+ ]);
+ }
+
+ public function getButton(): string
+ {
+ if ($this->button) {
+ return $this->button;
+ }
+
+ return Html::tag('craft-button', $this->getDefaultIcon(), [
+ 'id' => $this->getId(),
+ 'appearance' => 'plain',
+ 'variant' => 'inherit',
+ 'icon' => true,
+ 'size' => 'zero',
+ ]);
+ }
+
+ public static function make(array $config = []): static
+ {
+ return app(static::class, $config);
+ }
+}
diff --git a/src/View/Components/ViewComponent.php b/src/View/Components/ViewComponent.php
new file mode 100644
index 00000000000..b086e07945d
--- /dev/null
+++ b/src/View/Components/ViewComponent.php
@@ -0,0 +1,131 @@
+viewData[] = $data;
+
+ return $this;
+ }
+
+ /**
+ * @template T
+ *
+ * @param T | callable(): T $value
+ * @return T
+ */
+ public function evaluate(mixed $value): mixed
+ {
+ if (! $value instanceof Closure) {
+ return $value;
+ }
+
+ return $value();
+ }
+
+ protected function extractPublicMethods(): array
+ {
+ $reflection = new ReflectionClass($this);
+
+ $methods = [];
+
+ foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
+ $methods[$method->getName()] = Closure::fromCallable([$this, $method->getName()]);
+ }
+
+ return $methods;
+ }
+
+ protected function extractPublicProperties(): array
+ {
+ $reflection = new ReflectionClass($this);
+
+ $properties = [];
+
+ foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
+ if (! $property->isStatic()) {
+ $properties[$property->getName()] = $property->getValue($this);
+ }
+ }
+
+ return $properties;
+ }
+
+ public function getViewData(): array
+ {
+ return Arr::mapWithKeys(
+ $this->viewData,
+ fn (mixed $data): array => $this->evaluate($data) ?? [],
+ );
+ }
+
+ /**
+ * Set the view to be rendered.
+ */
+ public function view(?string $view, array|Closure $viewData = []): static
+ {
+ if ($view === null) {
+ return $this;
+ }
+
+ $this->view = $view;
+
+ if (filled($viewData)) {
+ $this->viewData($viewData);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getExtraViewData(): array
+ {
+ return [];
+ }
+
+ public function getView(): string
+ {
+ if (isset($this->view)) {
+ return $this->view;
+ }
+
+ throw new Exception('Class ['.static::class.'] extends ['.ViewComponent::class.'] but does not have a [$view] property defined.');
+ }
+
+ public function render(): View
+ {
+ return view($this->getView(), [
+ ...$this->extractPublicMethods(),
+ ...$this->extractPublicProperties(),
+ 'attributes' => new ComponentAttributeBag,
+ ...$this->getExtraViewData(),
+ ...$this->getViewData(),
+ ]);
+ }
+
+ public function toHtml(): string
+ {
+ return $this->render()->render();
+ }
+}
From 45e024453dbb870d0de01dc2f272d0ace7672e8f Mon Sep 17 00:00:00 2001
From: Brian Hanson
Date: Thu, 2 Apr 2026 12:15:46 -0500
Subject: [PATCH 5/5] Remove icon component
---
src/View/Components/Icon.php | 20 --------------------
1 file changed, 20 deletions(-)
delete mode 100644 src/View/Components/Icon.php
diff --git a/src/View/Components/Icon.php b/src/View/Components/Icon.php
deleted file mode 100644
index 66b12c0a117..00000000000
--- a/src/View/Components/Icon.php
+++ /dev/null
@@ -1,20 +0,0 @@
-name = $name;
-
- return $this;
- }
-}