跳至主要内容

RFC 2026-04-24-media-input-mark-index: MediaInput markIndex — 指定項目的視覺標記機制

狀態: rejected · 建立: 2026-04-24 · 決策: 2026-04-28 · 範疇: web

摘要

MediaInput 支援「標記特定項目」的視覺提示(徽章),並在所有 viewModethumbnail / single / carousel)中一致呈現。主要應用場景:電商商品的主圖標記

動機

使用場景

某 ecommerce 專案的商品管理需求:

  • 商品上傳時可放 1–6 張圖片
  • 第一張自動視為主圖,其餘為副圖
  • 使用者需要在 UI 上明確看到「哪一張是主圖」

目前的限制

MediaInput 目前沒有提供「標記特定項目」的機制。消費端(applet)嘗試以 CSS 繞道實作會碰到下列問題:

  1. 依賴框架內部 DOM:用 :is(.grid > div:first-child)::before 這類 selector 直接針對縮圖網格的第一張注入 ::before 徽章。框架內部 DOM 若重構,徽章會靜默失效(CSS selector 不命中,使用者看不到徽章,自動化測試也不易攔截)

  2. 跨 viewMode 不一致:viewMode 切換為 single 時(以下情境會發生),縮圖網格不存在,::before selector 無法命中,徽章直接消失:

    • items.length <= 1
    • 容器寬度 < SMALL_WIDTH_THRESHOLD(手機版)
    • 使用者點擊某張縮圖進入大圖預覽
  3. i18n 障礙:CSS content 屬性若硬編字面值,會失去 i18n 能力;消費端若以 CSS 變數 + React style 注入可解,但語法冗長脆弱

  4. 色彩主題耦合:消費端自行決定徽章色時,容易誤用非 DaisyUI 語意色,與設計語言脫鉤

  5. 重用性:每個 applet 都要重寫一遍相同的 CSS utility + React 注入邏輯

以上問題實質上是「框架缺少 API 導致消費端被迫 hack」的信號,適合由框架提供第一方支援。

建議設計

API

<MediaInput
name="images"
control={control}
multiple
maxFiles={6}
displayMode="thumbnail"

// 新增 props
markIndex={0} // 要標記的 item index
markLabel={t('Main')} // 徽章文字(必須接受 i18n 字串)
markColor="primary" // DaisyUI 語意色(預設 primary)
/>

Props 型別

export interface MediaInputProps<TFieldValues> {
// ... 既有 props

/**
* 要視覺標記的 item index(0-based)。
* 未提供時不顯示標記。
* 常用於標記主圖、精選項等。
*/
markIndex?: number

/**
* 標記徽章的文字。markIndex 有值時必填。
* 由消費端以 i18n 工具(如 t('Main'))傳入。
*/
markLabel?: string

/**
* 徽章色彩,對齊 DaisyUI 語意色。
* @default "primary"
*/
markColor?: 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error' | 'neutral'
}

視覺行為(各 viewMode 下的一致性)

viewMode徽章位置
thumbnail(縮圖網格)對應 index 的縮圖左上角
single(大圖預覽)當前顯示圖 index === markIndex 時,在大圖左上角;否則不顯示
carousel(自動輪播)single(輪到 markIndex 時顯示)

徽章一律使用 DaisyUI 語意色 + 圓角 pill(rounded-full)+ primary-content 文字色,與平台語意色系統對齊,不依賴 Tailwind 硬編色階。

互動

  • 徽章為純視覺指示(pointer-events: none),不攔截點擊
  • 使用者拖曳縮圖改變順序時,markIndex 不跟著移動(由消費端決定語意:是「第一張永遠是主圖」還是「某個固定的 item 是主圖」)

與既有行為的相容性

面向相容策略
既有 items/value/onChange不變
markIndex 傳入零視覺變化,完全向後相容
markIndex 超出 items.length 範圍靜默忽略,不報錯(item 可能被刪除)
readOnly / disabled 組合徽章照常顯示(純視覺指示,不涉互動)
與拖曳重排組合徽章依 markIndex 顯示;若消費端希望「第一張永遠是主圖」,應在 onChange 中固定 markIndex={0}

破壞性變更: 否(純新增 props,未提供時行為不變)

實作建議

1. SortableThumbnail 增加 props

interface SortableThumbnailProps {
// ... 既有
isMarked?: boolean
markLabel?: string
markColor?: MarkColor
}

縮圖內部加入條件 render:

{isMarked && markLabel && (
<div
className={cn(
'absolute top-2 left-2 z-10 rounded-full px-2 py-0.5 text-xs font-semibold pointer-events-none',
markColorClass(markColor),
)}
>
{markLabel}
</div>
)}

2. MediaInputrenderThumbnailGrid 傳遞 isMarked

{items.map((item, index) => (
<SortableThumbnail
key={item.id}
// ... 既有
isMarked={markIndex === index}
markLabel={markLabel}
markColor={markColor}
/>
))}

renderSingleView(或等價位置)偵測 currentIndex === markIndex 時加 overlay 徽章,樣式與縮圖版本一致(保持跨 viewMode 視覺一致)。

4. Storybook 範例

media-input.stories.tsx 新增 story:

  • WithMarkIndex — 第一張顯示「Main」徽章
  • MarkIndexCarousel — 輪播時跟著當前 index 切換

測試建議

  • Unit: markIndex 邊界值(undefined0items.length - 1、超出範圍)
  • Visual: Storybook play test 驗證各 viewMode 下徽章位置
  • Accessibility: 徽章文字應納入 aria-label(如「第 1 張,已標記為 Main」)

替代方案

方案 A: 第一方 API(本 RFC 採用)

見「建議設計」。

方案 B: 維持消費端 CSS hack

  • 優點:框架零改動
  • 缺點:依賴內部 DOM、跨 viewMode 不一致、i18n 障礙、每個 applet 重複工作
  • 為何不選:上述限制已列於「動機」,是本 RFC 存在的理由

方案 C: 擴展為 marks: Array<{index, label, color}> 多標記結構

  • 優點:支援多標記(主圖 + 精選)
  • 缺點:API 複雜度提高、目前沒有具體 use case
  • 為何不選:YAGNI。未來若需要可從 markIndex 演進為 marks[](保留擴展性)

不屬於本提案的範圍

  • 「可點擊變更主圖」的按鈕: 屬於消費端業務邏輯,應自行決定 markIndex 的值(本提案只提供顯示機制)
  • 多個徽章(如「主圖」+「精選」同時標記): 未來若有需求,可從 markIndex: number 擴展為 marks: { index, label, color }[]; 目前保持最小必要 API
  • 徽章位置客製化(角落、置中等): 目前固定左上角,與列表頁慣用的 Badge 位置一致

消費端遷移示意

<CollapsibleCard title={t('Images')}>
- <div className="main-image-badge" style={{ '--main-image-label': `"${t('Main')}"` }}>
<MediaInput
name="images"
control={control}
multiple
maxFiles={6}
displayMode="thumbnail"
+ markIndex={0}
+ markLabel={t('Main')}
helperText={t('First image will be the main image. Max 6 images. Drag thumbnails to reorder.')}
/>
- </div>
</CollapsibleCard>

消費端同時可移除自訂的 main-image-badge CSS utility。

Decision Log

2026-04-28 — Rejected

  • 決策者: ada
  • 拒絕理由: 重新評估後確認此提案不符合框架核心職責邊界。MediaViewer / MediaInput 經 2026-04-25 重構後,已建立明確的擴展策略「整塊 slot 的 escape hatch(renderThumbnailrenderThumbnailContainertoolbarSlot)+ 內建關鍵業務行為(DnD、remove)」;繼續加裝飾性 prop(markIndex / tags)會侵蝕這個剛建立的分工,把「主圖徽章」這種業務語意塞進媒體輸入元件,違反單一職責。原 RFC 動機提到的兩大消費端痛點,框架已從另外的角度解掉:
    1. i18n 障礙 — 2026-04-21 commit c8aa732 已加入「表單組件自動翻譯 aria-label/placeholder」,CSS content 字面值的 i18n 痛點已不存在
    2. 依賴框架內部 DOM — 2026-04-25 commit 8d627ef 引入 renderThumbnail slot 後,consumer 若真要自訂縮圖(含徽章)有正規 escape hatch 可走,不需 hack 內部 selector
  • 替代建議(給應用端):
    1. Helper text + 順序語意(首選): helperText="First image will be the main image" + 視覺順序(第一張即主圖)已能傳達語意,多數情境不需要徽章
    2. 應用端 overlay 自包: 若視覺上仍要徽章,在 MediaInput 外層用絕對定位 overlay 疊圖(限定 displayMode="thumbnail" 場景;接受 single/carousel 不顯示)
    3. MediaViewer + 自製 renderThumbnail(高成本): 不使用 MediaInput,直接用 MediaViewer 並提供完整 renderThumbnail。此方案會失去 MediaInput 的拖曳、上傳、表單整合,成本本身就是訊號——若這個成本不值得,代表需求不普遍到該進框架
  • 未來重新審視的條件(若以下任一成立可重新提案):
    • 跨 ≥3 個專案的 consumer 都重複實作類似徽章 hack,顯示通用性
    • MediaViewer 演化出新的「裝飾性 slot」概念(如 renderThumbnailDecoration),可作為 markIndex / tags 的通用基礎設施,屆時 MediaInput 可透傳該 slot
  • 決策過程詳見: 2026-04-28 與應用團隊(ada)的重新評估討論——確認 2026-04-25 之後的架構演進已改變判斷前提,原 deferred 條件中「加入方案 D(render prop)」的方向被框架自身的擴展點實作所滿足,無需在 MediaInput 加新 prop

2026-04-25 — Deferred

  • 決策者: ada
  • 延後理由: 方向有價值(消費端痛點真實),但關鍵技術問題未處理;render prop 方向未列入替代方案,需補足後重新審議
  • 三 AI 審議結論:
    • Gemini:建議 A 微調
    • Codex:建議 C(render prop / slot),堅持 A/B 則 reject
    • Claude sub-agent:建議 C + A 便利糖
    • 投票 2:1 傾向 C
  • 重新評估條件(提案者修訂 RFC 後重新審議):
    1. 加入「方案 D:renderItemBadge / slot 模式」並與 A、B 完整比較
    2. 解決「拖曳重排後 markIndex 語意」問題(具體機制或明確使用限制)
    3. markLabel 改為符合 m-web-common.md 的 i18n 機制(組件內部 t(),prop 傳 key 而非 t('Main')
    4. 補充 a11y 具體策略(alt / aria-label / live region 的責任分工)
    5. 補充與既有刪除按鈕(Top-Right)的佈局競爭分析
    6. 考慮命名:pin- / feature- 是否比 mark- 更精確
    7. 若採 render prop,框架端同時提供 <MediaBadge> helper 組件以保住 DaisyUI 視覺一致性
  • 品質維度修正(覆寫原 checklist 評分):
    • API 設計:Clear → Partial(reorder 語意 / 命名論證不足)
    • 替代方案:Clear → Partial(render prop 方向缺席)
    • 測試/驗證:Clear → Partial(缺拖曳互動、readOnly、a11y 驗證準則)

2026-04-24 — Under Review

  • 入站至 docs-web/rfcs/
  • 等待框架維護者以 /rfc-intake decide 2026-04-24-media-input-mark-index 執行審議

2026-04-24 — Proposed

  • 提案者: ecommerce 專案團隊
  • 來源: ecommerce

相關連結

  • 觸發此提案的消費端情境: 某 ecommerce 專案的商品管理 US(P.UAT 階段)

最後更新: 2026-04-24