Bundle Builder
組合搭售機:點選商品縮圖以 FLIP 飛入插槽,總價滾動更新,集滿指定件數即填滿折扣環並解鎖折扣。
載入預覽⋯
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
3
3
0.2
0
<BundleBuilder />
安裝
npx shadcn@latest add https://webberui.com/r/bundle-builder.json或在 components.json 設定 registries 後,改用 @webberui/bundle-builder 安裝。
安裝依賴後,從 registry JSON(/r/bundle-builder.json 的 files[0].content)複製 bundle-builder.tsx 原始碼到你的 components/ui/ 目錄:
npm install motion lucide-react clsx tailwind-merge使用
import {
BundleBuilder,
type BundleProduct,
} from "@/components/ui/bundle-builder";
const products: BundleProduct[] = [
{ id: "a", name: "可頌", price: 65, thumbnail: "🥐" },
{ id: "b", name: "拿鐵", price: 120, thumbnail: "☕" },
{ id: "c", name: "沙拉", price: 150, thumbnail: "🥗" },
{ id: "d", name: "蛋糕", price: 95, thumbnail: "🍰" },
];
<BundleBuilder
products={products}
columns={4}
slots={3}
discount={0.2}
currency="NT$"
onAddToBundle={(items, total) => console.log(items, total)}
/>;點選上方商品縮圖,縮圖會以 FLIP 動畫飛入下方插槽,插槽集滿設定的件數時,折扣環填滿並彈出折扣標籤、總價滾動至折後價。再次點選商品或插槽即可移除。
受控模式
傳入 selectedIds 即進入受控模式,改由外部狀態主導選取內容:
const [ids, setIds] = React.useState<string[]>([]);
<BundleBuilder
products={products}
selectedIds={ids}
onSelectionChange={setIds}
onUnlock={() => console.log("折扣已解鎖")}
/>;Props
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
products | BundleProduct[] | — | 商品格要顯示的商品清單 |
slots | number | 3 | 需集滿的插槽數,達成後解鎖折扣 |
columns | number | 3 | 商品格欄數 |
discount | number | 0.2 | 集滿後套用的折扣比例(0–1) |
currency | string | "$" | 貨幣符號 |
locale | string | "en-US" | 數字千分位所用地區設定 |
decimals | number | 0 | 價格顯示小數位數 |
selectedIds | string[] | — | 受控:目前選取的商品 id(依加入順序) |
defaultSelectedIds | string[] | — | 非受控模式的初始選取 |
onSelectionChange | (ids: string[]) => void | — | 選取變更時觸發 |
onUnlock | () => void | — | 由未集滿轉為集滿、折扣解鎖時觸發一次 |
onAddToBundle | (products, total) => void | — | 點擊主要按鈕時觸發,回傳選取商品與折後總價 |
actionLabel | React.ReactNode | "加入組合" | 主要按鈕文字 |
className | string | — | 外層容器樣式 |
BundleProduct
| 欄位 | 型別 | 說明 |
|---|---|---|
id | string | 唯一識別碼,用於受控與 FLIP 對應 |
name | string | 商品名稱 |
price | number | 單價(未折扣) |
thumbnail | React.ReactNode | 縮圖內容,可放 emoji、圖示或任意節點 |
thumbnailClassName | string | 覆寫縮圖背景(Tailwind class) |
細節
- FLIP 飛入:以 Motion 的
layoutId共享版面動畫實作。選取後同一縮圖從商品格移到插槽,Motion 自動量測前後位置做 FLIP 過場;格內留下打勾佔位以維持版面穩定。 - 滾動總價:以
useMotionValue+animate平滑滾動到新價,逐格更新不觸發 React re-render;集滿時套用折扣並顯示刪除線原價與省下金額。 - 折扣環:SVG 環形進度隨集滿件數填滿;達標時環轉為強調色並彈出打勾與折扣百分比。
- 插槽已滿時,未選取的商品會停用;點選插槽或已選商品即可移除。
可及性
- 商品為原生
button,帶aria-pressed標示選取狀態,aria-label含名稱與價格。 - 插槽移除鍵帶「從組合移除 ⋯」的
aria-label;折扣環為裝飾性內容並標記aria-hidden。 - 選取進度與解鎖狀態透過
role="status"的aria-live區域即時播報。 - 使用者系統開啟「減少動態效果」時,FLIP、滾動數字與折扣環改為瞬間定位,不做過場動畫。