**Scope.** Let the user fill the Caption column from the Title column via a `{title}` variable in the per-column default value, with a one-click "link" affordance between the two headers when they sit next to each other.
**What it solves.** "Many times the caption is the title (or the title plus a few words)." Today filling a caption per row is fully manual; the user wants a column-level recipe like `Photo of {title}` that resolves to the right caption per file.
== What's wanted ==
1. **`{title}` variable in caption defaults.** The Caption column's per-column default value (already settable from the column header chevron menu — see `HeaderMenuPopover` in `src/table.jsx:4047`) gains support for a `{title}` token. When the default is applied to a row (via the existing "Apply to blank cells / Apply to all selected / Overwrite selected / Overwrite all" split-button at `src/table.jsx:4280-4354`), `{title}` is replaced by **that row's title**.
2. **Free-form prefix and suffix.** No separate "prefix" / "suffix" inputs — they're just the literal text the user types around the token. e.g. default = `Photo of {title} — taken by me` → row with title `Sunset over Lake` resolves to `Photo of Sunset over Lake — taken by me`. The whole default field is a template string.
3. **Strip the auto-numbering placeholder from `{title}`.** The auto-sequence task ([T425984](https://phabricator.wikimedia.org/T425984)) introduces a trailing ` #` that gets resolved to a concrete integer at publish time. The `{title}` substitution must NOT include that ` #`. Strip a trailing `\s*#\s*$` before substitution (regardless of whether T425984 has shipped — the convention is forward-compatible). Same applies to whatever final form T425984 lands on; if the placeholder character changes, this code stays the single point of stripping.
4. **Inter-header link icon (the "linking icon" the user described).** When Title (`title`) and Caption (`description`) sit **adjacent** in the visible column order (in either direction — Title→Caption or Caption→Title), render a small chain icon centered on the boundary between their two header cells. Clicking it:
- Opens the Caption column's `HeaderMenuPopover` (existing chevron menu).
- Pre-expands the "Set default value" panel.
- If the current default is empty, pre-fills it with `{title}`. If non-empty, leaves it alone (don't clobber a user's existing string) but still opens the panel so the user sees the field and can edit.
- Tooltip: "Link Caption ← Title".
- Visible only when adjacent — when the columns aren't neighbors, the icon doesn't render at all.
5. **Caption header dropdown is the always-available fallback.** The Caption column header chevron already opens `HeaderMenuPopover`. Add a dedicated menu item — "Insert {title} variable" — above (or alongside) "Set default value", that does the same pre-fill as the link-icon click. This is the path users take when the columns aren't adjacent (e.g. Caption is far to the right with other columns in between).
== Where the code lives ==
- **Header rendering / column adjacency check.** `src/table.jsx:786` — the `visibleColumns.map((c, ci) => <HeaderCell ...>)` loop. Adjacency = check `visibleColumns[ci]` and `visibleColumns[ci+1]` against the `{ "title", "description" }` pair.
- **Header link icon visual.** New element rendered between two adjacent `HeaderCell`s. Two practical placements:
- Inside the right-hand `HeaderCell`, anchored at its left edge (negative margin).
- As a sibling absolutely-positioned element overlaying the column boundary.
Either approach is fine; the icon itself is `Icon name="link"` (already in `src/icons.jsx:42`). 14-16 px, opacity ~0.55 idle, 1.0 on hover, with a tooltip via `title="..."`. Click opens Caption's `HeaderMenuPopover` (programmatically set `headerMenu` state in `Table` to `{ colKey: 'description', anchorEl: <icon ref> }`).
- **Default-value editor for `description`.** `src/table.jsx:4233-4242` — currently the default branch (just an `<input>`). Two tweaks:
- Add a **"Insert {title}" chip/button** next to the input that appends `{title}` at the cursor (or at the end if focus isn't in the input). Mirrors the existing "Me" / "{{own}}" quick-insert pattern at `src/table.jsx:4189-4196` (author) and `src/table.jsx:4223-4231` (source).
- Below the input, a small live-preview line: `Preview: <expanded value from a sample row>`. Helps the user see what their template will produce.
- **Token expansion at apply-time.** Currently `applyDefault` / `overwriteWithDefault` in `Table` (`src/table.jsx:912-935`) write `def` into `it[k]` directly. For Caption, route through a new helper `expandCaptionTemplate(template, item) → string` that:
- Returns `template` unchanged if `template` doesn't contain `{title}`.
- Otherwise replaces `{title}` with `stripSequencePlaceholder(item.title)` where `stripSequencePlaceholder` removes a trailing `\s*#\s*$`.
- Returns the resulting string (may be empty if `template` was just `{title}` and the row has no title — that's fine, "apply to blanks" already only fills when the destination is blank, so this is a no-op for blank-blank rows).
- **Header menu item insertion.** `src/table.jsx:4251-4432` (`<ul className="hdr-pop__menu">`). Add a new `<li>` for "Insert {title} variable" gated to `col.key === 'description'`. Clicking it: expands the default-value panel and appends `{title}` to `value`. (Reuse the same `onChange` already wired to `setColumnDefaults`.)
- **No new persistence layer.** The default value lives in `columnDefaults['description']` which is already persisted via `setPref('columnDefaults', ...)` (see `src/api/user-store.js:770`). Just stays a string; the `{title}` token is data, not metadata.
== UX details ==
- Per-column default of `{title}` for Caption is treated as "no default" by `hasDefault` indicator dot in the header (`src/table.jsx:1288`)? No — keep treating it as having a default (the dot is a generic "this column has a default" marker; users will quickly learn that the dot for Caption means "templated").
- Clicking the link icon when a default already exists: open the popover, expand the panel, scroll the input into view — no overwrite. If the existing default doesn't contain `{title}`, the popover surfaces the "Insert {title}" chip so the user can append it.
- The token `{title}` is the only one in v1. If we later want `{filename}`, `{author}`, etc., the same expansion helper grows; out of scope here.
- Case-sensitive match for `{title}` — keep it simple. We can normalize or accept `{Title}` later if needed.
== Acceptance ==
- Caption header chevron → "Set default value" → typing `Photo of {title}` and clicking "Apply to blank cells" fills every blank caption with `Photo of <that-row's-title>`.
- A row whose title ends with the T425984 sequence placeholder (e.g. `Sunset over Lake #`) resolves the variable to `Sunset over Lake` (no trailing `#`, no trailing whitespace).
- A row whose title is empty resolves the variable to the empty string; the rest of the template still renders.
- When Caption and Title are adjacent (regardless of direction), a chain icon appears between the two header cells; clicking it opens the Caption header menu with the default-value panel expanded.
- When Caption and Title are NOT adjacent, the icon does not appear; the "Insert {title} variable" item in the Caption header dropdown is the access path.
- The default-value editor for Caption shows a one-click "Insert {title}" chip and a small live preview of the expanded result for a sample row.
- `{title}` substitution is one-shot at apply time — editing a title later does NOT retroactively update existing captions (consistent with the existing "default value + apply scope" model). The user re-clicks "Apply / Overwrite" if they want a refresh.
- No regressions to other columns' default-value editors or to the Caption column's existing per-row editing.
== Out of scope ==
- Live two-way binding (caption auto-updates when title changes after the first apply). The existing model is one-shot fill; this task stays consistent with it.
- Other variables besides `{title}` (e.g. `{filename}`, `{author}`, `{date}`). Easy to grow later — keep the expansion helper extensible but only `{title}` is wired now.
- Reverse direction (`{caption}` substituting into Title). The user explicitly asked Caption ← Title.
- Per-row override: if the user clicks "Apply to blank cells" with a `{title}` template and one row has a custom non-blank caption, that row stays untouched (existing apply-to-blanks semantics).
**Source:** User feedback session ([T426424](https://phabricator.wikimedia.org/T426424)).
**Related:** [T425984](https://phabricator.wikimedia.org/T425984) (auto-sequence ` #` placeholder — informs the strip-rule for `{title}` expansion), and the existing column-header menu plumbing at `src/table.jsx:4029` (HeaderMenuPopover).