Skip to content

Instantly share code, notes, and snippets.

@screeny05
Created June 9, 2026 07:29
Show Gist options
  • Select an option

  • Save screeny05/f4ff6f98f76644a183b4a654860f1130 to your computer and use it in GitHub Desktop.

Select an option

Save screeny05/f4ff6f98f76644a183b4a654860f1130 to your computer and use it in GitHub Desktop.
Frontend-Core 0.31.0 & Laioutr UI 2.3.0

Release notes

Core 0.31.0

Minor Changes

  • Render referenced global sections on pages (dereference + query merge + config wiring).

  • Add new Energy Label Model

    Canonical types (new): ProductVariantEnergyLabel entity 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 to gtin, 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.

Patch Changes

  • Add CalendarDate value type — an ISO YYYY-MM-DD calendar 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.

UI 2.3.0

Minor Changes

  • Add sectionBrandList.heading and sectionGlossaryList.heading translation keys (EN: "Brands"/"Glossary", DE: "Marken"/"Glossar"). These act as the locale-aware fallback heading when an editor leaves the section's heading field empty.

    Breaking: Remove the orphaned brandGrid.title translation key — it is no longer referenced anywhere. Consumers that overrode this key in custom locales can delete the override.

  • Add target?: string prop to LinkTile, LinkTileBasic, LinkTileCompact, LinkTileBig, and NavigationMenuTextItem. The new prop is forwarded to the underlying NuxtLink / MaybeLink, allowing consumers to open the link in a new browsing context (_blank) while keeping _self as 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>) and MediaAudio (native <audio> with the optional cover shown above it). A Media value 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 (default true), autoplay, muted, loop, playsinline (default false). These are deliberately not part of the Media value: 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 media object, 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.

    MediaStage now 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).

    MediaVideo suppresses autoplay when the user prefers reduced motion (prefers-reduced-motion: reduce), settling on the poster frame 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 Countdown component (#ui-kit/components/Countdown/Countdown.vue) and a useCountdown composable for editorial countdowns. Unit labels come from Intl.NumberFormat, so they are localized and plural-aware automatically; the unitDisplay prop ('long' | 'short' | 'narrow') sets their verbosity. Countdown also takes an optional frozen now (for tests/Storybook), and a pure computeCountdown(endDate, now) helper is exported. Adds countdown.expired and promotionBanner.{codeCopiedTitle,codeCopiedSubline,copyCodeAriaLabel} locale entries (EN + DE).

    useNow now accepts an optional tick interval — useNow(intervalMs = 60_000) — and is seeded via useState, so the shared clock renders byte-identically across SSR and hydration; consumers no longer need data-allow-mismatch on time-dependent nodes. Same-interval callers share one clock; Countdown ticks on useNow(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 exports PromotionBanner/types — consumers should import from @laioutr-core/ui instead.

  • Pagination now emits SEO-correct sequence semantics when hrefTemplate is 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> (no href); 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, Button and IconButton gain a rel?: string prop. It is forwarded to the underlying NuxtLink only when the component renders as a link (href set), and ignored when rendered as a <button>. Accepts any valid HTML rel token or space-separated combination — e.g. 'prev', 'next', 'noopener', 'noopener noreferrer', 'external nofollow'.

  • Add BlockProductDetailEnergyLabel to 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): EnergyLabel component renders the badge image, lightbox trigger, and optional data sheet link. Props: badge: Media, label: Media, title?: string, dataSheetLink?: string, plus width / height for the badge image. When title is omitted, the alt text falls back to the pdp.energyLabel translation.

    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.vue queries the product's variants link for ProductVariantEnergyLabel and renders the label of the selected variant (resolved via useProductVariantContext, 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 ProductVariantEnergyLabel component 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-kit because promo-codes + checkout language are commerce-domain). Accepts headings, optional countdown (via useCountdown from ui-kit), a promo-code copy button, a CTA, surface preset (default | pale | bright | solid) or custom colors with per-slot overrides (background / text / countdown / icon). Story title: UI/Sections/PromotionBanner.

    Internal:

    • Auto-promotes variant to 'custom' when any customColors field 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/ui dependency.

  • Plumb target?: string through link-rendering components so consumers can choose the browsing context of editorial links:

    • BrandListBrandListItem.target carries through to the per-brand NuxtLink.
    • HeaderBasic — new logoTarget prop on the logo link.
    • HeaderShop — new logoTarget prop on the logo link.
    • LogoSlider / LogoGrid / LogoSliderSlideLogoSliderSlideProps.target forwards to the slide's MaybeLink.
    • TopBarinformationLinks[].target forwards to each NavigationMenuTextItem.

    Default behaviour is unchanged when target is omitted (_self).

  • Breaking: Replace BrandList with the generic AlphabeticalIndex component. AlphabeticalIndex is an alphabetically grouped link list with a configurable heading and an optional per-item count — 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 the brands prop to items, and pass heading explicitly (the old built-in "Brands" translation no longer renders automatically).

  • Allow video in banner and media section/block media fields

    The media / backgroundImage / bannerImage fields of the banner and media sections and blocks now accept image and video assets (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 Target schema field on BlockButton, BlockCategoryCard, BlockLogoSliderSlide.
    • New per-item Target field on SectionBrandList.brandLinks and SectionTopBar.informationLinks.
    • New Logo Link Target field on SectionHeaderBasic and SectionHeaderShop.

    Existing configurations remain unchanged — the runtime default is _self, matching prior behaviour.

  • Add BlockProductDetailRating for the Product Detail section. Renders the product's average star rating, the rating value (x/maxRating) and the review count via RatingSummary from ui-kit.

    • New non-standalone block, allowed in the content slot of SectionProductDetail.
    • productDetailQuery now fetches the ProductRating component so any PDP block can read product.components.rating ({ average, count }).
  • Add SectionGlossaryList and refactor SectionBrandList. Both sections now render the shared AlphabeticalIndex component with a configurable heading text field. If the editor leaves heading empty, the storefront falls back to a locale-aware default (sectionBrandList.heading / sectionGlossaryList.heading). SectionGlossaryList exposes name + link entries (no count); SectionBrandList keeps the optional count.

    SectionGlossaryList ships with a new glossaryQuery shared field (also exported from @laioutr-app/ui/shared-fields/glossaryQuery) that consumes GlossaryBase + GlossaryExcerpt from @laioutr-core/canonical-types/entity/glossary. When the editor binds the field to a Hygraph data source (any app that registers a Glossary entity, e.g. @laioutr-org/laioutr-gmbh__corporate@^1.4.0), entries are pulled from the CMS and mapped into the list. The manually authored items array is retained as a fallback for storefronts that don't bind a query.

    Breaking (SectionBrandList): the brandLinks schema field is renamed to items, and a new heading text field has been added. Stored configurations referencing the old brandLinks name will not migrate automatically.

  • Add SectionLocationFinder, SectionLocationDetail and BlockLocationCard.

    SectionLocationFinder resolves locations from Orchestr via the new canonical Location entity (multi-query, LocationBase + LocationAddress + LocationCoordinates + LocationContact + LocationOpeningHours + LocationInfo components — the lightweight LocationInfo.cover feeds 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 a locationCards slot that accepts BlockLocationCard instances — manually curated cards are merged into the same locations array as the data-source results (one list, one map).

    BlockLocationCard (non-standalone — only usable inside SectionLocationFinder's locationCards slot) 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.

    SectionLocationDetail runs a single-entity query (also pulling LocationMedia and LocationContent) and renders the full <LLocationDetail> page — map, header with back-link, an image gallery (LocationMedia.images via <ImageGallery>), info list (address, opening hours, website, phone, email) and a description section rendering the rich LocationContent.description HTML 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 SectionPromotionBanner and register it in the Banners template list. Wraps PromotionBanner (from @laioutr-core/ui) in a Backdrop for canonical margin / padding / containerStyle chrome.

    Schema follows section-config-standard: top-level Content (heading, subline, icon, codeButton, cta, countdown*) + Design with stylingInfo divider (containerStyle, variant select, customColors object — gated by if: variant === 'custom') and layoutInfo divider (marginField, paddingField, fill, mobile/desktop alignment). All shared-field presets used (containerStyleField, marginField, paddingField, buttonFields, defineSelectOptions).

    BlockPromotionBanner is not introduced — the promotion banner is configured directly on the Section, mirroring how SectionBannerBasic / SectionBannerShowcase / SectionBannerIntegrated work.

    A short-lived BlockPromotionBanner existed 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.)

Patch Changes

  • Fix component props that were silently dropped because they didn't match the target component's API:

    • FeaturePillList passed left-icon to Badge (whose prop is icon-left), so pill icons never rendered.
    • OpeningStatusIndicator, OpeningStatusDetail, LocationCard, LocationFinder, and LocationHeader passed variant to Text, which has no variant prop. Headings using variant="heading" rendered with the default body styling.
    • FilterBar bound v-model:open to FilterOffCanvas, which exposes v-model:isOpen — the off-canvas filter panel could not be opened.
    • PromotionBanner passed an invalid type="text" to Button (whose buttonType is 'button' | 'submit'); the dead attribute was removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment