The code of the preview above.
demos/hotel_card.qwe
view HotelCardDemo() = <div.demo-stack> <HotelCard id="hotel-card-demo-alfama" name="Casa do Alfama" kind="Hotel" stars={4} score={8.7} reviews={1284} location="Alfama, Lisbon" distance="0.8 km from the centre" image="https://picsum.photos/seed/alfama/480/360" badge="Deal" price={129.00m} old={152.00m} nights={4} taxes={41.28m} amenities={["Free WiFi", "Breakfast included", "Air conditioning", "Rooftop terrace"]} cancellation="Free cancellation until 3 Nov" prepay="No prepayment needed" rooms_left={2} href="#casa-do-alfama"/> <div class="demo-row" style="align-items: start"> <HotelCard id="hotel-card-demo-flat" layout="column" name="Riverside flat" kind="Apartment" score={9.6} reviews={1} location="Porto, Ribeira" image="https://picsum.photos/seed/ribeira/480/300" price={95} currency="EUR" locale="de-DE" nights={3} price_note="Includes taxes and fees" amenities={["Kitchen", "Washing machine"]}/> <HotelCard id="hotel-card-demo-plain" layout="column" name="Harbour Inn" stars={2} score={6.4} score_label="Fair" price={74.50m} location="Dover" price_note="+ $9.20 taxes and fees" rooms_left={8}/> </div> </div>
ui/hotel_card.qwe
import ui/theme.qwe use ui_clslet hotel_card_css = css`.ui-hotelcard { position: relative; display: grid; grid-template-columns: minmax(160px, 240px) minmax(0, 1fr); min-width: 0; overflow: hidden; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.4 var(--ui-font); transition: border-color .15s, box-shadow .15s; }.ui-hotelcard:hover { border-color: var(--ui-strong); box-shadow: var(--ui-shadow); }.ui-hotelcard:has(.ui-hotelcard-name a:focus-visible) { border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-hotelcard-column { display: flex; flex-direction: column; width: 100%; max-width: 340px; }.ui-hotelcard-bare { grid-template-columns: 1fr; }.ui-hotelcard-media { position: relative; min-height: 180px; background: var(--ui-subtle); }.ui-hotelcard-column .ui-hotelcard-media { aspect-ratio: 16 / 10; min-height: 0; }.ui-hotelcard-media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }.ui-hotelcard-badge { position: absolute; top: 10px; left: 10px; height: 22px; padding: 0 9px; border-radius: 999px; background: var(--ui-accent); color: var(--ui-accent-fg); font: 600 11px/22px var(--ui-font); letter-spacing: .03em; text-transform: uppercase; }.ui-hotelcard-body { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px 16px; padding: 14px 16px; }.ui-hotelcard-column .ui-hotelcard-body { flex: 1; grid-template-columns: 1fr; grid-template-rows: 1fr auto; }.ui-hotelcard-info { display: grid; gap: 6px; align-content: start; min-width: 0; }.ui-hotelcard-kind { color: var(--ui-muted); font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }.ui-hotelcard-titlerow { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; }.ui-hotelcard .ui-hotelcard-name { margin: 0; font-size: 17px; font-weight: 650; line-height: 1.25; }.ui-hotelcard-name a { color: inherit; text-decoration: none; }.ui-hotelcard-name a::after { content: ""; position: absolute; inset: 0; }.ui-hotelcard-name a:hover { color: var(--ui-accent); }.ui-hotelcard-name a:focus-visible { outline: none; }.ui-hotelcard-stars { display: inline-flex; gap: 1px; color: var(--ui-warning); }.ui-hotelcard-stars svg { width: 13px; height: 13px; }.ui-hotelcard-where { color: var(--ui-muted); font-size: 13px; }.ui-hotelcard-amen { display: flex; flex-wrap: wrap; gap: 6px; margin: 2px 0 0; padding: 0; list-style: none; }.ui-hotelcard-amen li { padding: 2px 8px; border: 1px solid var(--ui-border); border-radius: 999px; color: var(--ui-muted); font-size: 12px; }.ui-hotelcard-terms { display: grid; gap: 1px; font-size: 13px; }.ui-hotelcard-cancel { color: var(--ui-success); font-weight: 600; }.ui-hotelcard-cancel::before { content: "✓ "; }.ui-hotelcard-prepay { color: var(--ui-muted); }.ui-hotelcard-left { color: var(--ui-danger); font-size: 13px; font-weight: 600; }.ui-hotelcard-side { display: grid; align-content: space-between; justify-items: end; gap: 12px; text-align: right; }.ui-hotelcard-column .ui-hotelcard-side { display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between; text-align: left; }.ui-hotelcard-column .ui-hotelcard-price { justify-items: end; text-align: right; }.ui-hotelcard-review { display: flex; align-items: center; gap: 8px; }.ui-hotelcard-words { display: grid; font-size: 12px; color: var(--ui-muted); text-align: right; }.ui-hotelcard-words b { color: var(--ui-fg); font-size: 14px; font-weight: 650; }.ui-hotelcard-column .ui-hotelcard-review { flex-direction: row-reverse; }.ui-hotelcard-column .ui-hotelcard-words { text-align: left; }.ui-hotelcard-score { display: grid; place-items: center; min-width: 36px; height: 34px; padding: 0 6px; border-radius: 8px 8px 8px 2px; background: var(--ui-accent); color: var(--ui-accent-fg); font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; }.ui-hotelcard-price { display: grid; gap: 1px; justify-items: end; font-variant-numeric: tabular-nums; }.ui-hotelcard-old { color: var(--ui-danger); font-size: 13px; text-decoration: line-through; }.ui-hotelcard-night { font-size: 22px; font-weight: 700; line-height: 1.15; letter-spacing: -.01em; white-space: nowrap; }.ui-hotelcard-night small { color: var(--ui-muted); font-size: 12px; font-weight: 500; letter-spacing: 0; }.ui-hotelcard-total, .ui-hotelcard-note { color: var(--ui-muted); font-size: 12px; }.ui-hotelcard-btn { position: relative; z-index: 1; display: inline-flex; align-items: center; height: 34px; margin-top: 6px; padding: 0 14px; border-radius: var(--ui-radius-sm); background: var(--ui-accent); color: var(--ui-accent-fg); font-size: 13px; font-weight: 600; text-decoration: none; }.ui-hotelcard-btn:hover { filter: brightness(1.08); }.ui-hotelcard-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }@media (max-width: 620px) { .ui-hotelcard { grid-template-columns: 1fr; } .ui-hotelcard-media { aspect-ratio: 16 / 9; min-height: 0; } .ui-hotelcard-body { grid-template-columns: 1fr; } .ui-hotelcard-side { justify-items: start; text-align: left; } .ui-hotelcard-price { justify-items: start; }}@media (prefers-reduced-motion: reduce) { .ui-hotelcard { transition: none; } }`let hotel_card_star = <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.6l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.4l-5.8 3.1 1.1-6.5L2.6 9.4l6.5-.9z"/></svg>fn hotel_card_word(score: num) -> str { if score >= 9.5 { return "Exceptional" } if score >= 8.5 { return "Excellent" } if score >= 8 { return "Very good" } if score >= 7 { return "Good" } if score >= 6 { return "Pleasant" } "Review score"}fn hotel_card_money(x: dec, currency: str, locale: str, whole: bool = false) -> str { let m = money.of(x, currency, "half_up") if type_of(m) == "error" { return `${format_number(round(x, 2, "half_up"), locale, 2)} ${currency}` } let text = money.format(m, locale) let units = money.minor_units(currency) !whole || units == 0 || x != floor(x) ? text : re.replace(text, `[.,]0{${units}}(\\D*)$`, "$1")}view HotelCard(name: str, price, image: str? = null, image_alt: str = "", href: str? = null, kind: str? = null, stars: int? = null, score = null, reviews: int? = null, score_label: str? = null, location: str? = null, distance: str? = null, amenities: list = [], cancellation: str? = null, prepay: str? = null, rooms_left: int? = null, badge: str? = null, old = null, nights: int? = null, taxes = null, price_note: str? = null, currency: str = "USD", locale: str = "en-US", layout: str = "row", action_label: str = "See availability", id: str? = null, class: str? = null) { style global hotel_card_css let night = dec(price) let before = old != null && dec(old) > night ? dec(old) : null let total = nights != null ? night * nights + (taxes != null ? dec(taxes) : 0m) : null let rated = score != null ? round(float(score), 1) : null let shown = rated != null ? format_number(rated, locale, 1) : null <article class={ui_cls(["ui-hotelcard", layout == "column" && "ui-hotelcard-column", image == null && "ui-hotelcard-bare", class])} id={id}> {image != null && <div.ui-hotelcard-media> <img src={image} alt={image_alt} loading="lazy"/> {badge != null && <span.ui-hotelcard-badge>{badge}</span>} </div>} <div.ui-hotelcard-body> <div.ui-hotelcard-info> {kind != null && <span.ui-hotelcard-kind>{kind}</span>} <div.ui-hotelcard-titlerow> <h3.ui-hotelcard-name>{href != null ? <a href={href}>{name}</a> : name}</h3> {stars != null && stars > 0 && <span.ui-hotelcard-stars role="img" aria-label={`${stars}-star ${lower(kind ?? "hotel")}`}> {for i in range(stars) => hotel_card_star} </span>} </div> {(location != null || distance != null) && <div.ui-hotelcard-where>{join([location, distance] | filter((x) => x != null), " · ")}</div>} {len(amenities) > 0 && <ul.ui-hotelcard-amen aria-label="Amenities">{for a in amenities => <li>{a}</li>}</ul>} {(cancellation != null || prepay != null) && <div.ui-hotelcard-terms> {cancellation != null && <span.ui-hotelcard-cancel>{cancellation}</span>} {prepay != null && <span.ui-hotelcard-prepay>{prepay}</span>} </div>} {rooms_left != null && rooms_left > 0 && rooms_left < 5 && <span.ui-hotelcard-left>{rooms_left == 1 ? "Only 1 room left" : `Only ${rooms_left} rooms left`} at this price</span>} </div> <div.ui-hotelcard-side> {rated != null ? <div.ui-hotelcard-review> <span.ui-hotelcard-words> <b>{score_label ?? hotel_card_word(rated)}</b> {reviews != null && [" ", <span>{format_number(reviews, locale, 0)} {reviews == 1 ? "review" : "reviews"}</span>]} </span> <span.ui-hotelcard-score role="img" aria-label={`Scored ${shown} out of 10`}>{shown}</span> </div> : <span></span>} <div.ui-hotelcard-price> {before != null && <span.ui-hotelcard-old><span.ui-sr>was </span>{hotel_card_money(before, currency, locale, true)}</span>} <span.ui-hotelcard-night data-amount={str(night)}>{hotel_card_money(night, currency, locale, true)} <small>/ night</small></span> {total != null && <span.ui-hotelcard-total data-amount={str(total)}> {nights == 1 ? "1 night" : `${nights} nights`}: {hotel_card_money(total, currency, locale)}{taxes != null && " with taxes"} </span>} {price_note != null && <span.ui-hotelcard-note>{price_note}</span>} {href != null && <a.ui-hotelcard-btn href={href} tabindex="-1" aria-hidden="true">{action_label}</a>} </div> </div> </div> </article>}