Skip to content

Instantly share code, notes, and snippets.

@screeny05
Created May 20, 2026 22:56
Show Gist options
  • Select an option

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

Select an option

Save screeny05/49e970444b10e1f0d092cff0780f1c2a to your computer and use it in GitHub Desktop.

Laioutr Core

0.30.1

  • e388d8e: Section slots prop now retains structural typing of block props at consumer call sites.

    Previously, props.slots.<slotName>[i].props was wrapped in SimplifyDeep, which recursively walked every nested block-schema decorator (headingStyle, sublineStyle, …). Past a certain schema depth the cumulative type instantiation exceeded TypeScript's 50-level limit and surfaced as TS2589: Type instantiation is excessively deep and possibly infinite in any section that mapped over its slot blocks.

    The wrapper now uses bounded Simplify (one level) at the right boundaries — the outer slot map, each block item, and the block's props shape. IDE hover output is unchanged; structural typing is unchanged. Sections that previously needed an as unknown as ReadonlyArray<{ props: Record<string, any> }> cast on props.slots.* to silence TS2589 can drop the cast and access block.props.<field> with full type inference.

    No runtime change. No API change.

  • 0861f3e: Add laioutr:beforeModuleRegister Nuxt hook. Fires before the ui-kit module registers each upstream Nuxt module, with { name, key, options }. Consumers can mutate options to override defaults applied by registerModule.

    // nuxt.config.ts
    export default defineNuxtConfig({
      hooks: {
        'laioutr:beforeModuleRegister': ({ name, options }) => {
          if (name === '@nuxt/icon') {
            options.foobar = {
              ...options.foobar,
              customProvider: {
                /* … */
              },
            };
          }
        },
      },
    });

Laioutr Core Types

0.30.1

  • 705ad15: Expand the Media type family with audio support and richer video metadata.

    Additions

    • MediaAudio (type: 'audio') — new variant of Media for playable audio assets, with sources: MediaSourceAudio[], optional cover?: MediaImage (album art / podcast cover), and optional tracks?: MediaTextTrack[] (chapters, transcripts).
    • MediaSourceAudio — single audio source with provider, src, optional format (MIME), length, and streaming.
    • MediaTextTrack — timed text track (subtitles, captions, descriptions, chapters, metadata) for MediaVideo and MediaAudio, mapping to the HTML <track> element.
    • MediaSourceVideo.streaming?: 'progressive' | 'hls' | 'dash' — delivery-format discriminator. progressive is assumed when omitted.
    • MediaVideo.tracks?: MediaTextTrack[] — subtitle/caption/chapter tracks for the video.
    • Studio media fields can now use allowedTypes: ['audio'], which narrows the field value to MediaAudio.

    Breaking: MediaVideo.preview renamed to MediaVideo.poster

    Matches the HTML <video poster="…"> attribute and the customer-facing terminology used in Studio.

    // Before
    const video: MediaVideo = {
      type: 'video',
      sources: [...],
      preview: posterImage,
    };
    
    // After
    const video: MediaVideo = {
      type: 'video',
      sources: [...],
      poster: posterImage,
    };

    Breaking: Media union widened to include MediaAudio

    Code that exhaustively switches on media.type without a default branch will need to handle 'audio':

    switch (media.type) {
      case 'image':
        return renderImage(media);
      case 'video':
        return renderVideo(media);
      case 'audio':
        return renderAudio(media); // new — handle or fall through to default
    }

Laioutr UI

2.2.0

Minor Changes

  • b5ed67d: Footer accepts two new optional props: logoPosition?: 'top' | 'bottom' (default 'top', current behavior) and logoAlignment?: 'left' | 'center' | 'right' (default 'left', only meaningful when logoPosition === 'bottom').

    When logoPosition === 'bottom':

    • The top logo-column is hidden entirely (logo image + contact-info slot). Menus span the full top row.
    • The logo image renders inside the bottom bar between the icons cluster (lower email + social icons, left) and the copyright (right). At sm+ the bottom bar becomes a 3-cell grid 1fr auto 1fr; logoAlignment controls the logo's justify-self within the auto cell.
    • On mobile (column flex) the logo sits in its own row above the copyright, with justify-content driven by logoAlignment.

    Three new Storybook stories cover LogoBottomCenter, LogoBottomLeft, LogoBottomRight.

  • bfce22f: Editors can now pick a button size per CTA across every section and block.

    • buttonFields (from shared-fields/button.ts) exposes a new Size selector alongside Label / Link / Variant, with options xs / s / m / l. Default is 'm', so existing CTAs render unchanged unless an editor explicitly picks a different size.
    • New buttonSizeOptions export from shared-fields/buttonSize.ts for sections or blocks that need an inline button-size select outside of buttonFields (mirrors buttonVariantOptions).
    • Every section and block that surfaces a CTA — including the banner family, product/category/content sliders, hero slider, header, page-not-found, plan-card slider, and the corresponding standalone blocks — now respects the editor-chosen size.

    For ui consumers building their own CTA shapes, the following components accept an optional size on each CTA entry (defaults preserve their previous hardcoded rendering):

    • HeroSlide, PageNotFound, BannerShowcase, ContentGrid (ui)
    • HeaderBasic.ctaButtons[], ContentSlider/CategoryCardSlider/ProductSlider cta types (ui)
    • PlanCard exposes new optional ctaVariant / ctaSize props
    • SwiperChrome.buttons[] accepts size?: ButtonSize (ui-kit)
  • 0506962: Forward the new sectionBackground (and customSectionBackground) props through the ui-layer wrappers that route to Backdrop:

    • Container accepts sectionBackground?: BackdropBackground and customSectionBackground?: string, resolves them, and forwards to <Backdrop :section-background="...">.
    • MediaText accepts sectionBackground?: BackdropBackground and forwards to <Backdrop :section-background="...">.

    Both default to 'none', so existing consumers render unchanged.

  • fc5c0f9: Fix FilterOffCanvas / FilterModal filter selections never reaching the parent's selectedFilters.

    FilterPanelContent bound all four inner filter components via :checked / @update:checked, but only FilterOffCanvasSwatchList still used a 'checked' v-model channel — the other three (FilterOffCanvasCheckboxList, FilterOffCanvasRangeSlider, FilterOffCanvasSwitchItem) had moved to the canonical modelValue channel, so :checked fell through to their root elements as a raw HTML attribute and @update:checked was never emitted. Verified at runtime in Storybook: brand checkboxes did not toggle, the price-range slider received checked="[object Object]" as a string attribute, and the boolean stock-filter switch toggled its own visual state but never propagated. The boolean branch additionally passed :title="filter.label" to FilterOffCanvasSwitchItem, whose prop is label, so the switch's text label rendered empty.

    • FilterOffCanvasSwatchList: v-model channel renamed from 'checked' to the default modelValue, with { default: () => [] }. Drops the now-unnecessary null guard in the toggle handler. Consumers using v-model:checked="…" must switch to v-model="…" (or v-model:model-value="…").
    • FilterPanelContent: every inner-item binding rewritten to :model-value / @update:model-value. The boolean / non-boolean branches were split into distinct <FilterOffCanvasSwitchItem> and <FilterOffCanvasAccordionItem> elements instead of a <component :is> union, fixing the :title/:label mismatch and removing dead :icon / :value / :active-value props that SwitchItem did not declare.

    Matches the v-model contract from DECISIONS.md (modelValue / update:modelValue for form inputs).

  • f195c25: Editor: every section AND block with a configurable heading or subline now exposes an HTML-element selector directly next to the field via the existing as: 'style' decorator popup — no more separate "SEO" panel at the bottom.

    • Heading options: H1H6 / DIV
    • Subline options: P / H1H6 / DIV

    Sections — newly available (13): SectionBannerBasic, SectionBannerShowcase, SectionBannerIntegrated, SectionBrandHero, SectionCategoryCardSlider, SectionCategoryCardGrid, SectionContentGrid, SectionContentSlider, SectionEditorialGrid, SectionLogoGrid, SectionLogoSlider, SectionNewsletterRegistration, SectionPageHero, SectionPageNotFound. Section defaults: heading → h2, subline → div (was implicit p; sections render subline as a plain block now by default).

    Sections — migrated (2): SectionProductSlider, SectionProductSliderShowcase lose their dedicated "SEO" fieldset; headingElement is now stored at headingStyle.element (object decorator path) instead of top-level headingElement. Action required: anyone with stored headingElement values on these two sections must re-pick the heading element in Studio once.

    Blocks — newly available (7): BlockBannerBasic, BlockBannerShowcase, BlockBannerIntegrated, BlockCard, BlockHeroSliderSlide, BlockProductDetailVariantSelectionConfigurator, BlockText. Block defaults: heading → h3, subline → h4 (one level below their hosting section's heading).

    Naming: ui-component props use the Reka convention as instead of tag. The underlying components now accept headingAs? / sublineAs? props (renamed from headingTag? / sublineTag?). Template attributes are :heading-as / :subline-as. The ui-kit types follow suit: TextGroupHeadingAs / TextGroupSublineAs (previously *Tag). TextGroupSublineAs now includes 'h1' so editors can override a subline to a page-level heading when SEO requires it.

    ui changes: BannerBasic, BannerShowcase, BannerIntegrated, BrandHero, CategoryCardSlider, CategoryCardGrid, ContentGrid, ContentSlider, EditorialGrid, HeroSlide, LogoGrid, LogoSlider, NewsletterRegistration, PageHero, PageNotFound, ProductSlider, ProductSliderShowcase, VariantSelectorConfigurator accept new optional headingAs / sublineAs props (additive). Where a component renders heading/subline manually instead of via TextGroup, the props drive <component :is> to pick the element at render time.

    ui-kit changes:

    • TextGroup: prop rename headingTagheadingAs, sublineTagsublineAs; type rename TextGroupHeadingTagTextGroupHeadingAs, TextGroupSublineTagTextGroupSublineAs. TextGroupSublineAs widened to include 'h1'.
    • SwiperChrome: same prop rename plus added optional sublineAs forwarding to its internal TextGroup.
    • Card: new optional headingAs? / sublineAs? props forwarded to LTextGroup.
    • AlertDialog: new optional titleAs / descriptionAs props (forwarded from DialogStore.DialogOptions); the inner <Text> under DialogTitle / DialogDescription uses as-child so the emitted tag follows Text's as.

    Breaking (ui-kit): the prop and type rename on TextGroup is a minor-version break in the public API for any external consumer that imports TextGroupHeadingTag / TextGroupSublineTag or binds :heading-tag / :subline-tag directly. Update to the new names.

    Internal: shared-fields/headingElement.ts exports headingElementOptions and sublineElementOptions; the top-level headingElementField is removed (only consumed by ProductSlider/Showcase, both migrated to the nested decorator).

  • 6b16c11: HeroSlide accepts two new optional props: headingSize?: 's' | 'm' | 'l' and sublineSize?: 's' | 'm' | 'l', forwarded to the underlying <Text> elements. Defaults preserve previous rendering (headingSize: 'l', sublineSize: 'm').

  • 8e8168b: Adds opening-hours primitives and the location-card domain components.

    @laioutr-core/ui-kit — opening-hours primitives:

    • useOpeningStatus(openingHours, now) composable — reactive isOpen + next state-change event (closes-at / opens-today / opens-tomorrow / opens-on-weekday / opens-on-date) across a 30-day horizon; respects IANA timezone and one-off date exceptions.
    • OpeningStatusIndicator (atom) — open/closed pill driven by the composable.
    • OpeningStatusDetail (atom) — localized one-liner for the next state change.
    • OpeningStatus (molecule) — Indicator + Detail combined.
    • OpeningHoursWeeklyTable (molecule) — weekly schedule rendered as a DescriptionList, grouping consecutive same-hours days ("Monday – Wednesday" etc.).
    • New i18n keys: openingStatus.*, openingHoursWeeklyTable.*, and locationCard.* (the last consumed cross-package by @laioutr-core/ui).

    @laioutr-core/ui — location domain components:

    • LocationCard — list / map-popup variants, composes the ui-kit opening-hours primitives.
    • PaymentMethodList — payment-method chips for location detail views.

    The opening-hours primitives live in ui-kit because OpeningHours is a Schema.org-style general concept (offices, doctors, restaurants — not commerce-specific). The location-card components live in ui because they encode commerce/storefront placement decisions.

  • 328ee9d: - HeaderBasicMenuMenuBasic. The component was a generic basic navigation menu (used by both SectionHeaderBasic and SectionHeaderShop), not a sub-component of HeaderBasic. Moved out of the HeaderBasic/ folder into its own MenuBasic/ folder per the parent-prefix-naming rule.

    • Exported type HeaderPropsMenuBasicProps.
    • Exported type MenuItemMenuBasicItem.
    • Root CSS class .header-basic-menu.menu-basic (parent-scoped rule .s-header-shop__desktop-nav .menu-basic updated accordingly).
    • BlockMenuHeaderBasicBlockMenuBasic. The wrapping block follows the new component name per the Block<Name> convention.
      • defineBlock({ component: 'BlockMenuHeaderBasic' }) string → 'BlockMenuBasic'stored project configurations that reference this block by name need a one-time migration.
      • Studio label 'Basic Header Menu''Basic Menu'. Tags extended with 'Navigation'.

    Consumer imports updated:

    • SectionHeaderBasic, SectionHeaderShop: BlockMenuHeaderBasicDefinitionBlockMenuBasicDefinition, restrictTo updated.
    • BlockMenuBasic itself: HeaderBasicMenu import → MenuBasic.

    No template / runtime behavior change beyond the rename. Lint + typecheck clean.

  • 8464f9e: HeroSlide accepts two new optional props: headingTextShadow?: 'none' | 'soft' | 'strong' and sublineTextShadow?: 'none' | 'soft' | 'strong'. When set, BEM modifier classes (slide-card__heading--text-shadow-{value}, slide-card__subline--text-shadow-{value}) apply a layered text-shadow for legibility against busy hero backgrounds. Defaults to 'none', so existing slides render unchanged. The shadow values resolve from the global --text-shadow-soft / --text-shadow-strong custom properties shipped by ui-kit. Marked TODO FIGMA until dedicated tokens land in ui-tokens. The new textShadow prop on CaptionFlag is also forwarded for slide captions rendered as flag chips.

  • 176d022: NewsletterRegistration accepts two new optional props: caption?: string (rendered as a <Text type="caption" size="s"> above the heading inside the existing headings group) and body?: string (rendered as a <RichContent> block between the headings group and the form). Mirrors the caption + body fields exposed by BlockText. Both props are unset by default, so existing consumers render identically.

    Breaking: the image?: MediaImage prop is removed in favor of a new media slot. Right-side / top-on-mobile media area is now consumer-provided via <template #media>...</template> instead of a single image — Sections that wrap this component decide what goes there (media, text, card, button, …). The legacy theme.image('newsletterRegistrationTeaser') fallback is gone; if no media is provided, the content side takes the full width. New Storybook story WithMediaSlot shows the pattern.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment