event-manual / chapter 19

手動ファイン調整エディタ

@event/session-card

SessionCardDesigner@event/session-card)は ivs の /admin/session-card/[id] designer の忠実移植 + 汎用化である。Session / API 直結を外し、CardModel を編集して onChange(patch) を発行するだけの存在にした。永続化は呼び出し側の仕事で、onSave prop があるときだけ SAVE ボタンが出る。

機能一式

内部編集状態は mount 時の model から初期化する defaultValue 方式なので、別カードへ切り替えるときは key={model.id} で remount する

onResolved auto-prefill — 核心トリック

textarea の初期値問題: 「いまカードが実際にどう改行しているか」を近似計算で出すと、プレビューと textarea が微妙にずれる。ivs の解法はカード自身に教えさせることである。

  1. 画面外に「上書きを全部空にした純自動カード」(autoModel)をもう1枚描画する
  2. そのカードがレイアウト確定時に onResolved: (r: ResolvedCardLines) => void自分が実際に描画した行割りtitleLines / roleLines: Record<speakerId, string[]>)を親へ報告する
  3. designer はこれをユーザー未編集の textarea に流し込む。カードの実計算そのものなので、プレビューの改行と textarea が完全一致する

ユーザーが手で書き換えた行は editedRoles(Set<speakerId>)/ titleEdited で追跡し、プレフィルで潰さない。↺ ボタンで編集フラグを外すと auto カードの行割りに戻る。プレフィルは表示用であって上書きではないため、onChange は発行しない — 保存されるのは「ユーザーが編集した行」だけで、自動改行を上書きとして凍結しない。

永続化 — OverlayStore single-record PATCH

createContentOverlayStore({ store, kv? })@event/session-card/persist、server 専用 subpath)が ivs の修正済みプロトコルを実装する:

cardSpeakerIds のタイムスタンプ規則

last-writer-wins(第6章)の手動側基準 cardSpeakerIdsUpdatedAt は、値が実際に変わった時だけサーバ時刻を刻む。同じ並びを保存し直しただけで時刻が更新されると、4S 側の新しい lineup 編集を不当に打ち負かすためである。選択をクリアした場合はタイムスタンプごと削除する。

starter の配線例

// CardStudio.tsx — CardModel → CardOverlay の写像(lang で JP/EN どちらへ書くか分岐)
function modelToOverlay(model: CardModel): CardOverlay {
  const isEn = model.lang === "en";
  return {
    cardVariant: model.variant,
    cardTheme: model.theme,
    cardSpeakerIds: model.speakers.map((s) => s.id),
    cardScale: model.scale,
    cardTitleScale: model.titleScale,
    cardRoleScale: model.roleScale,
    cardNameScales: model.nameScales,
    ...(isEn
      ? { cardTitleEn: model.titleOverride ?? null, cardRolesEn: model.roleOverrides }
      : { cardTitle: model.titleOverride ?? null, cardRoles: model.roleOverrides }),
  } as CardOverlay;
}

server action 側は adminAuth.isAuthedFromValue(cookie) を再検査してから overlayStore.patch(sessionId, overlay) を呼ぶ。ページ側は listSessionsFromEvent(config, { overlay }) に content JSON の overlay を注入し、resolveCardModel(session, speakers) で描画用 model を組む。