StoreHeader

The top of a restaurant or store page: a cover, a logo, the name, a rating, a line of short facts, the delivery time and fee, and whether the store is open.

Commerce StoreHeader.jsx StoreHeader.d.ts StoreHeader.md

Playground

Change a prop and watch it. The code below follows along.

Install and import

StoreHeader is in @dovetail-ds/react. Install it with React, if you haven't:

npm install @dovetail-ds/react react react-dom
import { StoreHeader } from "@dovetail-ds/react";

Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.

Guidelines

The top of a restaurant or store page: a cover, a logo, the name, a rating, a line of short facts, the delivery time and fee, and whether the store is open.

Use it when

  • Opening a store's own page in a food-ordering or retail app, above the FulfilmentToggle and the menu.
  • A store needs to say at a glance whether it takes orders now, how long delivery takes and what it costs.

Don't use it when

  • Listing many stores in a feed or a search result. Use Card with a media image; a header is for one store's page.
  • The page is about a product, not a store. Use Heading and Price.
  • You need a marketing hero. Use HeroBlock or Cover.

Example

<StoreHeader
  name="Bangkok Kitchen"
  image={{ src: cover, alt: "" }}
  logo={{ src: logo, alt: "" }}
  rating={{ value: 4.6, count: 1284 }}
  meta={["Thai", "$$", "1.2 mi"]}
  deliveryTime="25–35 min"
  deliveryFee={2.99}
  status={{ open: true, label: "Open until 10pm" }}
  actions={<IconButton label="Save to favourites"><HeartIcon /></IconButton>}
  locale="en-US"
/>

{/* Closed, with free delivery */}
<StoreHeader name="Bangkok Kitchen" deliveryFee={0} status={{ open: false, label: "Closed · opens 11am" }} />

Variants

PropWhat it is for
imageThe cover, 3:1 and no taller than --dt-store-cover-max-height. Its space is reserved before it loads, so the page does not jump; without a src the empty surface shows. Omit it for no cover.
logoA square tile. With a cover it overlaps the cover's lower edge on a ring of the page surface; without one it sits above the name.
ratingA small display Rating with the review count.
metaThree or four short facts separated by middle dots. A line never starts or ends with a dot when it wraps.
deliveryTime, deliveryFeeThe time with a clock icon; the fee through Price, "$2.99 delivery", or freeDeliveryLabel at 0. Leave the fee out for pickup.
statusopen: false greys the cover and logo, lays the scrim over the cover and turns the label the danger colour. The label says it in words.
actionsFavourite, share and similar IconButtons, beside the name.
headingLevelThe name's heading level, 1 by default.

Composition

Sits at the top of a store page, usually inside an AppShell body on a phone, followed by a FulfilmentToggle and the MenuSections. The fee reads Price and the rating reads Rating, so they follow those components' tokens. Actions are whatever you pass; keep them to two.

Tokens

Tier 3, in tokens/component/commerce.css; the colour ones are repeated under .dark:

  • --dt-store-cover-bg (--dt-surface-sunken), --dt-store-cover-radius (--dt-radius-media), --dt-store-cover-max-height (--dt-size-media-min).
  • --dt-store-logo-size (--dt-size-avatar-xl), --dt-store-logo-radius (--dt-radius-media), --dt-store-logo-bg (--dt-surface-raised), --dt-store-logo-ring (--dt-surface-base).
  • --dt-store-meta-color (--dt-text-secondary), --dt-store-open-color (--dt-text-success), --dt-store-closed-color (--dt-text-danger), --dt-store-closed-scrim (--dt-surface-scrim).

The name is --dt-text-heading-md-*; the facts are --dt-text-body-sm-*.

Accessibility

  • The name is a real heading (h1 by default). Set headingLevel to fit the page's outline.
  • Status is announced as its label ("Closed · opens 11am"). The greyed cover and the colour repeat it; they never carry it alone.
  • The middle dots are aria-hidden, so the facts read as a sequence of words. The rating reads as "4.6 out of 5 stars, 1,284 reviews".
  • Give the cover and logo empty alt when they only decorate; describe them when they show something the text does not.
  • Each action is yours to label: pass IconButtons with a label.

Content

  • name as the store writes it.
  • meta items are one or two words each: cuisine, price band, distance.
  • status.label is sentence case and says what happens next: "Open until 10pm", "Closed · opens 11am", "Busy · orders may be late".
  • deliveryTime is a range with an en dash and a unit: "25–35 min".

Props

The typed contract, from StoreHeader.d.ts.

import * as React from "react";

/** An image with its alternative text. */
export interface StoreHeaderImage {
  /** Image URL. Without one the reserved space shows the empty surface. */
  src?: string;
  /** Alternative text. Pass "" when the picture is decoration and says nothing the name does not. */
  alt: string;
}

/** Whether the store takes orders now, and the words for it. */
export interface StoreHeaderStatus {
  /** false greys the cover and logo, lays the cover under a scrim and turns the label the danger colour. */
  open: boolean;
  /** What to show, and what a screen reader hears: "Open until 10pm", "Closed · opens 11am". */
  label: string;
}

/** The top of a restaurant or store page: cover, logo, name, rating, facts, delivery and opening status. */
export interface StoreHeaderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
  /** The store's name, rendered as the heading. */
  name: string;
  /** Cover image. Its space (3:1, at most --dt-store-cover-max-height tall) is reserved before it loads. Omit for no cover. */
  image?: StoreHeaderImage;
  /** Square logo. With a cover it overlaps the cover's lower edge; without one it sits above the name. */
  logo?: StoreHeaderImage;
  /** Average rating and review count, shown as a small Rating. */
  rating?: { value: number; count?: number };
  /** Short facts after the rating, separated by middle dots: "Thai", "$$", "1.2 mi". */
  meta?: string[];
  /** Delivery or preparation time, shown with a clock: "25–35 min". */
  deliveryTime?: string;
  /** Delivery fee in major units, shown with Price. 0 shows freeDeliveryLabel. Omit to show no fee. */
  deliveryFee?: number;
  /** ISO 4217 currency code for the fee. @default "USD" */
  currency?: string;
  /** BCP 47 locale for the fee and the review count. Defaults to the runtime's; set it when server rendering. */
  locale?: string;
  /** Opening status. A closed store dims and says so. */
  status?: StoreHeaderStatus;
  /** Buttons beside the name, such as favourite and share IconButtons, each with its own label. */
  actions?: React.ReactNode;
  /** Level of the name's heading element. Use 1 when the header tops the store's own page. @default 1 */
  headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
  /** Shown instead of a fee of 0. @default "Free delivery" */
  freeDeliveryLabel?: string;
  /** Shown after a non-zero fee, as its unit: "$2.99 delivery". @default "delivery" */
  deliveryFeeLabel?: string;
}

export declare function StoreHeader(props: StoreHeaderProps): React.JSX.Element;

Tokens it reads

Every custom property StoreHeader.jsx resolves, read from the source: 11 component, 162 semantic, 1 primitive. Component tier first, because that is the one to reach for.

  • component Its own tier. Override one of these and only this component moves.
  • semantic Shared roles. Override one and everything using that role moves with it.
  • primitive Raw values. A component reading one directly is a deliberate exception.
TokenTierDeclared as
--dt-store-closed-colorcomponentvar(--dt-text-danger)
--dt-store-closed-scrimcomponentvar(--dt-surface-scrim)
--dt-store-cover-bgcomponentvar(--dt-surface-sunken)
--dt-store-cover-max-heightcomponentvar(--dt-size-media-min)
--dt-store-cover-radiuscomponentvar(--dt-radius-media)
--dt-store-logo-bgcomponentvar(--dt-surface-raised)
--dt-store-logo-radiuscomponentvar(--dt-radius-media)
--dt-store-logo-ringcomponentvar(--dt-surface-base)
--dt-store-logo-sizecomponentvar(--dt-size-avatar-xl)
--dt-store-meta-colorcomponentvar(--dt-text-secondary)
--dt-store-open-colorcomponentvar(--dt-text-success)
--dt-border-subtlesemanticvar(--dt-color-neutral-100)
--dt-border-width-defaultsemanticvar(--dt-dim-hair)
--dt-border-width-strongsemanticvar(--dt-dim-hair-2)
--dt-radius-pillsemanticvar(--dt-radius-raw-full)
--dt-size-icon-smsemanticvar(--dt-dim-4)
--dt-size-icon-xssemanticvar(--dt-dim-3)
--dt-space-inline-2xssemanticvar(--dt-dim-1)
--dt-space-inline-mdsemanticvar(--dt-dim-4)
--dt-space-inline-smsemanticvar(--dt-dim-3)
--dt-space-inset-mdsemanticvar(--dt-dim-4)
--dt-space-stack-2xssemanticvar(--dt-dim-1)
--dt-space-stack-smsemanticvar(--dt-dim-3)
--dt-text-artboard-body-linesemanticcalc(var(--dt-line-height-md) * 2.35)
--dt-text-artboard-body-sizesemanticcalc(var(--dt-font-size-md) * 2.5)
--dt-text-artboard-display-familysemanticvar(--dt-text-display-lg-family)
--dt-text-artboard-display-linesemanticcalc(var(--dt-line-height-7xl) * 2.55)
--dt-text-artboard-display-sizesemanticcalc(var(--dt-font-size-7xl) * 2.8)
--dt-text-artboard-display-trackingsemanticvar(--dt-tracking-tightest)
--dt-text-artboard-display-weightsemanticvar(--dt-text-display-lg-weight)
--dt-text-artboard-meta-sizesemanticcalc(var(--dt-font-size-sm) * 2.2)
--dt-text-artboard-meta-trackingsemanticvar(--dt-tracking-wider)
--dt-text-artboard-title-linesemanticcalc(var(--dt-line-height-7xl) * 1.7)
--dt-text-artboard-title-sizesemanticcalc(var(--dt-font-size-7xl) * 1.75)
--dt-text-body-lg-familysemanticvar(--dt-font-family-sans)
--dt-text-body-lg-linesemanticvar(--dt-line-height-lg)
--dt-text-body-lg-sizesemanticvar(--dt-font-size-lg)
--dt-text-body-lg-trackingsemanticvar(--dt-tracking-normal)
--dt-text-body-lg-weightsemanticvar(--dt-font-weight-regular)
--dt-text-body-md-familysemanticvar(--dt-font-family-sans)
--dt-text-body-md-linesemanticvar(--dt-line-height-md)
--dt-text-body-md-sizesemanticvar(--dt-font-size-md)
--dt-text-body-md-trackingsemanticvar(--dt-tracking-normal)
--dt-text-body-md-weightsemanticvar(--dt-font-weight-regular)
--dt-text-body-sm-familysemanticvar(--dt-font-family-sans)
--dt-text-body-sm-linesemanticvar(--dt-line-height-sm)
--dt-text-body-sm-sizesemanticvar(--dt-font-size-sm)
--dt-text-body-sm-trackingsemanticvar(--dt-tracking-normal)
--dt-text-body-sm-weightsemanticvar(--dt-font-weight-regular)
--dt-text-body-xs-familysemanticvar(--dt-font-family-sans)
--dt-text-body-xs-linesemanticvar(--dt-line-height-xs)
--dt-text-body-xs-sizesemanticvar(--dt-font-size-xs)
--dt-text-body-xs-trackingsemanticvar(--dt-tracking-normal)
--dt-text-body-xs-weightsemanticvar(--dt-font-weight-regular)
--dt-text-brandsemanticvar(--dt-color-primary-700)
--dt-text-brand-secondarysemanticvar(--dt-color-secondary-700)
--dt-text-code-md-familysemanticvar(--dt-font-family-mono)
--dt-text-code-md-linesemanticvar(--dt-line-height-sm)
--dt-text-code-md-sizesemanticvar(--dt-font-size-sm)
--dt-text-code-md-trackingsemanticvar(--dt-tracking-normal)
--dt-text-code-md-weightsemanticvar(--dt-font-weight-regular)
--dt-text-code-sm-familysemanticvar(--dt-font-family-mono)
--dt-text-code-sm-linesemanticvar(--dt-line-height-xs)
--dt-text-code-sm-sizesemanticvar(--dt-font-size-xs)
--dt-text-code-sm-trackingsemanticvar(--dt-tracking-normal)
--dt-text-code-sm-weightsemanticvar(--dt-font-weight-regular)
--dt-text-dangersemanticvar(--dt-color-red-800)
--dt-text-disabledsemanticvar(--dt-color-neutral-400)
--dt-text-display-2xl-familysemanticvar(--dt-font-family-sans)
--dt-text-display-2xl-linesemanticvar(--dt-line-height-fluid)
--dt-text-display-2xl-sizesemanticvar(--dt-font-size-fluid-2xl)
--dt-text-display-2xl-trackingsemanticvar(--dt-tracking-tightest)
--dt-text-display-2xl-weightsemanticvar(--dt-font-weight-medium)
--dt-text-display-lg-familysemanticvar(--dt-font-family-sans)
--dt-text-display-lg-linesemanticvar(--dt-line-height-7xl)
--dt-text-display-lg-sizesemanticvar(--dt-font-size-7xl)
--dt-text-display-lg-trackingsemanticvar(--dt-tracking-tightest)
--dt-text-display-lg-weightsemanticvar(--dt-font-weight-medium)
--dt-text-display-md-familysemanticvar(--dt-font-family-sans)
--dt-text-display-md-linesemanticvar(--dt-line-height-6xl)
--dt-text-display-md-sizesemanticvar(--dt-font-size-6xl)
--dt-text-display-md-trackingsemanticvar(--dt-tracking-tightest)
--dt-text-display-md-weightsemanticvar(--dt-font-weight-medium)
--dt-text-display-sm-familysemanticvar(--dt-font-family-sans)
--dt-text-display-sm-linesemanticvar(--dt-line-height-5xl)
--dt-text-display-sm-sizesemanticvar(--dt-font-size-5xl)
--dt-text-display-sm-trackingsemanticvar(--dt-tracking-tighter)
--dt-text-display-sm-weightsemanticvar(--dt-font-weight-medium)
--dt-text-display-xl-familysemanticvar(--dt-font-family-sans)
--dt-text-display-xl-linesemanticvar(--dt-line-height-fluid)
--dt-text-display-xl-sizesemanticvar(--dt-font-size-fluid-xl)
--dt-text-display-xl-trackingsemanticvar(--dt-tracking-tightest)
--dt-text-display-xl-weightsemanticvar(--dt-font-weight-medium)
--dt-text-eyebrow-familysemanticvar(--dt-font-family-secondary)
--dt-text-eyebrow-linesemanticvar(--dt-line-height-2xs)
--dt-text-eyebrow-sizesemanticvar(--dt-font-size-2xs)
--dt-text-eyebrow-trackingsemanticvar(--dt-tracking-wider)
--dt-text-eyebrow-weightsemanticvar(--dt-font-weight-semibold)
--dt-text-heading-lg-familysemanticvar(--dt-font-family-sans)
--dt-text-heading-lg-linesemanticvar(--dt-line-height-3xl)
--dt-text-heading-lg-sizesemanticvar(--dt-font-size-3xl)
--dt-text-heading-lg-trackingsemanticvar(--dt-tracking-tight)
--dt-text-heading-lg-weightsemanticvar(--dt-font-weight-medium)
--dt-text-heading-md-familysemanticvar(--dt-font-family-sans)
--dt-text-heading-md-linesemanticvar(--dt-line-height-2xl)
--dt-text-heading-md-sizesemanticvar(--dt-font-size-2xl)
--dt-text-heading-md-trackingsemanticvar(--dt-tracking-tight)
--dt-text-heading-md-weightsemanticvar(--dt-font-weight-medium)
--dt-text-heading-sm-familysemanticvar(--dt-font-family-sans)
--dt-text-heading-sm-linesemanticvar(--dt-line-height-xl)
--dt-text-heading-sm-sizesemanticvar(--dt-font-size-xl)
--dt-text-heading-sm-trackingsemanticvar(--dt-tracking-snug)
--dt-text-heading-sm-weightsemanticvar(--dt-font-weight-medium)
--dt-text-heading-xl-familysemanticvar(--dt-font-family-sans)
--dt-text-heading-xl-linesemanticvar(--dt-line-height-4xl)
--dt-text-heading-xl-sizesemanticvar(--dt-font-size-4xl)
--dt-text-heading-xl-trackingsemanticvar(--dt-tracking-tighter)
--dt-text-heading-xl-weightsemanticvar(--dt-font-weight-medium)
--dt-text-heading-xs-familysemanticvar(--dt-font-family-sans)
--dt-text-heading-xs-linesemanticvar(--dt-line-height-lg)
--dt-text-heading-xs-sizesemanticvar(--dt-font-size-lg)
--dt-text-heading-xs-trackingsemanticvar(--dt-tracking-normal)
--dt-text-heading-xs-weightsemanticvar(--dt-font-weight-medium)
--dt-text-headlinesemanticvar(--dt-text-primary)
--dt-text-infosemanticvar(--dt-color-cyan-900)
--dt-text-inversesemanticvar(--dt-color-neutral-050)
--dt-text-label-lg-familysemanticvar(--dt-font-family-secondary)
--dt-text-label-lg-linesemanticvar(--dt-line-height-md)
--dt-text-label-lg-sizesemanticvar(--dt-font-size-md)
--dt-text-label-lg-trackingsemanticvar(--dt-tracking-normal)
--dt-text-label-lg-weightsemanticvar(--dt-font-weight-medium)
--dt-text-label-md-familysemanticvar(--dt-font-family-secondary)
--dt-text-label-md-linesemanticvar(--dt-line-height-sm)
--dt-text-label-md-sizesemanticvar(--dt-font-size-sm)
--dt-text-label-md-trackingsemanticvar(--dt-tracking-normal)
--dt-text-label-md-weightsemanticvar(--dt-font-weight-medium)
--dt-text-label-sm-familysemanticvar(--dt-font-family-secondary)
--dt-text-label-sm-linesemanticvar(--dt-line-height-xs)
--dt-text-label-sm-sizesemanticvar(--dt-font-size-xs)
--dt-text-label-sm-trackingsemanticvar(--dt-tracking-normal)
--dt-text-label-sm-weightsemanticvar(--dt-font-weight-medium)
--dt-text-on-actionsemanticvar(--dt-color-white)
--dt-text-on-action-brandsemanticvar(--dt-color-white)
--dt-text-on-action-brand-secondarysemanticvar(--dt-color-white)
--dt-text-on-action-dangersemanticvar(--dt-color-white)
--dt-text-on-action-disabledsemanticvar(--dt-color-neutral-400)
--dt-text-on-action-ghostsemanticvar(--dt-color-neutral-900)
--dt-text-on-action-secondarysemanticvar(--dt-color-neutral-900)
--dt-text-on-brandsemanticvar(--dt-color-white)
--dt-text-on-brand-mutedsemanticvar(--dt-color-primary-900)
--dt-text-on-brand-secondarysemanticvar(--dt-color-white)
--dt-text-on-brand-secondary-mutedsemanticvar(--dt-color-secondary-900)
--dt-text-on-dangersemanticvar(--dt-color-white)
--dt-text-on-infosemanticvar(--dt-color-white)
--dt-text-on-scrimsemanticvar(--dt-color-neutral-050)
--dt-text-on-scrim-brandsemanticvar(--dt-color-primary-200)
--dt-text-on-scrim-secondarysemanticvar(--dt-color-neutral-200)
--dt-text-on-scrim-strongsemanticvar(--dt-color-white)
--dt-text-on-selectedsemanticvar(--dt-color-neutral-900)
--dt-text-on-selected-brandsemanticvar(--dt-color-primary-900)
--dt-text-on-successsemanticvar(--dt-color-white)
--dt-text-on-warningsemanticvar(--dt-color-neutral-950)
--dt-text-primarysemanticvar(--dt-color-neutral-900)
--dt-text-secondarysemanticvar(--dt-color-neutral-600)
--dt-text-successsemanticvar(--dt-color-green-800)
--dt-text-tertiarysemanticvar(--dt-color-neutral-500)
--dt-text-warningsemanticvar(--dt-color-amber-900)
--dt-text-wordmarksemanticvar(--dt-text-primary)
--dt-font-weight-mediumprimitive500

Source

React only, no dependencies, every value a token.

import React from "react";
import { AspectRatio } from "../content/AspectRatio.jsx";
import { Price } from "./Price.jsx";
import { Rating } from "./Rating.jsx";

const role = (name) => ({
  fontFamily: `var(--dt-text-${name}-family)`,
  fontSize: `var(--dt-text-${name}-size)`,
  lineHeight: `var(--dt-text-${name}-line)`,
  fontWeight: `var(--dt-text-${name}-weight)`,
  letterSpacing: `var(--dt-text-${name}-tracking)`,
});

function ClockIcon() {
  return (
    <svg
      viewBox="0 0 24 24" aria-hidden="true" focusable="false"
      fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
      style={{ display: "block", flex: "none", width: "var(--dt-size-icon-sm)", height: "var(--dt-size-icon-sm)" }}
    >
      <circle cx="12" cy="12" r="9" />
      <path d="M12 7v5l3 2" />
    </svg>
  );
}

/* Items in a line with a middle dot between them. The dots are decoration,
   hidden from assistive technology, so the line reads as a list of facts.
   Every item leads with its dot, and the row is pulled back by one dot's
   width inside a clipping box, so the dot that starts a line (the first, or
   one after a wrap) is cut off and no line begins or ends with a stray dot. */
const SEP = "var(--dt-space-inline-md)";
function Dotted({ items }) {
  const shown = items.filter(Boolean);
  if (!shown.length) return null;
  return (
    <div style={{ overflow: "hidden", minWidth: 0 }}>
      <div
        style={{
          display: "flex", flexWrap: "wrap", alignItems: "center",
          rowGap: "var(--dt-space-stack-2xs)", marginInlineStart: `calc(-1 * ${SEP})`,
          ...role("body-sm"), color: "var(--dt-store-meta-color)",
        }}
      >
        {shown.map((node, i) => (
          <span key={i} style={{ display: "inline-flex", alignItems: "center", minWidth: 0 }}>
            <span aria-hidden="true" style={{ flex: "none", width: SEP, textAlign: "center" }}>·</span>
            <span style={{ display: "inline-flex", alignItems: "center", gap: "var(--dt-space-inline-2xs)", minWidth: 0 }}>{node}</span>
          </span>
        ))}
      </div>
    </div>
  );
}

export function StoreHeader({
  name,
  image,
  logo,
  rating,
  meta = [],
  deliveryTime,
  deliveryFee,
  currency = "USD",
  locale,
  status,
  actions,
  headingLevel = 1,
  freeDeliveryLabel = "Free delivery",
  deliveryFeeLabel = "delivery",
  style,
  ...rest
}) {
  const closed = !!status && status.open === false;
  const level = Math.min(Math.max(Math.round(headingLevel) || 1, 1), 6);
  const H = `h${level}`;
  const hasCover = !!image;
  const greyed = closed ? "grayscale(1)" : undefined;

  const fee = typeof deliveryFee === "number" ? (
    <Price
      amount={deliveryFee}
      currency={currency}
      locale={locale}
      size="sm"
      freeLabel={freeDeliveryLabel}
      unit={deliveryFee === 0 ? undefined : deliveryFeeLabel}
    />
  ) : null;

  const statusNode = status ? (
    <span style={{ display: "inline-flex", alignItems: "center", gap: "var(--dt-space-inline-2xs)", color: closed ? "var(--dt-store-closed-color)" : "var(--dt-store-open-color)", fontWeight: "var(--dt-font-weight-medium)" }}>
      <span aria-hidden="true" style={{ flex: "none", width: "calc(var(--dt-size-icon-xs) / 2)", height: "calc(var(--dt-size-icon-xs) / 2)", borderRadius: "var(--dt-radius-pill)", background: "currentColor" }} />
      {status.label}
    </span>
  ) : null;

  const time = deliveryTime ? (
    <React.Fragment>
      <ClockIcon />
      {deliveryTime}
    </React.Fragment>
  ) : null;

  return (
    <div
      style={{ display: "flex", flexDirection: "column", gap: "var(--dt-space-stack-sm)", minWidth: 0, color: "var(--dt-text-primary)", ...style }}
      {...rest}
    >
      {hasCover && (
        <AspectRatio
          ratio={3}
          style={{ maxHeight: "var(--dt-store-cover-max-height)", background: "var(--dt-store-cover-bg)", borderRadius: "var(--dt-store-cover-radius)" }}
        >
          {image.src && (
            <img
              src={image.src}
              alt={image.alt || ""}
              style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", display: "block", filter: greyed }}
            />
          )}
          {closed && <span aria-hidden="true" style={{ position: "absolute", inset: 0, background: "var(--dt-store-closed-scrim)" }} />}
        </AspectRatio>
      )}

      {logo && (
        <span
          style={{
            position: "relative", display: "block", flex: "none", overflow: "hidden", boxSizing: "content-box",
            width: "var(--dt-store-logo-size)", height: "var(--dt-store-logo-size)",
            marginTop: hasCover ? "calc(-0.5 * var(--dt-store-logo-size) - var(--dt-space-stack-sm))" : 0,
            marginInlineStart: hasCover ? "var(--dt-space-inset-md)" : 0,
            borderRadius: "var(--dt-store-logo-radius)",
            border: hasCover ? "var(--dt-border-width-strong) solid var(--dt-store-logo-ring)" : "var(--dt-border-width-default) solid var(--dt-border-subtle)",
            background: "var(--dt-store-logo-bg)",
          }}
        >
          {logo.src && (
            <img src={logo.src} alt={logo.alt || ""} style={{ width: "100%", height: "100%", objectFit: "cover", display: "block", filter: greyed }} />
          )}
        </span>
      )}

      <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "var(--dt-space-inline-sm)", minWidth: 0 }}>
        <H style={{ ...role("heading-md"), margin: 0, minWidth: 0, overflowWrap: "anywhere", color: "var(--dt-text-primary)" }}>{name}</H>
        {actions && <div style={{ display: "flex", alignItems: "center", gap: "var(--dt-space-inline-2xs)", flex: "none" }}>{actions}</div>}
      </div>

      {(rating || meta.length > 0 || status || time || fee) && (
        <div style={{ display: "flex", flexDirection: "column", gap: "var(--dt-space-stack-2xs)", minWidth: 0 }}>
          <Dotted
            items={[
              rating && typeof rating.value === "number" ? <Rating value={rating.value} count={rating.count} size="sm" locale={locale} /> : null,
              ...meta,
            ]}
          />
          <Dotted items={[statusNode, time, fee]} />
        </div>
      )}
    </div>
  );
}