サンドボックスの@メンションにアバター画像を追加し、スタイルリグレッションを防止

basecamp/lexxy

サンドボックス環境の@メンションにアバター画像を導入し、メンション付きの画像スタイリングのリグレッションを早期に検出できる体制を整えました。合わせて、メンションチップのCSSも視覚的に整理されています。

背景

メンション機能における画像スタイリングのリグレッションが繰り返し発生していました。サンドボックス(test/dummy アプリ)の@メンションプロンプトアイテムは、これまでイニシャル文字列のみを表示する簡素な実装であったため、実際のアバター画像を伴うレイアウト崩れを事前に確認する手段がありませんでした。

PR本文では「特にメンションにおける画像スタイリングのリグレッションがいくつか発生した」と明示されており、本PRはその再発防止を目的としています。サンドボックスを本番に近い状態に保つことで、新機能開発時にスタイル上の問題を早期発見できるようにしています。

技術的な変更

アバター画像のサンドボックスへの組み込み

test/dummy/app/assets/images/user-1.jpguser-5.jpg の5枚のユーザーアバター画像が追加されました。これらの画像を各プロンプトアイテムテンプレートで参照するよう、2つのパーシャルが更新されています。

people/_prompt_item.html.erb では、menu テンプレート(ドロップダウン表示)と editor テンプレート(エディタ内埋め込み)の両方にアバター画像が追加されました。ランダム選択によって毎回異なる画像が使われるため、複数パターンの表示を確認しやすくなっています:

<% avatar_id = 1 + rand(5) %>
<% user_avatar = asset_path("user-#{avatar_id}.jpg") %>
<template type="menu">
  <span class="person person--prompt-item">
    <img src="<%= user_avatar %>" class="person--avatar" alt="">
    <span class="person--name"><%= person.name %></span>
  </span>
</template>
<template type="editor">
  <span class="person person--inline">
    <img src="<%= user_avatar %>" class="person--avatar" alt="">
    <span class="person--name"><%= person.name %></span>
  </span>
</template>

groups/_prompt_item.html.erb では、グループメンション(editor テンプレート)にも user-1.jpg が固定で追加されました。グループのメニュー表示のイニシャルは "GR" から "G" に短縮されています。

.person--avatar スタイルの刷新

test/dummy/app/assets/stylesheets/sandbox.css において、.person--avatar のスタイルが <img> タグを含む実装に対応する形に整理されました。

変更前:

.person {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;

  .person--avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 50%;
    background-color: oklch(70% 0.15 260);
    color: white;
    font-size: 0.5rem;
    font-weight: 600;
    flex-shrink: 0;
  }

  &.person--inline {
    background-color: oklch(0.7 0.175 240 / 15%);
    border-radius: 0.25rem;
    padding: 0 0.25rem;
  }
}

変更後:

.person {
  display: inline;

  .person--avatar {
    aspect-ratio: 1;
    background-color: var(--lexxy-color-accent-dark);
    block-size: 1lh;
    border-radius: 50%;
    display: inline-flex;
    inline-size: 1lh;
    vertical-align: text-bottom;

    &:not(img) {
      align-items: center;
      color: var(--lexxy-color-ink-inverted);
      font-weight: 600;
      justify-content: center;
      text-align: center;
    }
  }

  &.person--inline {
    background-color: oklch(50% 0 0 / 0.1);
    border-radius: 20px;
    box-shadow: 0 0 0 2px oklch(50% 0 0 / 0.1);
    padding-inline-end: 0.25ch;

    .node--selected & {
      background-color: var(--lexxy-focus-ring-color);
      box-shadow: 0 0 0 2px var(--lexxy-focus-ring-color);
    }
  }
}

サイズ指定が width/height の固定ピクセルから block-size: 1lh / inline-size: 1lh のフォント行高基準に変わり、テキストサイズに追従するようになりました。&:not(img) セレクタでイニシャル表示(<span>)と画像(<img>)のスタイルを分岐させており、どちらの要素型でも .person--avatar クラスを共用できる設計です。

lexxy-editor.css のメンションチップ修正

app/assets/stylesheets/lexxy-editor.css では、メンションノードの削除ボタン周りのスタイルが調整されました。border-start-end-radius: 0border-end-end-radius: 0(メンションチップとボタンを接合するための角丸の打ち消し)が削除され、代わりに inset-inline-start: 0lexxy-node-delete-button に追加されています。また、ボタン内の border-radiusvar(--floating-tools-radius) から 50% に変更され、削除ボタンが常に円形になりました。

設計判断

サイズの基準をフォント行高(1lh)に統一する アプローチが採用されました。固定ピクセルによるサイズ指定では、テキストサイズの変動に対してアバターが追従できないという問題がありましたが、lh 単位を使うことでインラインコンテキストにおける自然なスケーリングが実現されています。

&:not(img) を使ったセレクタ分岐は、イニシャル表示と画像表示を同一クラスで管理しながら、それぞれに適したスタイルを当てるシンプルな実装です。<img> 要素に align-items: center などのフレックス配置プロパティを適用しても意味がないため、<span> 等の非画像要素のみに絞っています。また、カラー値もハードコードされた oklch(70% 0.15 260) からCSSカスタムプロパティ(var(--lexxy-color-accent-dark))に切り替えられており、テーマとの一貫性が高まっています。

まとめ

この変更は、サンドボックスを実際のユースケースに近い状態に保つことでスタイルリグレッションの早期発見を可能にするものです。アバター画像の導入に合わせてCSSの実装も整理されており、imgspan を同一クラスで扱える柔軟な設計が確立されています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
1f9d0140

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)、背景、技術的な変更、設計判断(各論)、まとめ(結論)の3部構成が明確で、理想的な記事構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライトとGitHubのPRリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

専門用語を適切に用い、エンジニア読者に合わせた過不足のない説明がなされています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションとパラグラフが総論→各論→結論の構造を持ち、トピックセンテンスが明確で非常に読みやすいです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードブロックや説明は、提供されたDiffの内容と完全に一致しており、正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「リグレッション」「サンドボックス」「lhユニット」などの技術用語が文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

「テキストサイズに追従する」「スタイルを分岐させる」といったコード変更の技術的な帰結に関する説明が論理的かつ正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのDescriptionやDiff内のコードによって裏付けられており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#816)、ファイルパス、CSSの値などがすべて正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRのタイトル「Avatar for sandbox prompts」の内容を汲み取り、変更の目的である「スタイルリグレッションの防止」まで含んでおり、PRの内容を的確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報にない外部知識(バージョン情報、リリース予定など)の追記はなく、提供された情報源に忠実です。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「繰り返し発生していた」といった過去の事象や、「早期に検出できる」といった変更の目的について、時間表現がPRの内容と一致しています。