attribute-tile.fluid
Renders: one feature tile — icon, name, optional space count — linking to/t/spaces?attributes={{ Attribute.Id }}. Used exclusively bysections/attributes.fluid.
Data: expects Attribute (from the caller's loop: Id, Name,SpaceCount) and the caller's Section (it scans Section.Blocks for anicon block whose Settings.attributeId == Attribute.Id, and readsSection.Settings.showCounts). Icon precedence: block image → block iconkey (via | attr_icon_key | raw) → automatic keyword match on the name (via| attr_icon | raw).
Preserve: the variable names Attribute and Section; the?attributes={id} link (feeds the spaces listing's validated server-sideattribute filter); attr_icon / attr_icon_key must be piped through| raw (they return SVG markup). Both filters read icon maps loaded fromschema/attribute-icons.json — ship that file (keys icons, keywords,default) or every tile falls back to the built-in check SVG.
category-card.fluid
Renders: a simple card with the category's emoji/icon, name and"From $X/{duration}". Used by sections/category-tabs.fluid.
Data: expects item (a category: Icon, Name, Price,DurationType); uses the dollar filter.
Preserve: variable name item; the card / card-body classes.
google-review-card.fluid
Renders: the body of one Google review card — initials avatar,name, time-ago, the Google "G" logo, 1–5 star row, quote. Shared by all threestyles of sections/google-reviews.fluid (the section supplies the outer.testimonial-card.gr-card... wrapper div).
Data: expects item (a review block): item.Settings.name,item.Settings.quote, item.Settings.rating (default 5),item.Settings.timeAgo.
Preserve: variable name item; it renders card contents only — thecaller owns the wrapper (that wrapper's .testimonial-card class is what thecarousel JS measures). gr-stars / gr-star--empty star classes and thearia-label="{{ rating }} out of 5 stars" accessibility line.
location-card.fluid
Renders: one location card — image (or map-pin placeholder), name,address, optional phone, "View {Spaces} →" — the whole card is a link to/t/locations/{{item.Name | url_encode}}/spaces. Used bysections/locations-strip.fluid and templates/locations.fluid.
Data: expects item (a location: Name, Address, Image,PhoneNumber); Organisation.SpaceName for the CTA.
Preserve: variable name item; the URL-encoded name-based link (thebackend resolves location slugs by name, spaces↔dashes tolerated). Phonerenders only when set — admins hide it by clearing the location's phonefield, no template change.
section-style.fluid
Renders: an attribute, not an element — it emitsstyle="…" for the section wrapper <div> when any of the six sharedstyle-override settings are set on the section. This is the application halfof ThemeV2SectionStyleOverrides (the schema half lives inAdminThemeV2Service.GetSectionSchemasAsync).
Data: Section.Settings.overrideTextScale, overrideTextColor,overrideHeadingColor, overrideBackgroundColor, overrideButtonColor,overrideButtonTextColor.
How it works: overrides re-scope the theme's global CSS variables on thewrapper, so everything inside the section that reads --text-color,--heading-color, --bg-color / --surface-color, --button-bg /--button-hover-bg, --button-text follows automatically — no per-sectionCSS:
{%- if so.overrideTextColor and so.overrideTextColor != "" -%}--text-color:{{ so.overrideTextColor }};color:{{ so.overrideTextColor }};{%- endif -%}
{%- if so.overrideBackgroundColor and so.overrideBackgroundColor != "" -%}--bg-color:{{ so.overrideBackgroundColor }};--surface-color:{{ so.overrideBackgroundColor }};background:{{ so.overrideBackgroundColor }};{%- endif -%}
Preserve: must be included inside the wrapper div's tag(<div data-alyta-section=…{% include 'section-style' %}>) on everysection-rendering template. It's written with whitespace-trimming tags({%- -%}) so it emits either a single clean style="…" or nothing. Acustom theme's components must read the CSS variables above (rather thanhard-coding colours) for the overrides to have any effect. Empty string means"inherit the global theme setting".
space-card.fluid
Renders: the standard space listing card — badge (Available/Occupied),image or placeholder, category + area / dimensions, attribute tags, priceblock (with full promotion strikethrough support), location line, and theCTA stack (View + Get a free quote). Used by the space-listing snippet (allspaces pages + embed), sections/space-grid.fluid andsections/featured-space.fluid.
Data: expects item (a ThemeV2SpaceData); also reads Settings(HidePricing, ShowBookOnline, ShowGetQuote), Organisation.SpaceName,Request.IsEmbed and the caller-set EmbedInlineDetail flag.
Preserve — this is the most contract-heavy file in the theme:
- Filter/sort data attributes on the root
<article class="space-card">— read byapplyFilters,applySort,monthlyPriceandinitSpaceGridSectionsin theme.js:
<article class="space-card"
data-location="{{item.LocationName}}"
data-category="{{item.CategoryName}}"
data-price="{{item.Price}}"
data-duration="{{item.DurationType}}"
data-width="{{item.Width}}"
data-length="{{item.Length}}"
data-attributes="{% for attr in item.Attributes %}{{ attr }}|{% endfor %}">
data-attributes is pipe-delimited (trailing | is fine — JS trims and filters empties). data-price + data-duration feed the monthly-equivalent price conversion used by the price bands and sorting; data-width/data-length feed size sorting. The JS also writes data-match="0|1" onto cards — don't use that name yourself.
- CTA hooks: - Detail CTA: on embed pages that set
EmbedInlineDetail(onlytemplates/embed-spaces.fluiddoes) render<button class="space-card__cta js-view-space">; everywhere else render a link to the UId:<a href="/t/spaces/{{item.UId}}" class="space-card__cta">.js-view-spacetriggersshowSpaceDetailInline()which clones.space-card__visual,.space-card__bodyand.space-card__price-blockfrom the clicked card — keep those class names or the inline detail view renders empty. - Quote CTA:class="js-quote-btn"withdata-space-id,data-name,data-category,data-price(omit whenSettings.HidePricing),data-duration—openQuoteModal()reads all five fromdataset(delegated document-level click handler, so the button works inside cloned/inline content too). - Both CTAs are gated bySettings.ShowBookOnline/Settings.ShowGetQuote. - Pricing rules: wrap all price markup in
{% unless Settings.HidePricing %}; render the promotion strikethrough layout only whenitem.PromotionPriceexists anditem.PromotionRuleType == "MinimumRequirement"; always render the plain price line otherwise (thedollarfilter falls back to$0.00— a priceless space must not collapse the block).PromotionPrice == 0rendersFREE<sup>*</sup>. - Area is
width × length({{ item.Width | times: item.Length | round: 0 }}m²), matching the detail page.
space-listing.fluid
Renders: the complete shared spaces listing — filter sidebar (Type select,price bands, dynamic Features checkboxes, active-filter pills, reset), mobilefilter toggle, results bar (live count + sort select), the card grid,client-side pagination and both empty states. Included bytemplates/spaces.fluid (full page) and templates/embed-spaces.fluid(chrome-less embed) so both share one implementation.
Data: Spaces, Categories, Organisation, Request(FilterCategoryId, FilterLocationName). Iterates Spaces with loopvariable item into {% include 'space-card' %}.
Preserve — ids and hooks (single instance per page; everything isid-based):
#filterSidebar+filter-sidebar--openclass — Consumer in theme.js:toggleMobileFilters().js-toggle-mobile-filters(close ✕ + mobile toggle button) — Consumer in theme.js: delegated click#resetAllFilters,.js-reset-filters— Consumer in theme.js:resetAllFilters()— also clears the URL-drivencategory/location/attributesparams and reloads#filterPills— Consumer in theme.js: rebuilt byupdateFilterPills()on every filter change#categorySelect— Consumer in theme.js: change →?category={id}URL navigation (server re-projects)input.js-filter-cbwithdata-filter="price"andvalue="min-max"— Consumer in theme.js: price bands; values0-100,100-200,200-500,500-99999are compared against the monthly-equivalent price#attributeFilterGroup(startsdisplay:none) +#attributeFilters— Consumer in theme.js:initSpaceFilters()builds Feature checkboxes here from carddata-attributes, un-hides the group when any exist#filterMobileToggle+#filterCount— Consumer in theme.js: active-filter badge#visibleCount— Consumer in theme.js: live results count#sort-selectwith option valuespriceAsc/priceDesc/sizeAsc/sizeDesc— Consumer in theme.js:applySort(); initial value from?sort=, persisted viahistory.replaceState#spaceList— Consumer in theme.js: the card container all filter/sort/paging code queries#spacePagination(startsdisplay:none) — Consumer in theme.js: client-side pager (9 per page,SPACE_PAGE_SIZE); its mere presence enables paging#noFilterResults(startsdisplay:none) — Consumer in theme.js: zero-results empty state
Also preserve: the emptiness test is {% if Spaces.size > 0 %} — an emptylist is truthy in Fluid, so {% if Spaces %} makes the no-spaces stateunreachable. The Type/category select is server-driven (URL navigation);price/feature filters are client-side. The no-spaces empty statedifferentiates on Request.FilterLocationName (location-scoped message + a"View All" link back to /t/spaces).