-
Render referenced global sections on pages (dereference + query merge + config wiring).
-
Add new Energy Label Model
Canonical types (new):
ProductVariantEnergyLabelentity component (@laioutr-core/canonical-types/entity/product-variant) defines the per-variant shape:badge: Media— small inline energy-class badge image (A–G).label: Media— full-size energy label image, opened in a lightbox.title?: string— human-readable title (e.g. "Energy class A++"), used as the alt text on the badge image. Optional: consumers fall back to a generated name or a translated default.energyClass?: string— energy efficiency class (e.g. "A", "A++"). Free-form string, not an enum, so the type survives the EU periodically rescaling its class vocabulary (regulation 2017/1369).energyClassScaleMax?: string/energyClassScaleMin?: string— the most and least efficient class on the product category's regulated scale, so a class can be interpreted against its range.eprelRegistrationNumber?: string— identifier for the variant's entry in the EU EPREL registry.dataSheetLink?: string— optional URL of the product information sheet / data sheet PDF.
The component lives on
ProductVariant(next togtin,prices,availability) because each EU energy label is registered per commercial model in EPREL, keyed by the model identifier / GTIN — which is variant-level. Variants of the same product can carry different energy classes.Adapters (shopify, shopware, ambiendo, etc.) should populate this component when the underlying variant carries an EU energy label; variants without the data simply omit the component and the block renders nothing.
-
Add
CalendarDatevalue type — an ISOYYYY-MM-DDcalendar date (no time, no timezone), exported from@laioutr-core/core-types/common. Use it for whole-day values such as a location's opening/reopening date. -
Align RcGlobalSection slots and queries to RcDictionary; add optional studio.description.
-
Add
sectionBrandList.headingandsectionGlossaryList.headingtranslation keys (EN: "Brands"/"Glossary", DE: "Marken"/"Glossar"). These act as the locale-aware fallback heading when an editor leaves the section'sheadingfield empty.Breaking: Remove the orphaned
brandGrid.titletranslation key — it is no longer referenced anywhere. Consumers that overrode this key in custom locales can delete the override. -
Add
target?: stringprop toLinkTile,LinkTileBasic,LinkTileCompact,LinkTileBig, andNavigationMenuTextItem. The new prop is forwarded to the underlyingNuxtLink/MaybeLink, allowing consumers to open the link in a new browsing context (_blank) while keeping_selfas the default behaviour. -
Media: render video/audio via built-in native renderers, overridable per type
<Media>is now a dispatcher. Images render via the built-in image renderer (unchanged public prop API and DOM/CSS output). Video and audio now render via new built-in native renderers:MediaVideo(native<video>) andMediaAudio(native<audio>with the optionalcovershown above it). AMediavalue of any type renders out of the box with no registration.Playback is controlled by new flat props on
<Media>, mirroring the HTML attributes 1:1:controls(defaulttrue),autoplay,muted,loop,playsinline(defaultfalse). These are deliberately not part of theMediavalue: playback is a per-placement decision (a controllable player vs. a muted autoplay background loop), so the consuming Block sets them. They forward to whichever renderer runs; image media ignores them.Consumers can override the built-in renderer for a media type (e.g. for HLS/DASH streaming or a custom player) by registering one at app root. A registered renderer takes precedence over the built-in for its type:
// plugins/media-renderers.ts import { provideMediaRenderers } from '#ui-kit/components/Media/MediaRenderersProvider'; import VidstackMedia from '../components/VidstackMedia.vue'; export default defineNuxtPlugin((nuxtApp) => { provideMediaRenderers(nuxtApp.vueApp, { video: VidstackMedia, audio: VidstackMedia }); });
Each renderer receives the narrowed
mediaobject, the playback props, and any forwarded attributes. The native built-ins handle progressive sources; responsive source switching and adaptive streaming (HLS/DASH) are the concern of a registered player.MediaStagenow drives its background<Media>as a decorative loop (autoplay muted loop playsinline,controls={false}). A background asset is a per-placement concern owned by the stage, so a picked video plays silently and loops behind the foreground content instead of sitting idle with a play button. Image backgrounds are unaffected (image media ignores playback props).MediaVideosuppressesautoplaywhen the user prefers reduced motion (prefers-reduced-motion: reduce), settling on theposterframe instead. This applies to every autoplay video, not just backgrounds, and is the correct fallback for placements like the background loop above that hide controls. -
Add
Countdowncomponent (#ui-kit/components/Countdown/Countdown.vue) and auseCountdowncomposable for editorial countdowns. Unit labels come fromIntl.NumberFormat, so they are localized and plural-aware automatically; theunitDisplayprop ('long' | 'short' | 'narrow') sets their verbosity.Countdownalso takes an optional frozennow(for tests/Storybook), and a purecomputeCountdown(endDate, now)helper is exported. Addscountdown.expiredandpromotionBanner.{codeCopiedTitle,codeCopiedSubline,copyCodeAriaLabel}locale entries (EN + DE).useNownow accepts an optional tick interval —useNow(intervalMs = 60_000)— and is seeded viauseState, so the shared clock renders byte-identically across SSR and hydration; consumers no longer needdata-allow-mismatchon time-dependent nodes. Same-interval callers share one clock;Countdownticks onuseNow(1000).PromotionBanner used to live here; it has been moved to
@laioutr-core/ui(see that changeset) because promo-codes are commerce-domain. ui-kit no longer exportsPromotionBanner/types— consumers should import from@laioutr-core/uiinstead. -
Paginationnow emits SEO-correct sequence semantics whenhrefTemplateis set:- The previous/next anchors carry
rel="prev"/rel="next"only on edges where the target page actually exists. - On the first page, the previous control renders as a
<button>(nohref); on the last page, the next control does the same. This stops crawlers from following dead links to page 0 or page N+1.
To support this,
ButtonandIconButtongain arel?: stringprop. It is forwarded to the underlyingNuxtLinkonly when the component renders as a link (hrefset), and ignored when rendered as a<button>. Accepts any valid HTMLreltoken or space-separated combination — e.g.'prev','next','noopener','noopener noreferrer','external nofollow'. - The previous/next anchors carry
-
Add
BlockProductDetailEnergyLabelto the Product Detail page. Renders the EU energy efficiency label: an inline energy-class badge (opening the full label in a lightbox) and an optional product data sheet link next to the product information.ui (new):
EnergyLabelcomponent renders the badge image, lightbox trigger, and optional data sheet link. Props:badge: Media,label: Media,title?: string,dataSheetLink?: string, pluswidth/heightfor the badge image. Whentitleis omitted, the alt text falls back to thepdp.energyLabeltranslation.ui-kit: Added i18n key
pdp.energyLabel(en + de + type), used as the fallback alt text for the energy-label badge image.ui-app: New
BlockProductDetailEnergyLabel.vuequeries the product's variants link forProductVariantEnergyLabeland renders the label of the selected variant (resolved viauseProductVariantContext, falling back to the first variant). The block is non-standalone and placeable in the Product Detail section.Follow-up (out of scope here): adapter packages need to implement the
ProductVariantEnergyLabelcomponent resolver to deliver data from the underlying platform. Until then, the block is placeable but renders nothing in storefronts. -
Add
PromotionBanner(moved here from@laioutr-core/ui-kitbecause promo-codes + checkout language are commerce-domain). Accepts headings, optional countdown (viauseCountdownfrom ui-kit), a promo-code copy button, a CTA, surface preset (default | pale | bright | solid) orcustomcolors with per-slot overrides (background / text / countdown / icon). Story title:UI/Sections/PromotionBanner.Internal:
- Auto-promotes
variantto'custom'when anycustomColorsfield is set, so consumers don't have to flip the variant flag manually. - Uses the surface-tone API directly (
OnSurface+tone+colorToSurfaceTone). - Toast strings and aria-labels resolved through
useLocale().t().
Adds
@vueuse/core(useClipboard) as a@laioutr-core/uidependency. - Auto-promotes
-
Plumb
target?: stringthrough link-rendering components so consumers can choose the browsing context of editorial links:BrandList—BrandListItem.targetcarries through to the per-brandNuxtLink.HeaderBasic— newlogoTargetprop on the logo link.HeaderShop— newlogoTargetprop on the logo link.LogoSlider/LogoGrid/LogoSliderSlide—LogoSliderSlideProps.targetforwards to the slide'sMaybeLink.TopBar—informationLinks[].targetforwards to eachNavigationMenuTextItem.
Default behaviour is unchanged when
targetis omitted (_self). -
Breaking: Replace
BrandListwith the genericAlphabeticalIndexcomponent.AlphabeticalIndexis an alphabetically grouped link list with a configurableheadingand an optional per-itemcount— usable for brands, glossaries, and similar A–Z indexes.Props changed:
// Before interface BrandListProps { brands: { name: string; href: string; count?: number }[]; } // After interface AlphabeticalIndexProps { heading?: string; items: { name: string; href: string; count?: number }[]; }
Upgrade: import from
#ui/components/AlphabeticalIndex/AlphabeticalIndex.vue, rename thebrandsprop toitems, and passheadingexplicitly (the old built-in"Brands"translation no longer renders automatically). -
Allow video in banner and media section/block media fields
The
media/backgroundImage/bannerImagefields of the banner and media sections and blocks now acceptimageandvideoassets (previously image-only). Studio authors can pick a video for these fields; rendering is handled by<Media>, which plays video through its built-in native renderer (or a consumer-registered renderer when one is provided).Affected: BlockBannerBasic, BlockBannerIntegrated, BlockBannerShowcase, BlockCategoryCard, SectionBannerBasic, SectionBannerIntegrated, SectionBannerShowcase, SectionBrandHero (background only), SectionMediaText, SectionProductSliderShowcase.
SectionPageNotFound stays image-only: it renders the asset as a CSS
background-image, not via<Media>, so a video could never play. -
Expose a configurable link
target(_self/_blank, default_self) in Studio across single-link Blocks and Sections. Editors can now choose whether each link opens in the same browsing context or a new window without dropping into custom code.- New shared field
linkTargetOptions(shared-fields/linkTarget.ts) bundles the canonical option list for re-use. - New
Targetschema field onBlockButton,BlockCategoryCard,BlockLogoSliderSlide. - New per-item
Targetfield onSectionBrandList.brandLinksandSectionTopBar.informationLinks. - New
Logo Link Targetfield onSectionHeaderBasicandSectionHeaderShop.
Existing configurations remain unchanged — the runtime default is
_self, matching prior behaviour. - New shared field
-
Add
BlockProductDetailRatingfor the Product Detail section. Renders the product's average star rating, the rating value (x/maxRating) and the review count viaRatingSummaryfrom ui-kit.- New non-standalone block, allowed in the
contentslot ofSectionProductDetail. productDetailQuerynow fetches theProductRatingcomponent so any PDP block can readproduct.components.rating({ average, count }).
- New non-standalone block, allowed in the
-
Add
SectionGlossaryListand refactorSectionBrandList. Both sections now render the sharedAlphabeticalIndexcomponent with a configurableheadingtext field. If the editor leavesheadingempty, the storefront falls back to a locale-aware default (sectionBrandList.heading/sectionGlossaryList.heading).SectionGlossaryListexposes name + link entries (no count);SectionBrandListkeeps the optional count.SectionGlossaryListships with a newglossaryQueryshared field (also exported from@laioutr-app/ui/shared-fields/glossaryQuery) that consumesGlossaryBase+GlossaryExcerptfrom@laioutr-core/canonical-types/entity/glossary. When the editor binds the field to a Hygraph data source (any app that registers aGlossaryentity, e.g.@laioutr-org/laioutr-gmbh__corporate@^1.4.0), entries are pulled from the CMS and mapped into the list. The manually authoreditemsarray is retained as a fallback for storefronts that don't bind a query.Breaking (
SectionBrandList): thebrandLinksschema field is renamed toitems, and a newheadingtext field has been added. Stored configurations referencing the oldbrandLinksname will not migrate automatically. -
Add
SectionLocationFinder,SectionLocationDetailandBlockLocationCard.SectionLocationFinderresolves locations from Orchestr via the new canonicalLocationentity (multi-query,LocationBase+LocationAddress+LocationCoordinates+LocationContact+LocationOpeningHours+LocationInfocomponents — the lightweightLocationInfo.coverfeeds each list-card thumbnail, so the finder never resolves the heavy gallery) and renders<LLocationFinder>with a configurable heading, container style, and Google Maps Map ID. The section also exposes alocationCardsslot that acceptsBlockLocationCardinstances — manually curated cards are merged into the samelocationsarray as the data-source results (one list, one map).BlockLocationCard(non-standalone — only usable insideSectionLocationFinder'slocationCardsslot) lets editors author individual cards by hand: name, image, address, latitude/longitude, phone, directions URL, and a details link. Cards without coordinates are silently dropped — the map marker has nothing to anchor against.SectionLocationDetailruns a single-entity query (also pullingLocationMediaandLocationContent) and renders the full<LLocationDetail>page — map, header with back-link, an image gallery (LocationMedia.imagesvia<ImageGallery>), info list (address, opening hours, website, phone, email) and a description section rendering the richLocationContent.descriptionHTML via<RichContent>.Both sections read the Google Maps API promise from
useNuxtApp().$googleMapsApi. Storefronts integrating the sections register a Nuxt plugin that exposes$googleMapsApi: Promise<typeof google>; until the plugin is registered the map area renders inert without crashing the rest of the section. -
Add
SectionPromotionBannerand register it in theBannerstemplate list. WrapsPromotionBanner(from@laioutr-core/ui) in aBackdropfor canonical margin / padding / containerStyle chrome.Schema follows
section-config-standard: top-level Content (heading, subline, icon, codeButton, cta, countdown*) + Design withstylingInfodivider (containerStyle, variant select, customColors object — gated byif: variant === 'custom') andlayoutInfodivider (marginField, paddingField, fill, mobile/desktop alignment). All shared-field presets used (containerStyleField,marginField,paddingField,buttonFields,defineSelectOptions).BlockPromotionBanneris not introduced — the promotion banner is configured directly on the Section, mirroring howSectionBannerBasic/SectionBannerShowcase/SectionBannerIntegratedwork.A short-lived
BlockPromotionBannerexisted earlier on this branch and was removed before merge; if you had stored configurations referencing it, they will not migrate. (Reach out before merging if that's a problem.)
-
Fix component props that were silently dropped because they didn't match the target component's API:
FeaturePillListpassedleft-icontoBadge(whose prop isicon-left), so pill icons never rendered.OpeningStatusIndicator,OpeningStatusDetail,LocationCard,LocationFinder, andLocationHeaderpassedvarianttoText, which has novariantprop. Headings usingvariant="heading"rendered with the defaultbodystyling.FilterBarboundv-model:opentoFilterOffCanvas, which exposesv-model:isOpen— the off-canvas filter panel could not be opened.PromotionBannerpassed an invalidtype="text"toButton(whosebuttonTypeis'button' | 'submit'); the dead attribute was removed.