RFC 2026-04-24-media-input-mark-index: MediaInput markIndex — 指定項目的視覺標記機制
狀態: rejected · 建立: 2026-04-24 · 決策: 2026-04-28 · 範疇: web
摘要
讓 MediaInput 支援「標記特定項目」的視覺提示(徽章),並在所有 viewMode(thumbnail / single / carousel)中一致呈現。主要應用場景:電商商品的主圖標記。
動機
使用場景
某 ecommerce 專案的商品管理需求:
- 商品上傳時可放 1–6 張圖片
- 第一張自動視為主圖,其餘為副圖
- 使用者需要在 UI 上明確看到「哪一張是主圖」
目前的限制
MediaInput 目前沒有提供「標記特定項目」的機制。消費端(applet)嘗試以 CSS 繞道實作會碰到下列問題:
-
依賴框架內部 DOM:用
:is(.grid > div:first-child)::before這類 selector 直接針對縮圖網格的第一張注入::before徽章。框架內部 DOM 若重構,徽章會靜默失效(CSS selector 不命中,使用者看不到徽章,自動化測試也不易攔截) -
跨 viewMode 不一致:
viewMode切換為single時(以下情境會發生),縮圖網格不存在,::beforeselector 無法命中,徽章直接消失:items.length <= 1- 容器寬度 <
SMALL_WIDTH_THRESHOLD(手機版) - 使用者點擊某張縮圖進入大圖預覽
-
i18n 障礙:CSS
content屬性若硬編字面值,會失去 i18n 能力;消費端若以 CSS 變數 + React style 注入可解,但語法冗長脆弱 -
色彩主題耦合:消費端自行決定徽章色時,容易誤用非 DaisyUI 語意色,與設計語言脫鉤
-
重用性:每個 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. MediaInput 在 renderThumbnailGrid 傳遞 isMarked
{items.map((item, index) => (
<SortableThumbnail
key={item.id}
// ... 既有
isMarked={markIndex === index}
markLabel={markLabel}
markColor={markColor}
/>
))}
3. Single / Carousel view 的大圖區塊
在 renderSingleView(或等價位置)偵測 currentIndex === markIndex 時加 overlay 徽章,樣式與縮圖版本一致(保持跨 viewMode 視覺一致)。
4. Storybook 範例
於 media-input.stories.tsx 新增 story:
WithMarkIndex— 第一張顯示「Main」徽章MarkIndexCarousel— 輪播時跟著當前 index 切換
測試建議
- Unit:
markIndex邊界值(undefined、0、items.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(
renderThumbnail、renderThumbnailContainer、toolbarSlot)+ 內建關鍵業務行為(DnD、remove)」;繼續加裝飾性 prop(markIndex/tags)會侵蝕這個剛建立的分工,把「主圖徽章」這種業務語意塞進媒體輸入元件,違反單一職責。原 RFC 動機提到的兩大消費端痛點,框架已從另外的角度解掉:- i18n 障礙 — 2026-04-21 commit
c8aa732已加入「表單組件自動翻譯 aria-label/placeholder」,CSScontent字面值的 i18n 痛點已不存在 - 依賴框架內部 DOM — 2026-04-25 commit
8d627ef引入renderThumbnailslot 後,consumer 若真要自訂縮圖(含徽章)有正規 escape hatch 可走,不需 hack 內部 selector
- i18n 障礙 — 2026-04-21 commit
- 替代建議(給應用端):
- Helper text + 順序語意(首選):
helperText="First image will be the main image"+ 視覺順序(第一張即主圖)已能傳達語意,多數情境不需要徽章 - 應用端 overlay 自包: 若視覺上仍要徽章,在 MediaInput 外層用絕對定位 overlay 疊圖(限定
displayMode="thumbnail"場景;接受 single/carousel 不顯示) - MediaViewer + 自製 renderThumbnail(高成本): 不使用 MediaInput,直接用 MediaViewer 並提供完整
renderThumbnail。此方案會失去 MediaInput 的拖曳、上傳、表單整合,成本本身就是訊號——若這個成本不值得,代表需求不普遍到該進框架
- Helper text + 順序語意(首選):
- 未來重新審視的條件(若以下任一成立可重新提案):
- 跨 ≥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 後重新審議):
- 加入「方案 D:
renderItemBadge/ slot 模式」並與 A、B 完整比較 - 解決「拖曳重排後
markIndex語意」問題(具體機制或明確使用限制) markLabel改為符合m-web-common.md的 i18n 機制(組件內部t(),prop 傳 key 而非t('Main'))- 補充 a11y 具體策略(
alt/aria-label/ live region 的責任分工) - 補充與既有刪除按鈕(Top-Right)的佈局競爭分析
- 考慮命名:
pin-/feature-是否比mark-更精確 - 若採 render prop,框架端同時提供
<MediaBadge>helper 組件以保住 DaisyUI 視覺一致性
- 加入「方案 D:
- 品質維度修正(覆寫原 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