diff --git a/packages/craftcms-cp/src/components/input/input.ts b/packages/craftcms-cp/src/components/input/input.ts index 7bafe449ff5..925e4fc50ac 100644 --- a/packages/craftcms-cp/src/components/input/input.ts +++ b/packages/craftcms-cp/src/components/input/input.ts @@ -14,6 +14,8 @@ export default class CraftInput extends LionInput { @property({reflect: true, type: Boolean}) small = false; @property({reflect: true, type: Boolean}) center = false; + @property({reflect: true, type: Boolean}) monospace = false; + override connectedCallback() { super.connectedCallback(); diff --git a/packages/craftcms-cp/src/styles/form.styles.ts b/packages/craftcms-cp/src/styles/form.styles.ts index eeba5d58567..a53d927560e 100644 --- a/packages/craftcms-cp/src/styles/form.styles.ts +++ b/packages/craftcms-cp/src/styles/form.styles.ts @@ -62,6 +62,11 @@ export const baseFieldStyles = css` export const inputStyles = css` ${baseFieldStyles} + :host([monospace]) .input-group__container { + font-family: var(--c-font-mono); + font-size: 0.9em; + } + ::slotted([slot='input']) { font: inherit; padding-block: 0; diff --git a/packages/craftcms-cp/src/styles/shared/tokens.css b/packages/craftcms-cp/src/styles/shared/tokens.css index 8338d1a75dd..f8db68dbb85 100644 --- a/packages/craftcms-cp/src/styles/shared/tokens.css +++ b/packages/craftcms-cp/src/styles/shared/tokens.css @@ -54,6 +54,7 @@ --c-spacing-2xl: calc(var(--c-spacing) * 16); --c-size-touch-target: calc(34rem / 16); + --c-size-touch-target-sm: calc(24rem / 16); --c-size-icon-xs: calc(10rem / 16); --c-size-icon-sm: calc(12rem / 16); diff --git a/packages/craftcms-cp/tailwind.css b/packages/craftcms-cp/tailwind.css index fd95de39987..66436a6c699 100644 --- a/packages/craftcms-cp/tailwind.css +++ b/packages/craftcms-cp/tailwind.css @@ -110,4 +110,20 @@ --color-border-default: var(--c-color-neutral-border-normal); --color-border-strong: var(--c-color-neutral-border-loud); --color-border-form: var(--c-form-control-border-color); + + /* ——— Fill utilities ——— */ + --color-fill-quiet: var( + --c-color-fill-quiet, + var(--c-color-neutral-fill-quiet) + ); + --color-fill-normal: var( + --c-color-fill-normal, + var(--c-color-neutral-fill-normal) + ); + --color-fill-loud: var(--c-color-fill-loud, var(--c-color-neutral-fill-loud)); + + /* ——— Text utilities ——— */ + --color-on-quiet: var(--c-color-on-quiet, var(--c-color-neutral-on-quiet)); + --color-on-normal: var(--c-color-on-normal, var(--c-color-neutral-on-normal)); + --color-on-loud: var(--c-color-on-loud, var(--c-color-neutral-on-loud)); } diff --git a/packages/craftcms-legacy/cp/src/css/_fld.scss b/packages/craftcms-legacy/cp/src/css/_fld.scss deleted file mode 100644 index 28b5b699e06..00000000000 --- a/packages/craftcms-legacy/cp/src/css/_fld.scss +++ /dev/null @@ -1,469 +0,0 @@ -@charset "UTF-8"; -@use 'sass:color'; -@use '@craftcms/sass/mixins'; - -$base: 24px; -$tabPadding: 14px; -$tabWidth: $base * 12; -$gridColor: var(--gray-100); - -@mixin workspace-bg { - background-color: var(--gray-050); - background-image: - linear-gradient(to right, $gridColor 1px, transparent 0), - linear-gradient(to bottom, $gridColor 1px, transparent 1px); - background-size: $base $base; -} - -.layoutdesigner { - container-type: inline-size; -} - -.fld-container { - display: flex; - align-items: stretch; - position: relative; - @include mixins.input-styles; - overflow: hidden; - box-shadow: none; - min-height: 500px; - - .errors > .layoutdesigner > & { - border: 1px solid var(--fg-error) !important; - } - - .fld-workspace { - flex: 1; - max-width: 100%; - border-start-start-radius: calc(var(--radius-sm) - 1px); - border-start-end-radius: 0; - border-end-end-radius: 0; - border-end-start-radius: calc(var(--radius-sm) - 1px); - padding-inline: $base 0; - padding-block: $base; - @include workspace-bg; - background-position: -1px -1px; - box-shadow: inset 0 1px 3px -1px - color.adjust(mixins.$grey200, $lightness: -10%); - - .fld-tabs { - display: flex; - align-items: flex-start; - flex-wrap: wrap; - } - } - - .fld-library { - display: none; - } -} - -.fld-library-hud { - width: $tabWidth - 1; - max-width: 100%; - - .main { - height: 100%; - padding: $tabPadding; - } -} - -.fld-new-tab-btn:active { - background-color: var(--gray-050); -} - -.fld-library { - display: flex; - flex-direction: column; - height: 100%; - - .btngroup { - margin-block-end: $tabPadding; - } - - .fld-field-library, - .fld-ui-library { - margin-block: -3px; - margin-inline: #{-$tabPadding}; - padding-block: 3px; - padding-inline: #{$tabPadding}; - flex: 1; - min-height: 0; - max-height: 550px; - overflow: auto; - } - - .fld-field-library { - .fld-field-group { - margin-block-start: $tabPadding; - - & > *:not(:first-child) { - margin-block-start: var(--s); - } - } - - .fld-field-indicators { - display: none; - } - } - - .fld-ui-library > *:not(:first-child) { - margin-block-start: var(--s); - } - - .filtered { - display: none !important; - } -} - -.layoutdesigner .fld-library, -.fld-tab .tabs .tab, -.fld-tab .fld-tabcontent { - background-color: var(--white); - box-shadow: - 0 0 0 1px color.adjust(mixins.$grey900, $alpha: -0.9), - 0 2px 5px -2px color.adjust(mixins.$grey900, $alpha: -0.8); -} - -.fld-new-tab-btn { - background: var(--white) !important; - box-shadow: - 0 0 0 1px #{color.adjust(mixins.$grey900, $alpha: -0.9)}, - 0 2px 5px -2px #{color.adjust(mixins.$grey900, $alpha: -0.8)}; -} - -.fld-tab .settings::before, -.fld-element .settings::before { - margin-block-start: -2px; - font-size: 16px; - opacity: 0.5; -} - -.fld-tab .settings:hover::before, -.fld-tab .settings.active::before, -.fld-element .settings:hover::before, -.fld-element .settings.active::before { - opacity: 1; -} - -.fld-tab { - width: $tabWidth + $base; - max-width: 100%; - padding-inline: 0 $base + 1; - padding-block: 0 $base; - box-sizing: border-box; - - .tabs { - margin-block: -10px 0; - margin-inline: -12px; - padding-block: 10px 0; - padding-inline: 12px; - overflow: hidden; - display: flex; - - .tab { - display: flex; - align-items: center; - gap: var(--xs); - max-width: calc(100% - 10px); - box-sizing: border-box; - padding-block: 8px; - padding-inline: $tabPadding; - border-radius: var(--radius-md) var(--radius-md) 0 0; - - body:not(.dragging) &.draggable { - cursor: move; - cursor: grab; - } - } - } - - .fld-tab__name { - margin-block: 0; - font-weight: var(--font-weight-regular); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .fld-tabcontent { - padding: $tabPadding; - border-start-start-radius: 0; - border-start-end-radius: var(--radius-md); - border-end-end-radius: var(--radius-md); - border-end-start-radius: var(--radius-md); - - & > .fld-element, - & > .fld-add-btn { - &:not(:first-child) { - margin-block-start: var(--s); - } - } - } - - &.fld-insertion { - .tabs .tab, - .fld-tabcontent { - margin: -2px; - border: 2px dashed var(--border-hairline); - box-shadow: none; - @include workspace-bg; - } - - .tabs .tab { - background-position: -1px -1px; - } - - .fld-tabcontent { - background-position: -1px -13px; - } - } -} - -.fld-tab-caboose { - min-height: 24px; -} - -.fld-element { - position: relative; - display: flex; - align-items: center; - padding: var(--s); - gap: var(--s); - box-shadow: inset 0 0 0 1px var(--border-hairline); - border-radius: var(--radius-md); - background-color: var(--white); - - body:not(.dragging) & { - cursor: move; - cursor: grab; - } - - &.fld-insertion { - box-sizing: border-box; - border: 2px dashed var(--border-hairline); - border-radius: var(--radius-md); - background: none; - box-shadow: none; - } - - &.draghelper { - @include mixins.shadow; - } - - &.fld-field { - color: var(--fg-subtle); - background-color: var(--gray-100); - - &:not(.draghelper, :focus) { - box-shadow: none; - } - - .field-name { - display: flex; - flex-direction: column; - gap: var(--xs); - } - } - - .fld-element-icon { - text-align: center; - - &, - svg { - width: 16px; - height: 16px; - } - - svg { - @include mixins.svg-mask(var(--ui-control-color)); - } - } - - .field-name { - flex: 1; - overflow: hidden; - - .fld-element-label, - .fld-attribute { - flex: 1; - display: flex; - align-items: center; - gap: var(--xs); - } - - .fld-element-label h4, - .fld-attribute .smalltext { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .fld-element-label h4 { - font-weight: normal; - color: var(--text-color); - margin: 0; - } - } -} - -.fld-hr, -.fld-br { - position: relative; - flex: 1; - display: flex; - justify-content: center; - - &::before { - position: absolute; - display: block; - inset-block-start: calc(50% - 2px); - inset-inline-start: 0; - width: 100%; - height: 4px; - content: ''; - font-size: 0; - border-radius: 2px; - } - - .smalltext { - position: relative; - display: flex; - justify-content: center; - align-items: center; - background-color: var(--gray-100); - border-radius: var(--radius-lg); - padding-block: 0; - padding-inline: var(--s); - height: var(--touch-target-size); - } -} - -.fld-hr::before { - background-color: var(--gray-100); -} - -.fld-br::before { - background-image: repeating-linear-gradient( - to right, - var(--gray-100), - var(--gray-100) calc(100% / 19), - transparent calc(100% / 19), - transparent calc(100% / 9.5), - var(--gray-100) calc(100% / 9.5) - ); -} - -.fld-element-settings-body { - flex: 1; - margin-block: -24px 0; - margin-inline: var(--neg-padding); - padding-block: 24px; - padding-inline: var(--padding); - overflow: hidden auto; - position: relative; - - hr { - margin-inline: -24px; - } -} - -.fld-element-settings-footer { - position: relative; - display: flex; - flex-direction: row; - margin-block: 0 -24px; - margin-inline: var(--neg-padding); - padding-block: 5px; - padding-inline: var(--padding); - @include mixins.pane; - background-color: var(--gray-050); - z-index: 3; - - & > .ee-site-select { - flex: 1; - } - - & > .btn { - margin-inline-start: 5px; - } - - & > .spinner { - margin-inline: 0 var(--neg-padding); - margin-block: 0; - } -} - -// thumbnail management -.thumb-management { - margin-block: var(--xl); - - .field:first-child { - margin-inline-end: var(--xl); - } -} - -// card view designer -.card-view-designer { - container: cvd / inline-size; -} - -.cvd-container { - display: grid; - position: relative; - overflow: hidden; - box-shadow: none; - gap: var(--xl); -} - -@container cvd (width > 37.5rem) { - .cvd-container { - grid-template-columns: 1fr 2fr; - } -} - -.cvd-library { - .draggable { - display: flex; - } -} - -.cvd-preview-container { - padding: var(--xl); - border: 1px solid #{color.adjust(mixins.$inputColor, $alpha: -0.75)} !important; - border-radius: var(--radius-sm); - display: grid; - height: 100%; - align-items: center; -} - -.cvd-preview { - &:not(.loading) .spinner { - display: none; - } -} - -.cvd-thumbnail { - --icon-size: 2rem; - --icon-color: var(--gray-300); - width: 100%; - aspect-ratio: 4/3; - display: flex; - justify-content: center; - align-items: center; - background-color: var(--gray-150); - border-radius: var(--radius-md); -} - -.card-placeholder { - display: inline-block; - border: 1px dashed mixins.$grey300; - border-radius: var(--radius-sm); - padding-block: 0.1em; - padding-inline: 0.5em; -} - -.field.cvd-field { - margin-block-start: 0.2em !important; - margin-inline-start: 0.5em; -} diff --git a/packages/craftcms-legacy/cp/src/css/craft.scss b/packages/craftcms-legacy/cp/src/css/craft.scss index ff419da226b..30fb155eeac 100644 --- a/packages/craftcms-legacy/cp/src/css/craft.scss +++ b/packages/craftcms-legacy/cp/src/css/craft.scss @@ -17,7 +17,6 @@ @import 'craft-tooltip'; @import 'preview'; @import 'entry-type-select'; - @import 'fld'; @import 'grouped-entry-type-select'; @import 'image_editor'; @import 'shame'; diff --git a/packages/craftcms-legacy/cp/src/js/FieldLayoutDesigner.js b/packages/craftcms-legacy/cp/src/js/FieldLayoutDesigner.js index cc9a7f4f81e..7f13ae4aa25 100644 --- a/packages/craftcms-legacy/cp/src/js/FieldLayoutDesigner.js +++ b/packages/craftcms-legacy/cp/src/js/FieldLayoutDesigner.js @@ -46,7 +46,7 @@ Craft.FieldLayoutDesigner = Garnish.Base.extend( this.$innerContainer = this.$container.children('.fld-container'); const $workspace = this.$innerContainer.children('.fld-workspace'); this.$tabContainer = $workspace.children('.fld-tabs'); - this.$newTabBtn = $workspace.children('.fld-new-tab-btn'); + this.$newTabBtn = $workspace.find('[command="--add-tab"]'); this.$libraryContainer = this.$innerContainer.children('.fld-library'); this.$fieldLibrary = this.$selectedLibrary = @@ -245,9 +245,16 @@ Craft.FieldLayoutDesigner = Garnish.Base.extend(
- +
`); @@ -500,7 +507,7 @@ Craft.FieldLayoutDesigner.Tab = Garnish.Base.extend({ this.designer.tabGrid.refreshCols(true); }); - this.$addBtn = $tabContent.children('.fld-add-btn'); + this.$addBtn = $tabContent.find('[command="--add-field"]'); const hud = new Garnish.HUD(this.$addBtn, { hudClass: 'hud fld-library-hud cp-legacy', @@ -523,7 +530,9 @@ Craft.FieldLayoutDesigner.Tab = Garnish.Base.extend({ hud.show(); }); - const $elements = $tabContent.children().not(this.$addBtn); + // Match the drag system's item selector (ElementDrag.findItems) so every + // draggable element gets initialized, regardless of how it's nested. + const $elements = $tabContent.find('.fld-element'); for (let i = 0; i < $elements.length; i++) { this.initElement($($elements[i])); @@ -533,16 +542,23 @@ Craft.FieldLayoutDesigner.Tab = Garnish.Base.extend({ createMenu: function () { const $tab = this.$container.find('.tabs .tab'); const menuId = `actionmenu${Math.floor(Math.random() * 1000000)}`; - const $btn = $(' + + + @endif + +
+ + + {!! $designer->fldFieldSelectorsHtml(\CraftCms\Cms\t('Native Fields'), $availableNativeFields, $fieldLayout) !!} + + @foreach ($availableCustomFields as $groupName => $groupFields) + {!! $designer->fldFieldSelectorsHtml($groupName, $groupFields, $fieldLayout) !!} + @endforeach +
+ + @if ($customizableUi) + + @endif + + + diff --git a/resources/views/forms/fld/tab.blade.php b/resources/views/forms/fld/tab.blade.php new file mode 100644 index 00000000000..a79d147f0d6 --- /dev/null +++ b/resources/views/forms/fld/tab.blade.php @@ -0,0 +1,33 @@ +@php + /** + * A single tab within the field layout designer. + * + * @var \CraftCms\Cms\Cp\FieldLayoutDesigner\FieldLayoutDesigner $designer + * @var \CraftCms\Cms\FieldLayout\FieldLayoutTab $tab + * @var bool $customizable + * @var bool $disabled + */ +@endphp +
+
+
$customizable])> + {!! $tab->labelHtml() !!} +
+
+
+ @foreach ($tab->getElements() as $element) + {!! $designer->layoutElementSelectorHtml($element) !!} + @endforeach + + + {{ \CraftCms\Cms\t('Add') }} + +
+
diff --git a/src/Cp/FieldLayoutDesigner/FieldLayoutDesigner.php b/src/Cp/FieldLayoutDesigner/FieldLayoutDesigner.php index 6e49106e878..9943e14613c 100644 --- a/src/Cp/FieldLayoutDesigner/FieldLayoutDesigner.php +++ b/src/Cp/FieldLayoutDesigner/FieldLayoutDesigner.php @@ -87,20 +87,29 @@ public function html(FieldLayout $fieldLayout, array $config = []): string $tab->setElements($layoutElements); } - $jsSettings = JsonHelper::encode([ + $settings = [ 'elementType' => $fieldLayout->type, 'customizableTabs' => $config['customizableTabs'], 'customizableUi' => $config['customizableUi'], 'withCardViewDesigner' => $config['withCardViewDesigner'] ?? false, 'alwaysShowThumbAlignmentBtns' => $fieldLayout->type::hasThumbs(), 'readOnly' => $config['disabled'], - ]); - $namespacedId = InputNamespace::namespaceId($config['id']); + ]; - $js = <<getAvailableCustomFields(); $availableNativeFields = $fieldLayout->getAvailableNativeFields(); @@ -139,81 +148,21 @@ public function html(FieldLayout $fieldLayout, array $config = []): string $fieldLayoutConfig['type'] = $fieldLayout->type; } - return - Html::beginTag('div', [ - 'id' => $config['id'], - 'class' => 'layoutdesigner', - ]). - Html::hiddenInput('fieldLayout', JsonHelper::encode($fieldLayoutConfig), [ - 'data' => ['config-input' => true], - ]). - Html::beginTag('div', ['class' => 'fld-container']). - Html::beginTag('div', ['class' => 'fld-workspace']). - Html::beginTag('div', ['class' => 'fld-tabs']). - implode('', array_map( - fn (FieldLayoutTab $tab) => $this->fldTabHtml($tab, $config['customizableTabs'], $config['disabled']), - $tabs, - )). - Html::endTag('div'). // .fld-tabs - ($config['customizableTabs'] - ? Html::button(t('New Tab'), [ - 'type' => 'button', - 'class' => ['fld-new-tab-btn', 'btn', 'add', 'icon'], - 'disabled' => $config['disabled'], - ]) - : ''). - Html::endTag('div'). // .fld-workspace - Html::beginTag('div', ['class' => 'fld-library']). - ($config['customizableUi'] - ? Html::beginTag('section', [ - 'class' => ['btngroup', 'btngroup--exclusive', 'small', 'fullwidth'], - 'aria' => ['label' => t('Layout element types')], - ]). - Html::button(t('Fields'), [ - 'type' => 'button', - 'class' => ['btn', 'small', 'active'], - 'aria' => ['pressed' => 'true'], - 'data' => ['library' => 'field'], - 'disabled' => $config['disabled'], - ]). - Html::button(t('UI Elements'), [ - 'type' => 'button', - 'class' => ['btn', 'small'], - 'aria' => ['pressed' => 'false'], - 'data' => ['library' => 'ui'], - 'disabled' => $config['disabled'], - ]). - Html::endTag('section') // .btngroup - : ''). - Html::beginTag('div', ['class' => 'fld-field-library']). - Html::beginTag('div', ['class' => ['texticon', 'search', 'icon', 'clearable']]). - FormFields::textHtml([ - 'class' => 'fullwidth', - 'inputmode' => 'search', - 'placeholder' => t('Search'), - 'disabled' => $config['disabled'], - ]). - Html::tag('div', '', [ - 'class' => ['clear-btn', 'hidden'], - 'title' => t('Clear'), - 'aria' => ['label' => t('Clear')], - ]). - Html::endTag('div'). // .texticon - $this->fldFieldSelectorsHtml(t('Native Fields'), $availableNativeFields, $fieldLayout). - implode('', array_map(fn (string $groupName) => $this->fldFieldSelectorsHtml($groupName, $availableCustomFields[$groupName], $fieldLayout), array_keys($availableCustomFields))). - Html::endTag('div'). // .fld-field-library - ($config['customizableUi'] - ? Html::beginTag('div', ['class' => ['fld-ui-library', 'hidden']]). - implode('', array_map(fn (FieldLayoutElement $element) => $this->layoutElementSelectorHtml($element, true, [ - 'class' => array_filter([ - ! $this->showFldUiElementSelector($fieldLayout, $element) ? 'hidden' : null, - ]), - ]), $availableUiElements)). - Html::endTag('div') // .fld-ui-library - : ''). - Html::endTag('div'). // .fld-library - Html::endTag('div'). // .fld-container - Html::endTag('div'); // .layoutdesigner + return view('c::forms.fld.designer', [ + 'designer' => $this, + 'id' => $config['id'], + 'autoBoot' => $autoBoot, + 'settings' => $settings, + 'fieldLayoutConfig' => $fieldLayoutConfig, + 'fieldLayout' => $fieldLayout, + 'tabs' => $tabs, + 'customizableTabs' => $config['customizableTabs'], + 'customizableUi' => $config['customizableUi'], + 'disabled' => $config['disabled'], + 'availableNativeFields' => $availableNativeFields, + 'availableCustomFields' => $availableCustomFields, + 'availableUiElements' => $availableUiElements, + ])->render(); } public function layoutElementSelectorHtml( @@ -341,38 +290,20 @@ private function setLayoutOnElements(array $elements, FieldLayout $fieldLayout): } } - private function fldTabHtml(FieldLayoutTab $tab, bool $customizable, bool $disabled): string + public function fldTabHtml(FieldLayoutTab $tab, bool $customizable, bool $disabled): string { - return - Html::beginTag('div', [ - 'class' => 'fld-tab', - 'data' => [ - 'uid' => $tab->uid, - ], - ]). - Html::beginTag('div', ['class' => 'tabs']). - Html::tag('div', $tab->labelHtml(), [ - 'class' => array_filter([ - 'tab', - 'sel', - $customizable ? 'draggable' : null, - ]), - ]). - Html::endTag('div'). // .tabs - Html::beginTag('div', ['class' => 'fld-tabcontent']). - implode('', array_map(fn (FieldLayoutElement $element) => $this->layoutElementSelectorHtml($element, false), $tab->getElements())). - Html::button(t('Add'), [ - 'class' => ['btn', 'add', 'icon', 'dashed', 'fullwidth', 'fld-add-btn'], - 'disabled' => $disabled, - ]). - Html::endTag('div'). // .fld-tabcontent - Html::endTag('div'); // .fld-tab + return view('c::forms.fld.tab', [ + 'designer' => $this, + 'tab' => $tab, + 'customizable' => $customizable, + 'disabled' => $disabled, + ])->render(); } /** * @param BaseField[] $groupFields */ - private function fldFieldSelectorsHtml(string $groupName, array $groupFields, FieldLayout $fieldLayout): string + public function fldFieldSelectorsHtml(string $groupName, array $groupFields, FieldLayout $fieldLayout): string { $showGroup = Collection::make($groupFields)->contains( fn (BaseField $field) => $this->showFldFieldSelector($fieldLayout, $field), @@ -412,7 +343,7 @@ private function showFldFieldSelector(FieldLayout $fieldLayout, BaseField $field }); } - private function showFldUiElementSelector(FieldLayout $fieldLayout, FieldLayoutElement $uiElement): bool + public function showFldUiElementSelector(FieldLayout $fieldLayout, FieldLayoutElement $uiElement): bool { if ($uiElement->isMultiInstance()) { return true; diff --git a/src/FieldLayout/LayoutElements/BaseField.php b/src/FieldLayout/LayoutElements/BaseField.php index 1f31a872974..f49e4c2ffe9 100644 --- a/src/FieldLayout/LayoutElements/BaseField.php +++ b/src/FieldLayout/LayoutElements/BaseField.php @@ -185,7 +185,8 @@ protected function selectorInnerHtml(): string $icon = $this->selectorIcon(); $indicatorHtml = implode('', array_map(fn (array $indicator) => Html::tag('div', Icons::svg($indicator['icon'], altText: $indicator['label']), [ - 'class' => array_filter(['cp-icon', 'puny', $indicator['iconColor'] ?? null]), + 'class' => ['cp-icon', 'w-[0.75em]', 'text-fill-normal'], + 'data-color' => $indicator['iconColor'] ?? null, 'title' => $indicator['label'], ]), $this->selectorIndicators())); @@ -204,14 +205,14 @@ protected function selectorInnerHtml(): string 'class' => 'fld-attribute', ]). Html::tag('div', $this->attribute(), [ - 'class' => ['smalltext', 'light', 'code', 'fld-attribute-label'], + 'class' => ['text-xs', 'font-light', 'font-mono', 'fld-attribute-label'], 'title' => $this->attribute(), ]). Html::endTag('div'); // .fld-attribute if ($indicatorHtml) { $innerHtml .= Html::tag('div', $indicatorHtml, [ - 'class' => ['fld-field-indicators', 'flex', 'flex-nowrap', 'gap-xs'], + 'class' => ['fld-field-indicators', 'flex', 'flex-nowrap', 'gap-2'], ]); } @@ -221,7 +222,7 @@ protected function selectorInnerHtml(): string if ($icon) { return Html::tag('div', Icons::svg($icon), [ - 'class' => ['cp-icon', 'medium'], + 'class' => ['fld-element-icon'], ]).$html; } diff --git a/src/FieldLayout/LayoutElements/HorizontalRule.php b/src/FieldLayout/LayoutElements/HorizontalRule.php index 9fa1985cd57..9c3561fd0d5 100644 --- a/src/FieldLayout/LayoutElements/HorizontalRule.php +++ b/src/FieldLayout/LayoutElements/HorizontalRule.php @@ -35,7 +35,7 @@ public function selectorHtml(): string return <<
-
+
$label $indicatorHtml
diff --git a/src/FieldLayout/LayoutElements/LineBreak.php b/src/FieldLayout/LayoutElements/LineBreak.php index c93a710cd91..d5da595c642 100644 --- a/src/FieldLayout/LayoutElements/LineBreak.php +++ b/src/FieldLayout/LayoutElements/LineBreak.php @@ -35,7 +35,7 @@ public function selectorHtml(): string return <<
-
+
$label $indicatorHtml
diff --git a/src/Http/Controllers/Settings/EntryTypesController.php b/src/Http/Controllers/Settings/EntryTypesController.php index 287c5793901..e699d36b624 100644 --- a/src/Http/Controllers/Settings/EntryTypesController.php +++ b/src/Http/Controllers/Settings/EntryTypesController.php @@ -4,18 +4,14 @@ namespace CraftCms\Cms\Http\Controllers\Settings; -use CraftCms\Cms\Component\Contracts\Iconic; use CraftCms\Cms\Config\GeneralConfig; -use CraftCms\Cms\Cp\Html\ContentHtml; +use CraftCms\Cms\Cp\FieldLayoutDesigner\FieldLayoutDesigner; use CraftCms\Cms\Cp\Html\ElementHtml; -use CraftCms\Cms\Cp\Icons; use CraftCms\Cms\Entry\Data\EntryType; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Entry\EntryTypes; use CraftCms\Cms\Entry\Models\EntryType as EntryTypeModel; use CraftCms\Cms\Entry\Resources\EntryTypeResource; -use CraftCms\Cms\Field\Contracts\ElementContainerFieldInterface; -use CraftCms\Cms\Field\Contracts\FieldInterface; use CraftCms\Cms\Field\Enums\TranslationMethod; use CraftCms\Cms\Field\Fields; use CraftCms\Cms\FieldLayout\FieldLayout; @@ -24,11 +20,10 @@ use CraftCms\Cms\Http\Requests\TableRequest; use CraftCms\Cms\Http\RespondsWithFlash; use CraftCms\Cms\Http\Responses\CpScreenResponse; -use CraftCms\Cms\Section\Data\Section; use CraftCms\Cms\Shared\Enums\Color; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Facades\InputNamespace; -use CraftCms\Cms\Support\Html; +use CraftCms\Cms\Support\Facades\Sites; use CraftCms\Cms\Support\Str; use CraftCms\Cms\Support\Url; use CraftCms\Cms\View\HtmlStack; @@ -54,6 +49,7 @@ public function __construct( Fields $fields, GeneralConfig $generalConfig, private readonly EntryTypes $entryTypes, + private readonly FieldLayoutDesigner $fieldLayoutDesigner, ) { $this->readOnly = ! $generalConfig->allowAdminChanges; @@ -91,30 +87,12 @@ public function create(): CpScreenResponse { $entryType = new EntryType; - $fieldLayout = $entryType->getFieldLayout(); - - if ($entryType->hasTitleField && ! $fieldLayout->isFieldIncluded('title')) { - $fieldLayout->prependElements([new EntryTitleField]); - } - return new CpScreenResponse() ->title(t('Create a new entry type')) ->addCrumb(t('Settings'), 'settings') ->addCrumb(t('Entry Types'), 'settings/entry-types') - ->contentTemplate('settings/entry-types/_edit.twig', [ - 'entryTypeId' => null, - 'entryType' => $entryType, - 'typeName' => Entry::displayName(), - 'lowerTypeName' => Entry::lowerDisplayName(), - 'readOnly' => $this->readOnly, - ]) - ->action('entry-types/save') ->redirectUrl('settings/entry-types') - ->addAltAction(t('Save and continue editing'), [ - 'redirect' => 'settings/entry-types/{id}', - 'shortcut' => true, - 'retainScroll' => true, - ]); + ->inertiaPage('settings/EntryTypesEdit', $this->entryTypeProps($entryType, brandNew: true)); } public function edit(Request $request, ?EntryTypeModel $entryType = null): CpScreenResponse @@ -127,94 +105,80 @@ public function edit(Request $request, ?EntryTypeModel $entryType = null): CpScr abort_if(is_null($entryTypeData), 404, 'Entry type not found'); - $fieldLayout = $entryTypeData->getFieldLayout(); + return new CpScreenResponse() + ->editUrl($entryTypeData->getCpEditUrl()) + ->title(trim($entryTypeData->name) ?: t('Edit Entry Type')) + ->addCrumb(t('Settings'), 'settings') + ->addCrumb(t('Entry Types'), 'settings/entry-types') + ->redirectUrl('settings/entry-types') + ->inertiaPage('settings/EntryTypesEdit', $this->entryTypeProps($entryTypeData, brandNew: false)); + } - if ($entryTypeData->hasTitleField) { - // Ensure the Title field is present + /** + * Builds the Inertia props for the entry type edit/new screen. + */ + private function entryTypeProps(EntryType $entryType, bool $brandNew): array + { + $fieldLayout = $entryType->getFieldLayout(); + + // Normalize the Title field's presence so the round-tripped config matches what gets saved back. + if ($entryType->hasTitleField) { if (! $fieldLayout->isFieldIncluded('title')) { $fieldLayout->prependElements([new EntryTitleField]); } } else { - // Remove the title field foreach ($fieldLayout->getTabs() as $tab) { - $elements = array_filter($tab->getElements(), - fn (FieldLayoutElement $element) => ! $element instanceof EntryTitleField); + $elements = array_filter( + $tab->getElements(), + fn (FieldLayoutElement $element) => ! $element instanceof EntryTitleField, + ); $tab->setElements($elements); } } - return new CpScreenResponse() - ->editUrl($entryTypeData->getCpEditUrl()) - ->title(trim($entryTypeData->name) ?: t('Edit Entry Type')) - ->addCrumb(t('Settings'), 'settings') - ->addCrumb(t('Entry Types'), 'settings/entry-types') - ->contentTemplate('settings/entry-types/_edit.twig', [ - 'entryTypeId' => $entryTypeData->id, - 'entryType' => $entryTypeData, - 'typeName' => Entry::displayName(), - 'lowerTypeName' => Entry::lowerDisplayName(), - 'readOnly' => $this->readOnly, - ]) - ->unless( - $this->readOnly, - callback: function (CpScreenResponse $response) use ($entryTypeData) { - $response - ->action('entry-types/save') - ->redirectUrl('settings/entry-types') - ->addAltAction(t('Save and continue editing'), [ - 'redirect' => 'settings/entry-types/{id}', - 'shortcut' => true, - 'retainScroll' => true, - ]) - ->addAltAction(t('Save as a new entry type'), [ - 'params' => ['saveAsNew' => true], - 'redirect' => 'settings/entry-types/{id}', - ]) - ->addAltAction(t('Delete'), [ - 'action' => 'entry-types/delete', - 'destructive' => true, - ]) - ->metaSidebarHtml(app(ContentHtml::class)->metadataHtml([ - t('ID') => $entryTypeData->id, - t('Used by') => function () use ($entryTypeData) { - $usages = $entryTypeData->findUsages(); - if (empty($usages)) { - return Html::tag('i', t('No usages')); - } - - $labels = []; - $items = array_map(function (Section|ElementContainerFieldInterface $usage) use ( - &$labels - ) { - $icon = $usage instanceof FieldInterface && ! $usage instanceof Iconic - ? $usage::icon() - : $usage->getIcon(); - $label = $labels[] = $usage->getUiLabel(); - $labelHtml = Html::beginTag('span', [ - 'class' => ['flex', 'flex-nowrap', 'gap-s'], - ]). - Html::tag('div', Icons::svg($icon), [ - 'class' => ['cp-icon', 'small'], - ]). - Html::tag('span', Html::encode($label)). - Html::endTag('span'); - - return Html::a($labelHtml, $usage->getCpEditUrl()); - }, $entryTypeData->findUsages()); - - // sort by label - array_multisort($labels, SORT_ASC, $items); - - $items = array_map(fn ($item) => Html::li($item)->encode(false), $items); - - return Html::ul()->items(...$items)->render(); - }, - ])); - }, - default: function (CpScreenResponse $response) { - $response->noticeHtml(app(ContentHtml::class)->readOnlyNoticeHtml()); - }, - ); + // Render just the designer markup (with `autoBoot: false`, so it doesn't queue its + // own boot JS). + $fieldLayoutDesigner = [ + 'html' => $this->fieldLayoutDesigner->html($fieldLayout, [ + 'disabled' => $this->readOnly, + 'withGeneratedFields' => true, + 'withCardViewDesigner' => true, + 'autoBoot' => false, + ]), + ]; + + $translationMethodOptions = [ + ['value' => TranslationMethod::None->value, 'label' => t('Not translatable')], + ['value' => TranslationMethod::Site->value, 'label' => t('Translate for each site')], + ['value' => TranslationMethod::SiteGroup->value, 'label' => t('Translate for each site group')], + ['value' => TranslationMethod::Language->value, 'label' => t('Translate for each language')], + ['value' => TranslationMethod::Custom->value, 'label' => t('Custom…')], + ]; + + return [ + 'brandNew' => $brandNew, + 'entryType' => [ + 'id' => $entryType->id, + 'name' => $entryType->name, + 'handle' => $entryType->handle, + 'description' => $entryType->description, + 'uiLabelFormat' => $entryType->uiLabelFormat, + 'titleTranslationMethod' => $entryType->titleTranslationMethod->value, + 'titleTranslationKeyFormat' => $entryType->titleTranslationKeyFormat, + 'titleFormat' => $entryType->titleFormat, + 'allowLineBreaksInTitles' => (bool) $entryType->allowLineBreaksInTitles, + 'showSlugField' => (bool) $entryType->showSlugField, + 'slugTranslationMethod' => $entryType->slugTranslationMethod->value, + 'slugTranslationKeyFormat' => $entryType->slugTranslationKeyFormat, + 'showStatusField' => (bool) $entryType->showStatusField, + ], + 'fieldLayoutDesigner' => $fieldLayoutDesigner, + 'translationMethodOptions' => $translationMethodOptions, + 'typeName' => Entry::displayName(), + 'lowerTypeName' => Entry::lowerDisplayName(), + 'isMultiSite' => Sites::isMultiSite(), + 'readOnly' => $this->readOnly, + ]; } #[Deprecated(message: 'in 6.0. Use `settings/entry-types` instead.')]