This commit is contained in:
2026-08-13 12:49:10 +02:00
parent 48da8bccd8
commit 8d3ac08a37
22 changed files with 1127 additions and 350 deletions

689
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Evomedien\Vitec\UserFunc;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\ParameterType;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
@@ -20,12 +21,23 @@ use TYPO3\CMS\Extbase\Service\ImageService;
* today or later), soonest first. FlexForm options: showpast (append past
* events, most recent first, under "pastEvents"), limit, debug.
*
* Layout `regions` additionally emits "regions": the regions that actually have
* an upcoming event, each with the image of its next one. See fetchRegions().
*
* `render()` = TypoScript entry (cObj->data or page discovery).
* `renderForRecord()` = one specific tt_content row; reused by
* ContainerChildrenProcessor / ContentElementResolver. Exception-safe.
*/
class EventlistJsonRenderer
{
/**
* Parent of the region categories. Hard-coded like the other taxonomy roots
* in this extension (Annex B-5 tracks them); making it a FlexForm setting
* would put a raw uid in front of editors for a value that belongs to the
* content model, not to a single content element.
*/
private const REGION_PARENT_CATEGORY = 104;
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
@@ -101,6 +113,10 @@ class EventlistJsonRenderer
],
];
if ($layout === 'regions') {
$response['regions'] = $this->fetchRegions($daysInAdvance);
}
if ($showPast) {
$past = $this->fetchEvents(false, $limit);
$response['pastEvents'] = array_map(fn($e) => $this->serializeEvent($e), $past);
@@ -111,6 +127,9 @@ class EventlistJsonRenderer
'upcomingCount' => count($upcoming),
'settings' => $settings,
];
if (isset($response['regions'])) {
$response['debug']['regionCount'] = count($response['regions']);
}
}
return json_encode($response);
@@ -181,6 +200,215 @@ class EventlistJsonRenderer
return $qb->executeQuery()->fetchAllAssociative();
}
/**
* Regions for the `regions` layout: the categories below
* REGION_PARENT_CATEGORY that have at least one upcoming event, each
* carrying the image of its next one as the card logo.
*
* A region without an upcoming event is left out entirely -- that is the
* whole point of the layout, so `showpast` does not resurrect it. `limit`
* stays out too: it caps events, and applying it before the grouping would
* drop whole regions from the carousel without the editor seeing why.
*
* Order is the backend sorting of the categories, so the editor arranges the
* carousel by dragging them; `nextEvent.eventstart` is in the payload for a
* frontend that would rather sort chronologically.
*
* @return array<int,array<string,mixed>>
*/
private function fetchRegions(int $daysInAdvance): array
{
$regions = $this->fetchRegionCategories();
if ($regions === []) {
return [];
}
// Every category below a region -- the region itself included -- points
// at that region, so an event filed under a sub-category still counts.
$regionByCategory = [];
foreach ($regions as $regionUid => $region) {
foreach ($region['categoryUids'] as $categoryUid) {
$regionByCategory[$categoryUid] = $regionUid;
}
}
$rows = $this->fetchUpcomingEventsByCategory(array_keys($regionByCategory), $daysInAdvance);
// One row per event/category pair, sorted by eventstart -- so the first
// row seen for a region is its next event, and the ??= keeps it.
$eventsPerRegion = [];
foreach ($rows as $row) {
$regionUid = $regionByCategory[(int)$row['uid_local']] ?? null;
if ($regionUid === null) {
continue;
}
$eventsPerRegion[$regionUid][(int)$row['uid']] ??= $row;
}
$result = [];
foreach ($regions as $regionUid => $region) {
$regionEvents = $eventsPerRegion[$regionUid] ?? [];
if ($regionEvents === []) {
continue;
}
$next = reset($regionEvents);
$start = (int)($next['eventstart'] ?? 0);
$end = (int)($next['eventend'] ?? 0);
$result[] = [
'uid' => $regionUid,
'title' => $region['title'],
// Plain text, not RTE: sys_category descriptions are named as
// out of scope for the richtext conversion in Clause 9.11.
'description' => $region['description'],
'eventCount' => count($regionEvents),
'image' => $this->getEventImage((int)$next['uid']),
'nextEvent' => [
'uid' => (int)$next['uid'],
'title' => (string)($next['title'] ?? ''),
'slug' => (string)($next['slug'] ?? ''),
'eventstart' => $start > 0 ? date('Y-m-d', $start) : null,
'eventend' => $end > 0 ? date('Y-m-d', $end) : null,
'eventurl' => (string)($next['eventurl'] ?? ''),
],
];
}
return $result;
}
/**
* The region categories, keyed by uid and in backend sorting order, each
* with its own uid plus every descendant.
*
* sys_category is read in one go rather than per level: the table is small,
* and CustomerlogosJsonRenderer and MarketListJsonRenderer make the same
* trade-off.
*
* @return array<int,array{title:string,description:string,categoryUids:int[]}>
*/
private function fetchRegionCategories(): array
{
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_category');
$rows = $qb
->select('uid', 'title', 'description', 'parent')
->from('sys_category')
->where(
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
)
->orderBy('sorting', 'ASC')
->executeQuery()
->fetchAllAssociative();
$byUid = [];
$childrenByParent = [];
foreach ($rows as $row) {
$uid = (int)$row['uid'];
$byUid[$uid] = $row;
$childrenByParent[(int)($row['parent'] ?? 0)][] = $uid;
}
$regions = [];
foreach ($childrenByParent[self::REGION_PARENT_CATEGORY] ?? [] as $regionUid) {
$regions[$regionUid] = [
'title' => (string)($byUid[$regionUid]['title'] ?? ''),
'description' => (string)($byUid[$regionUid]['description'] ?? ''),
'categoryUids' => $this->collectCategoryBranch($regionUid, $childrenByParent),
];
}
return $regions;
}
/**
* A category plus every descendant, breadth first. Iterative and
* seen-guarded: a parent pointing back up the tree would otherwise loop.
*
* @param array<int,int[]> $childrenByParent
* @return int[]
*/
private function collectCategoryBranch(int $uid, array $childrenByParent): array
{
$collected = [$uid];
$seen = [$uid => true];
$queue = [$uid];
while ($queue !== []) {
$current = array_shift($queue);
foreach ($childrenByParent[$current] ?? [] as $childUid) {
if (isset($seen[$childUid])) {
continue;
}
$seen[$childUid] = true;
$collected[] = $childUid;
$queue[] = $childUid;
}
}
return $collected;
}
/**
* Upcoming events assigned to any of the given categories, soonest first.
* One row per event/category pair, so an event in two regions appears in
* both. The "upcoming" test is the one fetchEvents() uses.
*
* @param int[] $categoryUids
* @return array<int,array<string,mixed>>
*/
private function fetchUpcomingEventsByCategory(array $categoryUids, int $daysInAdvance): array
{
if ($categoryUids === []) {
return [];
}
$todayMidnight = strtotime('today');
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_domain_model_event');
$expr = $qb->expr();
$qb
->select('e.uid', 'e.title', 'e.slug', 'e.eventstart', 'e.eventend', 'e.eventurl')
->addSelect('mm.uid_local')
->from('tx_vitec_domain_model_event', 'e')
->join(
'e',
'sys_category_record_mm',
'mm',
'mm.uid_foreign = e.uid AND mm.tablenames = ' .
$qb->createNamedParameter('tx_vitec_domain_model_event', ParameterType::STRING) .
' AND mm.fieldname = ' .
$qb->createNamedParameter('categories', ParameterType::STRING)
)
->where(
$expr->in('mm.uid_local', $qb->createNamedParameter($categoryUids, ArrayParameterType::INTEGER)),
$expr->eq('e.deleted', 0),
$expr->eq('e.hidden', 0),
$expr->eq('e.hideonwebsite', 0),
$expr->or(
$expr->gte('e.eventend', $qb->createNamedParameter($todayMidnight, ParameterType::INTEGER)),
$expr->and(
$expr->eq('e.eventend', 0),
$expr->gte('e.eventstart', $qb->createNamedParameter($todayMidnight, ParameterType::INTEGER))
)
)
)
->orderBy('e.eventstart', 'ASC');
if ($daysInAdvance > 0) {
$qb->andWhere(
$expr->lt('e.eventstart', $qb->createNamedParameter($todayMidnight + ($daysInAdvance * 86400), ParameterType::INTEGER))
);
}
return $qb->executeQuery()->fetchAllAssociative();
}
/**
* @param array<string,mixed> $event
* @return array<string,mixed>

View File

@@ -27,6 +27,10 @@
<label>Teaser Bar</label>
<value>teaserbar</value>
</numIndex>
<numIndex index="3">
<label>List by Region</label>
<value>regions</value>
</numIndex>
</items>
<default>list</default>
</config>

View File

@@ -110,6 +110,12 @@ editor:
- { name: 'Keyboard input', element: 'kbd' }
- { name: 'Delete / Strike', element: 'del' }
- { name: 'Insert / Underline', element: 'ins' }
# Inline text colours. Class names mirror the button styles
# (btn-vitec--blue -> text-vitec--blue) so both share one vocabulary.
- { name: 'Text: VITEC Blue', element: 'span', classes: ['text-vitec--blue'] }
- { name: 'Text: VITEC Orange', element: 'span', classes: ['text-vitec--orange'] }
- { name: 'Text: VITEC Midnight', element: 'span', classes: ['text-vitec--midnight'] }
- { name: 'Text: VITEC White', element: 'span', classes: ['text-vitec--white'] }
# ---- Alignment -----------------------------------------
alignment:

View File

@@ -125,6 +125,26 @@ use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
['label' => 'Vitec: Card Style', 'value' => 'vitec-card'],
['label' => 'Vitec: Dark Background', 'value' => 'vitec-dark'],
['label' => 'Vitec: Highlight Box', 'value' => 'vitec-highlight'],
// Background variants, ordered light to dark. Mutually exclusive by
// intent, but frame_class is a multi-select here (selectCheckBox,
// see above) and stays that way on purpose -- a restriction would
// have to be built for every other class too. If an editor ticks two
// backgrounds, both end up comma-separated in the JSON `frameClass`
// and the frontend decides which one wins.
//
// `vitec-bg-graphite`, `-blue` and `-midnight` were already styled in
// the frontend bundle but had no backend option; `-white` and
// `-light-grey` still need their CSS.
//
// The older 'Vitec: Dark Background' (vitec-dark) above stays as it
// is: existing content depends on it, and it is the most heavily
// styled class of them all.
['label' => 'Vitec: White Background', 'value' => 'vitec-bg-white'],
['label' => 'Vitec: Light-Grey Background', 'value' => 'vitec-bg-light-grey'],
['label' => 'Vitec: VITEC-Graphite Background', 'value' => 'vitec-bg-graphite'],
['label' => 'Vitec: VITEC-Blue Background', 'value' => 'vitec-bg-blue'],
['label' => 'Vitec: VITEC-Midnight Background', 'value' => 'vitec-bg-midnight'],
]
);
@@ -132,4 +152,53 @@ use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
$GLOBALS['TCA']['tt_content']['columns']['tx_vitec_usecase_content'] = [
'config' => ['type' => 'passthrough'],
];
// The standard TYPO3 "Appearance" tab for the Content Blocks elements.
//
// Content Blocks builds each element's showitem itself
// (TcaGenerator::getContentElementStandardShowItem) and appends only the
// "Extended" tab -- Appearance is never part of it. The Extbase plugins are
// unaffected: ExtensionManagementUtility::addPlugin() seeds their type from
// tt_content types['header'], which carries the tab already.
//
// Until now that meant an editor could not reach layout, frame_class
// (including the Vitec classes registered above), spaceBefore/spaceAfter,
// sectionIndex or linkToTop on a Content Block at all.
//
// Deliberately NOT declared as a palette in every config.yaml: palettes live
// in TCA under a global identifier, so a second definition of `frames` would
// collide with the Core one -- the same trap that took the page module down
// on 2026-08-10. Pointing at the Core palettes keeps a single definition.
//
// This file runs after Content Blocks, which generates its TCA on
// BeforeTcaOverridesEvent.
$appearanceTab = '--div--;core.form.tabs:appearance,'
. '--palette--;;frames,'
. '--palette--;;appearanceLinks';
$extendedTab = '--div--;core.form.tabs:extended';
foreach ($GLOBALS['TCA']['tt_content']['types'] as $cType => &$typeConfig) {
// Own elements only, and only those still missing the tab -- that skips
// the plugins and makes a repeated run a no-op.
if (!str_starts_with((string)$cType, 'vitec_')
|| !isset($typeConfig['showitem'])
|| str_contains($typeConfig['showitem'], '--palette--;;frames')
) {
continue;
}
// Content Blocks puts "Extended" last, so Appearance goes in front of it
// and the tab order matches the Core content elements. Should a future
// version drop that marker, append rather than silently do nothing.
if (str_contains($typeConfig['showitem'], $extendedTab)) {
$typeConfig['showitem'] = str_replace(
$extendedTab,
$appearanceTab . ',' . $extendedTab,
$typeConfig['showitem']
);
} else {
$typeConfig['showitem'] = rtrim($typeConfig['showitem'], ", \t\n\r") . ',' . $appearanceTab;
}
}
unset($typeConfig);
})();

View File

@@ -19,5 +19,11 @@ fields:
value: light
- label: 'LLL:EXT:vitec/Resources/Private/Language/locallang_buttons.xlf:button_style.items.dark.label'
value: dark
- label: 'LLL:EXT:vitec/Resources/Private/Language/locallang_buttons.xlf:button_style.items.transparent_white_outline.label'
value: transparent_white_outline
- label: 'LLL:EXT:vitec/Resources/Private/Language/locallang_buttons.xlf:button_style.items.transparent_black_outline.label'
value: transparent_black_outline
- label: 'LLL:EXT:vitec/Resources/Private/Language/locallang_buttons.xlf:button_style.items.transparent_red_outline.label'
value: transparent_red_outline
- label: 'LLL:EXT:vitec/Resources/Private/Language/locallang_buttons.xlf:button_style.items.text_only.label'
value: text_only

View File

@@ -19,5 +19,11 @@ fields:
value: light
- label: 'LLL:EXT:vitec/Resources/Private/Language/locallang_buttons.xlf:secondary_button_style.items.dark.label'
value: dark
- label: 'LLL:EXT:vitec/Resources/Private/Language/locallang_buttons.xlf:secondary_button_style.items.transparent_white_outline.label'
value: transparent_white_outline
- label: 'LLL:EXT:vitec/Resources/Private/Language/locallang_buttons.xlf:secondary_button_style.items.transparent_black_outline.label'
value: transparent_black_outline
- label: 'LLL:EXT:vitec/Resources/Private/Language/locallang_buttons.xlf:secondary_button_style.items.transparent_red_outline.label'
value: transparent_red_outline
- label: 'LLL:EXT:vitec/Resources/Private/Language/locallang_buttons.xlf:secondary_button_style.items.text_only.label'
value: text_only

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
<rect x="1.5" y="3.5" width="13" height="9" rx="1" fill="none" stroke="#000" stroke-width="1.2"/>
<rect x="8" y="4.7" width="5.3" height="6.6" rx="0.3" fill="#000" opacity="0.5"/>
<rect x="3" y="5.4" width="4" height="1" rx="0.2" fill="#000" opacity="0.55"/>
<rect x="3" y="7.1" width="3.2" height="0.8" rx="0.2" fill="#000" opacity="0.35"/>
<rect x="3" y="8.6" width="4" height="0.8" rx="0.2" fill="#000" opacity="0.35"/>
<rect x="3" y="10.2" width="2.6" height="1.2" rx="0.3" fill="#000" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 609 B

View File

@@ -0,0 +1,129 @@
name: vitec/featured-content
group: vitec_custom_components
prefixFields: true
prefixType: vendor
fields:
# ──────────────────────────────────────────────────────────────────────
- identifier: tab_content
type: Tab
label: Content
- identifier: header_section
type: Palette
label: Header
fields:
- identifier: header
useExistingField: true
required: true
- type: Linebreak
- identifier: header_layout
useExistingField: true
- identifier: header_position
useExistingField: true
- identifier: date
useExistingField: true
- type: Linebreak
- identifier: header_link
useExistingField: true
- type: Linebreak
- identifier: subheader
useExistingField: true
- identifier: bodytext
useExistingField: true
enableRichtext: true
- identifier: cta
type: Link
allowedTypes:
- page
- url
- file
- email
- identifier: cta_label
type: Text
default: 'Learn more'
max: 30
- identifier: Vitec/ButtonStyle
type: Basic
- identifier: featured_layout
type: Select
renderType: selectSingle
default: text_left
items:
- label: Text left, media right
value: text_left
- label: Text right, media left
value: text_right
# ──────────────────────────────────────────────────────────────────────
- identifier: tab_media
type: Tab
label: Media
- identifier: featured_image
type: File
minitems: 0
maxitems: 1
allowed: common-image-types
extendedPalette: true
- identifier: featured_video
type: File
minitems: 0
maxitems: 1
allowed: mp4,webm,ogv,mov,m4v
- identifier: featured_video_autoplay
type: Checkbox
default: 0
- identifier: featured_video_loop
type: Checkbox
default: 0
- identifier: featured_video_muted
type: Checkbox
default: 1
# ──────────────────────────────────────────────────────────────────────
- identifier: tab_overlay
type: Tab
label: Overlay
- identifier: overlay_color
type: Color
valuePicker:
items:
- label: 'VITEC Blue'
value: '#26358C'
- identifier: overlay_opacity
type: Number
format: decimal
default: 0.4
range:
lower: 0
upper: 1
slider:
step: 0.05
width: 200
# Horizontal position, not a vertical one: this element is wide rather than
# tall, so the overlay runs across the width. 0 = gradient starts at the very
# left edge (overlay covers everything from there), 100 = it starts at the
# right edge. Which side is covered follows featured_layout, so the text side
# stays readable.
- identifier: overlay_gradient_start
type: Number
default: 50
range:
lower: 0
upper: 100
slider:
step: 1
width: 300

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2">
<file source-language="en" datatype="plaintext" original="messages">
<body>
<!-- Element -->
<trans-unit id="title">
<source>VITEC · Featured Content</source>
</trans-unit>
<trans-unit id="description">
<source>Wide feature block: headline + RTE text + CTA next to an image or video, with a horizontal overlay gradient.</source>
</trans-unit>
<!-- Tabs -->
<trans-unit id="tab_content.label">
<source>Content</source>
</trans-unit>
<trans-unit id="tab_media.label">
<source>Media</source>
</trans-unit>
<trans-unit id="tab_overlay.label">
<source>Overlay</source>
</trans-unit>
<!-- Content -->
<trans-unit id="cta.label">
<source>CTA Link</source>
</trans-unit>
<trans-unit id="cta_label.label">
<source>CTA Button Text</source>
</trans-unit>
<trans-unit id="featured_layout.label">
<source>Layout</source>
</trans-unit>
<trans-unit id="featured_layout.description">
<source>Which side the text sits on. The media fills the other side.</source>
</trans-unit>
<trans-unit id="featured_layout.items.text_left.label">
<source>Text left, media right</source>
</trans-unit>
<trans-unit id="featured_layout.items.text_right.label">
<source>Text right, media left</source>
</trans-unit>
<!-- Media -->
<trans-unit id="featured_image.label">
<source>Image</source>
</trans-unit>
<trans-unit id="featured_video.label">
<source>Video</source>
</trans-unit>
<trans-unit id="featured_video.description">
<source>If both image and video are set, the video wins and the image serves as its poster.</source>
</trans-unit>
<trans-unit id="featured_video_autoplay.label">
<source>Autoplay video</source>
</trans-unit>
<trans-unit id="featured_video_loop.label">
<source>Loop video</source>
</trans-unit>
<trans-unit id="featured_video_muted.label">
<source>Mute video</source>
</trans-unit>
<!-- Overlay -->
<trans-unit id="overlay_color.label">
<source>Overlay Colour</source>
</trans-unit>
<trans-unit id="overlay_color.description">
<source>Colour of the gradient laid over the image or video. Leave empty for no overlay.</source>
</trans-unit>
<trans-unit id="overlay_opacity.label">
<source>Overlay Opacity (01)</source>
</trans-unit>
<trans-unit id="overlay_opacity.description">
<source>Strength of the overlay at its fully covered end.</source>
</trans-unit>
<trans-unit id="overlay_gradient_start.label">
<source>Gradient Start (% of width)</source>
</trans-unit>
<trans-unit id="overlay_gradient_start.description">
<source>Where across the width the overlay begins to fade out. 0 = fades out right at the text edge, 100 = the overlay covers the full width. The fade always runs away from the text side, so the text stays readable.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -0,0 +1,93 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:layout name="Preview"/>
<f:section name="Content">
<f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/>
<div class="vitec-preview">
<f:comment>Thumbnail: video poster falls back to the image</f:comment>
<f:if condition="{data.featured_image.0}">
<f:then>
<f:image image="{data.featured_image.0}"
class="vitec-preview__thumb"
width="120c"
height="80c"
alt="Featured Content Preview"/>
</f:then>
<f:else>
<div class="vitec-preview__thumb-placeholder">No Image</div>
</f:else>
</f:if>
<div class="vitec-preview__body">
<div class="vitec-preview__label">VITEC · Featured Content</div>
<h3 class="vitec-preview__headline">
<f:if condition="{data.header}">
<f:then>{data.header}</f:then>
<f:else>
<em style="color:#c00;">⚠ Headline missing</em>
</f:else>
</f:if>
</h3>
<f:if condition="{data.subheader}">
<div class="vitec-preview__subline">{data.subheader}</div>
</f:if>
<f:if condition="{data.bodytext}">
<div class="vitec-preview__body-text">
<f:format.stripTags>{data.bodytext}</f:format.stripTags>
</div>
</f:if>
<f:if condition="{data.cta_label}">
<div class="vitec-preview__ctas">
<span class="vitec-preview__cta-btn">{data.cta_label} →</span>
</div>
</f:if>
</div>
<div class="vitec-preview__settings">
<span class="vitec-badge">
<f:switch expression="{data.featured_layout}">
<f:case value="text_left">◧ Text left</f:case>
<f:case value="text_right">◨ Text right</f:case>
<f:defaultCase>{data.featured_layout}</f:defaultCase>
</f:switch>
</span>
<f:if condition="{data.featured_video.0}">
<span class="vitec-badge">▶ Video</span>
</f:if>
<f:comment>
The preview cannot paint the styles, so the chosen one is named.
Same reasoning as in the hero section.
</f:comment>
<f:if condition="{data.cta_label}">
<span class="vitec-badge">Button: {data.button_style}</span>
</f:if>
<f:if condition="{data.overlay_color}">
<span class="vitec-badge">
Overlay: {data.overlay_color} · {data.overlay_opacity} · from {data.overlay_gradient_start}%
</span>
</f:if>
<f:if condition="{data.cta_label} && {data.cta.url} == ''">
<span class="vitec-badge vitec-badge--warning">⚠ Button text without link</span>
</f:if>
<f:if condition="{data.featured_image.0} == '' && {data.featured_video.0} == ''">
<span class="vitec-badge vitec-badge--warning">⚠ No image or video</span>
</f:if>
</div>
</div>
</f:section>
</html>

View File

@@ -0,0 +1,3 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<!-- Headless mode: JSON is built by nb-headless-content-blocks -->
</html>

View File

@@ -51,9 +51,24 @@ fields:
default: 'Learn more'
max: 30
- identifier: secondary_cta
type: Link
allowedTypes:
- page
- url
- file
- email
- identifier: secondary_cta_label
type: Text
max: 30
- identifier: Vitec/ButtonStyle
type: Basic
- identifier: Vitec/SecondaryButtonStyle
type: Basic
- identifier: hero_layout_variant
type: Select
renderType: selectSingle

View File

@@ -26,6 +26,17 @@
<source>CTA Button Text</source>
</trans-unit>
<trans-unit id="secondary_cta.label">
<source>Second CTA Link (optional)</source>
</trans-unit>
<trans-unit id="secondary_cta.description">
<source>Leave empty for a single button. With both link and label filled, the two buttons render side by side.</source>
</trans-unit>
<trans-unit id="secondary_cta_label.label">
<source>Second CTA Button Text</source>
</trans-unit>
<trans-unit id="hero_image.label">
<source>Hero Image</source>
</trans-unit>

View File

@@ -46,9 +46,14 @@
</div>
</f:if>
<f:if condition="{data.cta_label}">
<f:if condition="{data.cta_label} || {data.secondary_cta_label}">
<div class="vitec-preview__ctas">
<span class="vitec-preview__cta-btn">{data.cta_label}</span>
<f:if condition="{data.cta_label}">
<span class="vitec-preview__cta-btn">{data.cta_label} →</span>
</f:if>
<f:if condition="{data.secondary_cta_label}">
<span class="vitec-preview__cta-btn vitec-preview__cta-btn--secondary">{data.secondary_cta_label} →</span>
</f:if>
</div>
</f:if>
</div>
@@ -60,6 +65,27 @@
Background: {data.background_variant}
</span>
<f:comment>
The preview has no per-style colours, so the chosen styles are named
instead of painted — otherwise "Transparent White Outline" would be
indistinguishable from the default orange here.
</f:comment>
<f:if condition="{data.cta_label}">
<span class="vitec-badge">Button: {data.button_style}</span>
</f:if>
<f:if condition="{data.secondary_cta_label}">
<span class="vitec-badge">2nd Button: {data.secondary_button_style}</span>
</f:if>
<f:if condition="{data.secondary_cta_label}">
<f:if condition="{data.secondary_cta}">
<f:else>
<span class="vitec-badge vitec-badge--warning">⚠ 2nd button without link</span>
</f:else>
</f:if>
</f:if>
<f:if condition="{data.show_logo_wall}">
<span class="vitec-badge">🏢 Logo Wall</span>
</f:if>

View File

@@ -121,6 +121,29 @@ fields:
type: Checkbox
default: 0
# Background colour for the whole element. No default on purpose: empty means
# "no background", so every existing record keeps the look it has today.
# The presets are the VITEC palette from Resources/Public/SCSS/_vitec.scss,
# in the same order as the button styles above so both dropdowns read alike.
# Lower-case hex, because the native colour picker emits lower case -- an
# upper-case preset would store the same colour under two spellings.
- identifier: background_color
type: Color
valuePicker:
items:
- label: 'VITEC Orange'
value: '#f47937'
- label: 'VITEC Blue'
value: '#26358c'
- label: 'VITEC Graphite'
value: '#313131'
- label: 'VITEC Midnight'
value: '#0d0d0d'
- label: 'VITEC Light Grey'
value: '#cccccc'
- label: 'White'
value: '#ffffff'
- identifier: debug
type: Checkbox
default: 0

View File

@@ -66,6 +66,14 @@
<trans-unit id="fullwidth.label">
<source>Render full-width (edge to edge)</source>
</trans-unit>
<trans-unit id="background_color.label">
<source>Background Colour</source>
</trans-unit>
<trans-unit id="background_color.description">
<source>Background for the whole element. Leave empty for no background. The list next to the field offers the VITEC palette; any other colour can still be picked freely.</source>
</trans-unit>
<trans-unit id="debug.label">
<source>Allow Debug Output</source>
</trans-unit>

View File

@@ -60,6 +60,18 @@
<span class="vitec-badge">↔ Full width</span>
</f:if>
<f:comment>
The dot modifiers in backend-preview.css are fixed palette classes;
this colour is free-form, so the swatch carries the stored hex as an
inline style. The outline keeps white visible on the light preview.
</f:comment>
<f:if condition="{data.background_color}">
<span class="vitec-badge">
<span class="vitec-badge__dot" style="background: {data.background_color}; border: 1px solid #999;"></span>
BG: {data.background_color}
</span>
</f:if>
<f:if condition="{data.button_text} && {data.button_link.url} == ''">
<span class="vitec-badge vitec-badge--warning">⚠ Button text without link</span>
</f:if>

View File

@@ -25,6 +25,15 @@
<trans-unit id="button_style.items.dark.label" resname="button_style.items.dark.label">
<source>Dark</source>
</trans-unit>
<trans-unit id="button_style.items.transparent_white_outline.label" resname="button_style.items.transparent_white_outline.label">
<source>Transparent, White Outline</source>
</trans-unit>
<trans-unit id="button_style.items.transparent_black_outline.label" resname="button_style.items.transparent_black_outline.label">
<source>Transparent, Black Outline</source>
</trans-unit>
<trans-unit id="button_style.items.transparent_red_outline.label" resname="button_style.items.transparent_red_outline.label">
<source>Transparent, Red Outline</source>
</trans-unit>
<trans-unit id="button_style.items.text_only.label" resname="button_style.items.text_only.label">
<source>Only Text (no button background)</source>
</trans-unit>
@@ -51,6 +60,15 @@
<trans-unit id="secondary_button_style.items.dark.label" resname="secondary_button_style.items.dark.label">
<source>Dark</source>
</trans-unit>
<trans-unit id="secondary_button_style.items.transparent_white_outline.label" resname="secondary_button_style.items.transparent_white_outline.label">
<source>Transparent, White Outline</source>
</trans-unit>
<trans-unit id="secondary_button_style.items.transparent_black_outline.label" resname="secondary_button_style.items.transparent_black_outline.label">
<source>Transparent, Black Outline</source>
</trans-unit>
<trans-unit id="secondary_button_style.items.transparent_red_outline.label" resname="secondary_button_style.items.transparent_red_outline.label">
<source>Transparent, Red Outline</source>
</trans-unit>
<trans-unit id="secondary_button_style.items.text_only.label" resname="secondary_button_style.items.text_only.label">
<source>Only Text (no button background)</source>
</trans-unit>

View File

@@ -73,6 +73,29 @@
color: #26358c;
}
/* Inline text colours. The names mirror the button styles above
(btn-vitec--blue -> text-vitec--blue) so both use one vocabulary. */
.text-vitec--orange {
color: #f47937;
}
.text-vitec--blue {
color: #26358c;
}
.text-vitec--midnight {
color: #0d0d0d;
}
.text-vitec--white {
color: #ffffff;
/* Editor only: white on the white editor canvas is invisible, so the preview
puts it on a dark plate. This file is loaded into the RTE iframe alone --
the frontend styles the class itself and needs no plate. */
background-color: #313131;
padding: 0 0.15em;
}
.text-start {
text-align: left;
}

View File

@@ -401,6 +401,6 @@ AuthUserFile "/usr/www/users/vitecevo/live/public/.htpasswd"
AuthName "DEV"
AuthType Basic
<RequireAny>
Require ip 217.70.192.164
Require valid-user
Require ip 217.70.192.164
Require valid-user
</RequireAny>

View File

@@ -25,8 +25,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<title>VITEC</title>
<script type="module" crossorigin src="/_frontend/assets/index-DjsvU0JX.js"></script>
<link rel="stylesheet" crossorigin href="/_frontend/assets/index-CUeIsXnS.css">
<script type="module" crossorigin src="/_frontend/assets/index-CI1dU4kK.js"></script>
<link rel="stylesheet" crossorigin href="/_frontend/assets/index-DtQru7Bt.css">
</head>
<body>
<div id="page"></div>