独家

All-in Price <mv-all-in>

诚实的全包价格展示,把滴漏式定价隐藏的内容直接说出来。

文化出处

皇帝的新装,安徒生(1837 年,童话)。所有人都假装赞美根本不存在的衣服,直到一个孩子把大家都看得见的事实直接说了出来。UI 扮演的就是这个孩子:它在价格旁边用直白的话说明真实总价和每一项隐藏费用,而不是附和一个人人都知道并非实付金额的广告价格。

工作原理

诚实的全包价格展示,把滴漏式定价隐藏的内容直接说出来。包裹任意价格(商品卡片、酒店或租赁预订、票务、SaaS 结账),声明广告基础价,并以 data-fee 子元素或 lines 属性声明每一项费用:固定或按比例的费用、按件或按单收取的费用、按地区计算的税费(基于基础价、小计或总价)、配送费、刷卡附加费、折扣、一次性开通费、稍后支付的款项(“at the property”)。标题始终是客户实际要支付的总价(“$713.05 total for 3 nights”),广告价格作为次要信息(“Room rate $164/night · + $221.05 fees & taxes”);“What’s included · 4 charges”展开项会在真正的表格中逐项列出所有费用,附带通俗说明、现在支付与稍后支付的行以及续费价格;可选附加项则单独放在自己的清单中,绝不会被预先加上。隐藏费用模式会在价格旁边标出,而不是藏在小字里:续费价格高于首期价格(“Then $62.03/mo after 3 months”)、带最低数量的单价(“Minimum 3 seats: priced for 3”)、按年计费的金额、“from”起价以及稍后支付的费用。金额使用 Intl.NumberFormat,并按货币规则舍入(默认 en-US / USD,支持任意语言区域和货币)。配套的 <mv-all-in-list> 让比较列表变得公平:每张卡片都显示全包总价,条目可按总价或广告价格重新排序(FLIP,保持焦点),并标出最低总价。其设计考虑了 2024-2025 年推动总价展示的趋势(针对垃圾费用和滴漏式定价的规定);它只展示应用所声明的内容,本身并不能让结账流程合规。

分类数据展示
类型Web Component(<mv-all-in>)
状态稳定版
套件信任与隐私
Keywordsexclusive, culture, price, pricing, total-price, all-in-pricing, fees, junk-fees, drip-pricing, taxes, vat, breakdown, checkout, booking, ticketing, subscription, renewal, e-commerce, comparison, currency, intl

When to use

  • A booking, ticketing or rental page must lead with the total including every mandatory fee and tax, not the nightly or per-ticket rate
  • A subscription checkout has an intro price, a higher renewal, a per-seat minimum or annual billing that must be stated next to the price
  • Offers from several sellers must be compared and sorted by what the customer will actually pay
  • Taxes and currency depend on the buyer's region and optional add-ons must stay out of the total until the customer adds them

Avoid when

  • Only a formatted price with a compare-at discount is needed and there are no fees or taxes to disclose → use Price instead
  • Several subscription plans must be compared feature by feature with a monthly / yearly switch → use Pricing instead
  • The shopper is editing a multi-item cart with promo codes, shipping choice and a checkout button → use Cart instead

安装

node scripts/add.mjs all-in --out ./src/marvelous

通过 Marvelous UI MCP 服务器使用 AI 智能体:install_components({ slugs: ["all-in"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。

复制的文件(含依赖):tokens/tokens.css、core/base.css、core/dom.js、core/element.js、core/motion.js、components/all-in/all-in.js、components/all-in/all-in.css。

用法

快速开始,最小可运行标记:

<mv-all-in base="164" quantity="3" unit="night">
  <span data-fee="32" data-per="unit">Destination fee</span>
  <span data-fee data-kind="tax" data-rate="17.49%">Hotel tax</span>
</mv-all-in>

标准标记,可在此基础上通过属性、data-* 和 CSS 变量进行定制:

<div id="ai-demo" style="width:min(100%,64rem);margin-inline:auto">
  <style>
    #ai-demo { display:grid; gap:1.25rem; align-content:start }
    #ai-demo .ai-top { display:grid; grid-template-columns:minmax(0,1.25fr) minmax(0,1fr); gap:1.25rem; align-items:start }
    #ai-demo .ai-card { display:grid; gap:1rem; min-width:0; padding:1.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm) }
    #ai-demo .ai-eyebrow { margin:0; color:var(--mv-fg-subtle); font-size:.6875rem; font-weight:600; letter-spacing:.06em; text-transform:uppercase }
    #ai-demo .ai-title { margin:.125rem 0 0; font-size:1.0625rem; font-weight:650; letter-spacing:-.01em }
    #ai-demo .ai-meta { margin:.25rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #ai-demo .ai-media { display:flex; gap:.875rem; align-items:flex-start }
    #ai-demo .ai-thumb { flex:none; display:grid; place-items:center; width:4.5rem; height:4.5rem; border-radius:var(--mv-radius-lg); background:linear-gradient(135deg, color-mix(in oklab, var(--mv-accent) 22%, var(--mv-bg-muted)), var(--mv-bg-muted)); color:var(--mv-fg-muted) }
    #ai-demo .ai-thumb svg { width:1.75rem; height:1.75rem }
    #ai-demo .ai-controls { display:flex; flex-wrap:wrap; align-items:center; gap:.625rem 1rem; padding-top:1rem; border-top:1px solid var(--mv-border) }
    #ai-demo .ai-control { display:grid; gap:.3125rem }
    #ai-demo .ai-label { color:var(--mv-fg-muted); font-size:.75rem; font-weight:500 }
    #ai-demo .ai-log { min-height:1.1rem; margin:0; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono) }
    #ai-demo .ai-compare { display:grid; gap:1rem; padding:1.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle) }
    #ai-demo .ai-compare-head { display:flex; flex-wrap:wrap; align-items:flex-end; justify-content:space-between; gap:.75rem 1.25rem }
    #ai-demo .ai-compare-tools { display:flex; flex-wrap:wrap; gap:.625rem 1rem }
    #ai-demo .ai-offer { display:grid; grid-template-columns:minmax(0,13rem) minmax(0,1fr) auto; gap:.75rem 1.25rem; align-items:start; padding:1rem 1.125rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg); background:var(--mv-surface) }
    #ai-demo .ai-seller { display:grid; gap:.25rem; min-width:0 }
    #ai-demo .ai-seller h4 { margin:0; font-size:.9375rem; font-weight:650 }
    #ai-demo .ai-seller p { margin:0; color:var(--mv-fg-muted); font-size:.75rem; line-height:1.45 }
    #ai-demo .ai-offer .mv-button { align-self:start }
    @media (max-width:52rem) {
      #ai-demo .ai-top { grid-template-columns:minmax(0,1fr) }
      #ai-demo .ai-offer { grid-template-columns:minmax(0,1fr) }
    }
  </style>

  <div class="ai-top">
    <!-- Hotel booking: the total leads, the nightly rate is secondary, the destination fee is due at the property. -->
    <section class="ai-card" aria-labelledby="ai-hotel-title">
      <div class="ai-media">
        <span class="ai-thumb" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21V8l9-5 9 5v13"/><path d="M9 21v-6h6v6"/><path d="M8 10h.01M12 10h.01M16 10h.01"/></svg></span>
        <div>
          <p class="ai-eyebrow">Hotel · Chicago, IL</p>
          <h3 class="ai-title" id="ai-hotel-title">The Wrenfield River North</h3>
          <p class="ai-meta">Deluxe King · Wed, Oct 14 to Sat, Oct 17, 2026 · 2 guests</p>
        </div>
      </div>

      <mv-all-in id="ai-hotel" base="164" quantity="3" unit="night" base-label="Room rate" data-size="lg">
        <span data-fee="32" data-per="unit" data-due="at the property" data-note="Wi-Fi, fitness center and two welcome drinks. Charged by the hotel.">Destination fee</span>
        <span data-fee="18.90" data-note="Charged by the booking site to process your reservation.">Service fee</span>
        <span data-fee data-kind="tax" data-rate="17.49%" data-note="State, county and city hotel taxes.">Chicago hotel tax</span>
        <span data-fee="28" data-per="unit" data-optional data-note="Served 7-10:30 a.m. in the lobby café.">Breakfast for two</span>
        <span data-fee="72" data-per="unit" data-optional data-note="In-and-out privileges.">Valet parking</span>
        <span data-fee="40" data-optional data-note="Subject to availability, confirmed at check-in.">Late checkout (2 p.m.)</span>
      </mv-all-in>

      <div class="ai-controls">
        <div class="ai-control">
          <span class="ai-label" id="ai-nights-l">Nights</span>
          <mv-segmented id="ai-nights" aria-labelledby="ai-nights-l" name="nights" value="3">
            <button value="2">2</button>
            <button value="3">3</button>
            <button value="5">5</button>
          </mv-segmented>
        </div>
        <div class="ai-control">
          <span class="ai-label" id="ai-head-l">Headline</span>
          <mv-segmented id="ai-head" aria-labelledby="ai-head-l" name="headline" value="total">
            <button value="total">All-in total</button>
            <button value="base">Base price</button>
          </mv-segmented>
        </div>
        <p class="ai-log" id="ai-log" aria-live="polite" style="flex:1 1 12rem;align-self:end">Open “What’s included” or add an extra.</p>
      </div>
    </section>

    <!-- SaaS checkout: intro price, higher renewal, per-seat minimum, tax and currency by region. -->
    <section class="ai-card" aria-labelledby="ai-saas-title">
      <div>
        <p class="ai-eyebrow">Checkout · Team plan</p>
        <h3 class="ai-title" id="ai-saas-title">Pipewren CRM</h3>
        <p class="ai-meta">Shared inbox, pipelines and reporting for small sales teams.</p>
      </div>

      <mv-all-in id="ai-saas" base="12" renewal="19" intro-periods="3" unit="seat" period="month" quantity="2" min-quantity="3" region="US-NY" base-label="Intro price">
        <span data-fee data-kind="tax" data-rate="8.875" data-region="US-NY">New York sales tax</span>
        <span data-fee data-kind="tax" data-rate="19" data-region="DE">German VAT</span>
        <span data-fee data-kind="tax" data-rate="20" data-region="GB">UK VAT</span>
        <span data-fee="2" data-per="unit" data-optional data-note="10 GB extra per seat.">Extra file storage</span>
      </mv-all-in>

      <div class="ai-controls">
        <div class="ai-control">
          <span class="ai-label" id="ai-seats-l">Seats</span>
          <mv-segmented id="ai-seats" aria-labelledby="ai-seats-l" name="seats" value="2">
            <button value="2">2</button>
            <button value="5">5</button>
            <button value="12">12</button>
          </mv-segmented>
        </div>
        <div class="ai-control">
          <label class="ai-label" for="ai-region">Billing country</label>
          <select class="mv-select" id="ai-region" data-size="sm">
            <option value="US-NY" selected>United States (NY)</option>
            <option value="US-OR">United States (OR)</option>
            <option value="DE">Germany</option>
            <option value="GB">United Kingdom</option>
          </select>
        </div>
      </div>
    </section>
  </div>

  <!-- Ticket comparison: every offer shows its all-in total, sorted by what you will actually pay. -->
  <section class="ai-compare" aria-labelledby="ai-cmp-title">
    <div class="ai-compare-head">
      <div>
        <p class="ai-eyebrow">2 tickets · Floor B, Row 12</p>
        <h3 class="ai-title" id="ai-cmp-title">Ashgrove Arena · Sat, Nov 7, 2026, 8:00 p.m.</h3>
      </div>
      <div class="ai-compare-tools">
        <div class="ai-control">
          <span class="ai-label" id="ai-show-l">Show</span>
          <mv-segmented id="ai-show" aria-labelledby="ai-show-l" name="show" value="total">
            <button value="total">All-in totals</button>
            <button value="base">Advertised prices</button>
          </mv-segmented>
        </div>
        <div class="ai-control">
          <span class="ai-label" id="ai-sort-l">Sort by</span>
          <mv-segmented id="ai-sort" aria-labelledby="ai-sort-l" name="sort" value="total">
            <button value="total">Total</button>
            <button value="base">Advertised</button>
          </mv-segmented>
        </div>
      </div>
    </div>

    <mv-all-in-list id="ai-list" sort="total" headline="total">
      <article class="ai-offer" data-name="StubLoft">
        <div class="ai-seller">
          <h4>StubLoft</h4>
          <p>Resale marketplace · delivered to your phone 48 h before the show</p>
        </div>
        <mv-all-in base="89" quantity="2" unit="ticket" data-size="sm">
          <span data-fee data-rate="27%" data-note="Marketplace fee, charged per order.">Service fee</span>
          <span data-fee="7.50" data-per="unit" data-note="Collected for the venue.">Facility fee</span>
          <span data-fee="5.95">Order processing</span>
          <span data-fee data-kind="tax" data-rate="9%">Amusement tax</span>
        </mv-all-in>
        <button type="button" class="mv-button" data-variant="outline" data-size="sm">Select</button>
      </article>

      <article class="ai-offer" data-name="Ticketvale">
        <div class="ai-seller">
          <h4>Ticketvale</h4>
          <p>Verified resale · instant mobile transfer</p>
        </div>
        <mv-all-in base="104" quantity="2" unit="ticket" data-size="sm">
          <span data-fee="11" data-per="unit">Service fee</span>
          <span data-fee data-kind="tax" data-rate="9%">Amusement tax</span>
        </mv-all-in>
        <button type="button" class="mv-button" data-variant="outline" data-size="sm">Select</button>
      </article>

      <article class="ai-offer" data-name="Ashgrove Arena box office">
        <div class="ai-seller">
          <h4>Ashgrove Arena box office</h4>
          <p>Official seller · pick up at will call or print at home</p>
        </div>
        <mv-all-in base="118" quantity="2" unit="ticket" data-size="sm">
          <span data-fee="3" data-per="unit" data-note="Venue restoration fee.">Facility fee</span>
          <span data-fee data-kind="tax" data-rate="9%">Amusement tax</span>
        </mv-all-in>
        <button type="button" class="mv-button" data-variant="outline" data-size="sm">Select</button>
      </article>
    </mv-all-in-list>
  </section>

  <script type="module">
    const hotel = document.getElementById("ai-hotel");
    const saas = document.getElementById("ai-saas");
    const list = document.getElementById("ai-list");
    const log = document.getElementById("ai-log");
    const say = (text) => { log.textContent = text; };

    document.getElementById("ai-nights").addEventListener("mv-change", (e) => { hotel.quantity = Number(e.detail.value); });
    document.getElementById("ai-head").addEventListener("mv-change", (e) => { hotel.headline = e.detail.value; });
    hotel.addEventListener("mv-breakdown-open", (e) => say(`mv-breakdown-open · total ${hotel.formattedTotal}`));
    hotel.addEventListener("mv-extra-change", (e) => say(`mv-extra-change · ${e.detail.label} ${e.detail.added ? "added" : "removed"}`));

    document.getElementById("ai-seats").addEventListener("mv-change", (e) => { saas.quantity = Number(e.detail.value); });
    // Each billing country has its own currency and list price.
    const regions = {
      "US-NY": { currency: "USD", locale: "en-US", base: 12, renewal: 19 },
      "US-OR": { currency: "USD", locale: "en-US", base: 12, renewal: 19 },
      DE: { currency: "EUR", locale: "de-DE", base: 11, renewal: 18 },
      GB: { currency: "GBP", locale: "en-GB", base: 10, renewal: 16 },
    };
    document.getElementById("ai-region").addEventListener("change", (e) => {
      const r = regions[e.target.value];
      Object.assign(saas, { currency: r.currency, locale: r.locale, base: r.base, renewal: r.renewal, region: e.target.value });
    });

    document.getElementById("ai-show").addEventListener("mv-change", (e) => { list.headline = e.detail.value; });
    document.getElementById("ai-sort").addEventListener("mv-change", (e) => { list.sort = e.detail.value; });
  </script>
</div>

API

Attributes

Name类型DefaultDescription
basenumber0Advertised base price per unit and per period (the rate drip pricing would show alone). Shown secondary to the total, or as the headline with headline="base".
quantitynumber1Units bought (nights, tickets, seats). Multiplies the base and every data-per="unit" line.
min-quantitynumberMinimum billable units (per-seat minimums). The total is computed on max(quantity, min-quantity); a flag says “Minimum 3 seats: priced for 3” when the request is below it, “Minimum 3 seats” otherwise.
unit / unitsstringUnit name, singular and plural (units defaults to unit + "s"): “$164/night”, “total for 3 nights”, “$89 × 2 tickets”.
periodday | night | week | month | year | stringMakes the price recurring: “$12/seat/mo”, headline “$39.19 /mo total”.
billed-everynumber1Periods charged at once (12 with period="month" = billed annually). The headline becomes the amount charged (“billed annually”) and a flag gives the per-period equivalent.
renewalnumberBase price per unit after the intro periods. The renewal total is computed with the same fees, taxes and quantity (one-time lines excluded) and flagged next to the price: “Then $62.03/mo after 3 months” (warning tone when higher), plus a “Then, from month 4” row in the breakdown.
intro-periodsnumberNumber of periods billed at the base (intro) price before renewal applies. 1 reads “after the first year”; 12 months too.
frombooleanThe price is a starting price: “From” precedes the headline, the sentence starts with “From” and a flag says options can raise it.
regionstringBuyer region code (e.g. US-NY, DE, GB). Lines with data-region apply only when it matches (a line region “US” also matches “US-NY”; several codes separated by spaces or commas; “*” always applies).
currencyISO 4217 codeUSDCurrency of every amount; rounding follows its minor units (JPY has none).
localeBCP 47 tagen-USNumber formatting locale. Whole amounts drop the cents ($164), others keep them ($187.40).
headlinetotal | basetotalWhat the big number shows. base keeps the total on the line right under it (“$713.05 total · incl. $221.05 fees & taxes”) and in the accessible sentence. Inherited from an enclosing <mv-all-in-list headline> when not set.
base-labelstringBase priceName of the advertised price in the secondary line and the breakdown (“Room rate”, “Intro price”, “Ticket price”).
openbooleanBreakdown expanded. Reflects the disclosure state; setting it opens or closes the panel and emits mv-breakdown-open / mv-breakdown-close.
data-sizesm | lgCompact (list rows, cards) or large (booking summary) headline. Default is medium.
data-headline / data-loweststateSet by the component (styleable): the effective headline mode, and data-lowest on the price with the lowest total inside an <mv-all-in-list>.
sort (mv-all-in-list)none | total | basenoneReorders the list children by all-in total or by advertised base price, lowest first (stable, original order for ties); none restores the original order.
headline (mv-all-in-list)total | baseHeadline mode for every contained price that doesn't set its own, so all cards compare on the same number.
highlight (mv-all-in-list)lowest | nonelowestMarks the price with the lowest total (data-lowest + a “Lowest total” chip) when the list has two items or more.

Properties

Name类型Description
linesArray<{ id?, label, amount?, rate?, kind?, per?, on?, optional?, once?, due?, note?, region? }>Declared charges. Setting an array replaces the data-fee children; null goes back to them. Reading returns normalized copies. amount is a fixed amount (discounts may be negative), rate a percentage (8.875 = 8.875%); kind: fee | tax | delivery | surcharge | discount; per: order | unit; on: base | subtotal | total (defaults: tax → subtotal, surcharge → total, others → base).
extrasstring[]Ids of the optional extras the customer added. Extras start unchecked; set it only to restore a choice the customer already made.
totalnumberAll-in amount for the current selection (read-only, rounded to the currency's minor units).
formattedTotalstringThe total formatted for the locale and currency (read-only).
summaryobjectRead-only computation: { currency, locale, region, quantity, requestedQuantity, cycles, base, baseUnit, fees, taxes, discounts, extras, total, dueNow, dueLater: [{ due, amount }], renewal, lines: [{ id, label, kind, amount, optional, due }] }.
sentencestringThe accessible sentence used as the price's name (read-only).
stringsPartial<Record<string, string>>Overrides for every visible text and announcement, with {placeholders} (e.g. total, totalFor, basePrice, plusFeesTaxes, noFees, toggle, toggleCount, groupFees, groupTaxes, dueLater, thenRow, extras, extrasHint, flagRenewal, flagRenewalFirst, flagMinimumApplied, flagBilled, flagDue, flagFrom, lowest, sentence, including, announce, extraAdded; period names and suffixes of the built-in periods: periodDay … periodHour ("month|months", plural form picked for locale), perDay … perHour ("/mo"); sentencePer, sentenceFor, cyclesDetail, unitFallback, unitsFallback, chargeFallback). English defaults. <mv-all-in-list> has its own strings (listSorted, listUnsorted, listTotals, sortTotal, sortBase).
items (mv-all-in-list)Element[]The list's current children in display order (read-only).

Methods

NameDescription
toggle(force?)Opens or closes the breakdown like the button (opening emits the cancelable mv-breakdown-open). Returns false if the opening was cancelled.
refresh()Re-reads the declarations and redraws. Changes to data-fee children and attributes are picked up automatically; this is for bulk edits or custom flows. On <mv-all-in-list>, re-sorts and re-marks after you insert items.

Events

NameDescription
mv-breakdown-openThe breakdown is opening. Cancelable when triggered by the button or toggle(). detail: { total, summary }.
mv-breakdown-closeThe breakdown closed. detail: { total }.
mv-extra-changeCancelable, before an optional extra is added or removed by the customer. detail: { id, label, added, amount }. preventDefault() keeps the previous state (e.g. out of stock).
mv-total-changeThe total changed (quantity, region, currency, extras, declarations). detail: { total, previous, summary }. Not emitted on first render.
mv-sort (mv-all-in-list)The list was reordered after its sort changed. detail: { by, items }.

Content structure

NameDescription
[data-fee] childrenOne element per charge, hidden: its text is the label, data-fee the fixed amount (empty with data-rate="8.875%" for a percentage), plus data-kind, data-per="unit", data-on, data-optional, data-once, data-due="at the property", data-note (plain-language explanation), data-region and data-id.

CSS classes

NameDescription
mv-all-in-pricePrice block: -lowest (chip), -headline (-from, -amount, -suffix; aria-hidden), -sub (-base, -base-label, -base-value, -delta[data-tone=fees|clear|extras|discount], -total-inline) and the visually hidden -sentence.
mv-all-in-flags / mv-all-in-flagList of hidden-cost flags; data-tone="warning" | "info", data-flag="renewal" | "minimum" | "billed" | "due" | "once" | "from".
mv-all-in-toggleDisclosure button: -toggle-label, -toggle-count, -chevron.
mv-all-in-panelCollapsible panel (data-open, inert while closed) with -panel-inner, the breakdown -table (tbody[data-group=base|fees|taxes|extras|discounts], -group, -item, -item-label, -item-note, -tag, -value, -total-row, -due-row, -then-row).
mv-all-in-extrasOptional extras fieldset: -extras-legend, -extras-hint, -extras-list, -extra (label), -check, -extra-label, -extra-note, -extra-amount.

CSS variables

NameDefaultDescription
--mv-all-in-flagvar(--mv-warning)Tone of warning flags (renewal higher than intro, minimum applied) and of the renewal row.
--mv-all-in-goodvar(--mv-success)Tone of “No added fees” and of the “Lowest total” chip.

Accessibility

The visible price is aria-hidden and replaced for assistive technologies by one complete, visually hidden sentence that always starts with the total, whatever the headline mode: “$713.05 total for 3 nights, including $221.05 in fees and taxes; base price $164 per night” (“From”, extras added, discounts and billing cycle are woven in), so a screen reader never hears a base price without its total. Hidden-cost flags are a real list of plain text next to the price, each with an icon and words (color is never the only signal). “What’s included” is a native <button> with aria-expanded and aria-controls pointing to the panel; the collapsed panel is inert and hidden, Escape inside it collapses it and returns focus to the button. The breakdown is a real <table> with a caption, column headers, row headers per charge, rowgroup headers (Fees, Taxes, Extras you added) and a tfoot with the total, due-now / due-later rows and the renewal row; notes and tags (“Due at the property”, “minimum”) sit inside the row header so they are read with their charge. Optional extras are a <fieldset> with a legend and a hint (“Not included unless you add them.”), native checkboxes wrapped in their <label>, described by their note; they are never pre-checked. Total changes (quantity, region, extras) are announced politely once, debounced (“Breakfast for two added. Total now $843.46.”); inside <mv-all-in-list> the list announces instead (“Totals updated. Lowest: Ticketvale, $250.70.” / “Sorted by total price, lowest first.”) and reordering keeps DOM order equal to visual order and restores focus to the element that had it. Keyboard: Tab reaches the button, the extras and nothing decorative; visible focus rings, forced-colors borders on flags and chips. Reduced motion (OS or data-motion="reduce"): the panel, total and list reorder change without animation. Known limits: it displays and adds up what the app declares (it is not a tax engine and does not make a checkout compliant by itself); without the units attribute the plural of unit is unit + "s", and a custom period (other than day, night, week, month, year, hour) is pluralized the same way, so set units and strings for other languages; each line is rounded to the currency's minor units before summing.

本页面由 AI 翻译。报告翻译问题