---
title: Refactor nfl_week_id Column Param UX
type: task
status: completed
description: >-
  Refactor the existing ColumnParamNflWeekFilter component (in league/app/views/components) into a
  direct-mutation ColumnParamNflWeekSelector with mutual-exclusion between static/dynamic selections
  and in-memory undo/redo; removes the Add-to-Selection staging step and extends the existing
  shift-click + click-drag range patterns to the year row.
base_uri: user:task/league/refactor-nfl-week-id-column-param-ux.md
created_at: '2026-04-22T16:27:27.679Z'
entity_id: bcfba519-6f81-4f89-b4cc-e84820fdfeec
finished_at: '2026-04-22T21:46:24.387Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - relates [[user:workflow/write/write-software-implementation-plan.md]]
started_at: '2026-04-22T20:56:05.982Z'
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-04-22T21:46:24.387Z'
visibility_analyzed_at: '2026-11-05T23:49:58.628Z'
---

# Implementation Plan: Refactor `nfl_week_id` Column Param UX

## Overview

- Refactor the existing `ColumnParamNflWeekFilter` (in `repository/active/league/app/views/components/column-param-nfl-week-filter/`) into a direct-mutation year-first selector with mutual exclusion between dynamic and static selections and in-memory undo/redo.
- Remove the "Add to Selection" builder-staging mechanism so every user action produces the final selection (no accumulation buffer).
- Rename the component from `ColumnParamNflWeekFilter` to `ColumnParamNflWeekSelector` to drop the `Filter` suffix (which implies `where`-clause semantics) while reflecting that it sets a column param.
- Preserve the existing wire format and the isomorphic `libs-shared/common-column-params.mjs` definition so server-side consumers are unaffected.

## Background

- `nfl_week_id` is defined in `repository/active/league/libs-shared/common-column-params.mjs:165-224` as `data_type: SELECT` with `values: get_all_nfl_week_identifiers()` plus five `dynamic_values` entries (`current_year_reg_weeks`, `current_nfl_week`, `last_n_nfl_weeks`, `last_n_nfl_years`, `next_n_nfl_years`).
- A dedicated component `ColumnParamNflWeekFilter` already exists at `repository/active/league/app/views/components/column-param-nfl-week-filter/` with files:
  - `column-param-nfl-week-filter.js` -- top-level component rendering Dynamic section + `NflWeekBuilder` + `NflWeekSelectionSummary`, calling `handle_change` with the canonical mixed-array wire format.
  - `nfl-week-builder.js` -- staged builder. Holds its own `selected_years` + `selected_weeks` state, supports shift-click year ranges, shift-click week ranges, click-and-drag week selection, and an "Add to Selection" button that flushes the staged selection into the parent via `on_add`.
  - `nfl-week-selection-summary.js` -- renders the current static selection as grouped `YEAR SEAS_TYPE: weeks` rows with per-group Remove and a "Show full list" expander.
  - `index.js` -- barrel (re-exports the component; violates `user:guideline/write/write-javascript.md` no-barrel rule).
  - `column-param-nfl-week-filter.styl`.
- The component is wired in `repository/active/league/app/core/data-views-fields/index.js:131-133` via direct mutation: `fields[key].column_params.nfl_week_id.component = ColumnParamNflWeekFilter`. The import on `index.js:6` uses the `@components` alias and relies on the barrel.
- The render pipeline in `repository/active/react-table/src/column-controls-column-param-item/column-controls-column-param-item.js:62` already honors `column_param_definition.component` -- no react-table change is needed for this refactor.
- `FilterControlsColumnParamItem` (the `where`-clause equivalent) does not currently route `column_param_definition.component`. Explicitly out of scope for this task -- follow-up.
- `repository/active/league/libs-shared/nfl-week-identifier.mjs` already exports the helpers the refactor needs: `get_nfl_week_identifiers_for_year`, `format_nfl_week_identifier`, `validate_nfl_week_identifier`, `decompose_nfl_weeks`, `group_nfl_weeks`, `format_nfl_week_param_values`, `format_week_ranges`, `compare_nfl_week_group_keys`, `get_postseason_week_label`, `get_max_weeks_for_season_type`. `MIN_YEAR` is internal but not needed (year list is derived from `decompose_nfl_weeks` on `column_param_definition.values`). **`WEEK_RANGES` is defined but not currently exported** (line 5: `const WEEK_RANGES = ...`) -- the refactor needs to add `export` to this constant, otherwise selection-mode helpers that derive max week counts per season type will resolve to `undefined` at runtime.
- The refactor stays entirely inside the league client app. No `react-table` source changes; no cross-package alias work; no change to `libs-shared`.

## Design

- **Approach.** Refactor-in-place: rename the directory and files, merge `NflWeekBuilder`'s interactions directly into the selector (no staged "Add to Selection" button), add mutual exclusion + undo/redo, and extend the existing shift-click range pattern on years with click-and-drag range selection (mirroring what the builder already does for weeks). The selection summary stays as a read-only always-visible strip; dynamic objects are shown alongside static groups.
- **Component name.** `ColumnParamNflWeekSelector`. Rename flows through directory (`column-param-nfl-week-selector/`), files (`column-param-nfl-week-selector.{js,styl}`), the default export, and the import site in `data-views-fields/index.js`.
- **File organization (post-refactor).**
  - `repository/active/league/app/views/components/column-param-nfl-week-selector/column-param-nfl-week-selector.js` -- top-level component.
  - `nfl-week-selector-year-row.js` -- year-button row; each year = main button + adjacent caret trigger that opens a per-year popper. Handles plain click, shift-click range, click-and-drag range.
  - `nfl-week-selector-year-popper.js` -- per-year popper with PRE / REG / POST sections, each with All / Clear and per-week checkboxes. Supports shift-click and click-drag on weeks.
  - `nfl-week-selector-dynamic-values-section.js` -- dynamic values checkbox list; selecting purges statics.
  - `nfl-week-selector-summary.js` -- always-visible selection strip; renders static groups via `format_nfl_week_param_values` (reusing existing summary logic) plus dynamic entry labels.
  - `nfl-week-selector-history-controls.js` -- Undo / Redo icon buttons.
  - `nfl-week-selection-mode.js` -- pure helpers: `is_dynamic_selection`, `purge_dynamic`, `purge_static`, `apply_year_selection({ year, current_selection, values })`, `apply_year_range_selection({ from_year, to_year, current_selection, values })`, `apply_per_week_toggle`, `apply_season_type_bulk`, `apply_dynamic_toggle`. All multi-argument functions use destructured named parameters.
  - `nfl-week-selection-history.js` -- pure `{ past, present, future }` helpers: `create_initial_history`, `push_history`, `undo_history`, `redo_history`, `can_undo`, `can_redo`, bounded by `NFL_WEEK_SELECTION_HISTORY_LIMIT = 50`.
  - `column-param-nfl-week-selector.styl` -- renamed from the old `.styl`, plus new classes for year-row caret split-button, per-year popper, history controls.
  - `index.js` barrel -- deleted (violates no-barrel rule). Import site switched to the implementation file path.
- **Deleted files.**
  - `column-param-nfl-week-filter/column-param-nfl-week-filter.js` (replaced by renamed component).
  - `column-param-nfl-week-filter/nfl-week-builder.js` (its staged-selection logic is dissolved into the new selector; relevant interaction helpers move into `nfl-week-selection-mode.js`).
  - `column-param-nfl-week-filter/nfl-week-selection-summary.js` (replaced by renamed `nfl-week-selector-summary.js`; core rendering logic carries over, adapted to include dynamic entries).
  - `column-param-nfl-week-filter/index.js` (barrel removal).
  - `column-param-nfl-week-filter/column-param-nfl-week-filter.styl` (replaced by renamed `.styl`).
  - The entire `column-param-nfl-week-filter/` directory after the refactor.
- **Interaction rules.**
  - Selection is a single `Array<string | { dynamic_type, value }>` (wire format unchanged).
  - Dynamic-mode actions purge all static identifiers first; static-mode actions purge all dynamic objects first (mutual exclusion).
  - **Plain year-button click** -> selection = `get_nfl_week_identifiers_for_year({ year, seas_type: 'REG' })` filtered through `validate_nfl_week_identifier` (drops weeks not yet played for the current season). Replaces any prior static selection. Records anchor.
  - **Re-click on a fully-selected year** -> idempotent replace (same result). No toggle-off behavior; Clear is the deselect path.
  - **Shift-click on year button** -> selection = all validated REG identifiers for inclusive `[anchor, clicked]` year range. Falls back to plain click if no anchor.
  - **Click-and-drag across year buttons** -> identical to the builder's existing week-row pattern: `onMouseDown` begins a year drag (records mode `select` vs `deselect-from-fully-selected`), `onMouseEnter` extends selection to the hovered year, `onMouseUp` ends the drag. Each extension appends that year's REG identifiers (select) or strips them (deselect). This replaces the prior plan's range slider.
  - **Per-year popper checkboxes** -> add/remove individual identifiers, scoped to that year + that season type. Shift-click and click-drag on weeks preserve the existing builder semantics.
  - **Per-year popper All / Clear per season type** -> derives identifiers from `WEEK_RANGES[seas_type]` then filters via `validate_nfl_week_identifier`, so current-season incomplete regular seasons never produce invalid identifiers (matches the existing builder's `handle_add` safeguard).
  - **Dynamic-values section** -> checkbox list built from `column_param_definition.dynamic_values`; toggling one replaces selection with `[{ dynamic_type, value }]` (purging statics). `has_value_field` dynamics preserve the TextField value-update behavior from the existing component.
  - Every action that mutates selection first pushes current selection to the history `past`, clears `future`, emits through `handle_change`. Undo / Redo traverse the stack (bounded at 50). History is ephemeral -- resets on popper close / component unmount.
- **Configuration & defaults.** `default_value: { dynamic_type: 'current_year_reg_weeks' }` preserved on the `libs-shared` definition (untouched). No new npm dependencies -- MUI primitives already in use. Import paths follow league-app conventions: `Popper` from `@mui/base/Popper`, `ClickAwayListener` from `@mui/material/ClickAwayListener`, `Checkbox` / `TextField` / `IconButton` from `@mui/material/*`, `UndoIcon` / `RedoIcon` from `@mui/icons-material/*`. The `column-param-select-filter-with-overrides.js` reference in the per-year popper task is a behavioral model (click-away ignoring nested `.table-popper` ancestors); the imports themselves come from MUI packages, not from `react-table`.
- **Integration points.**
  - `data-views-fields/index.js:6` import updated from `@components/column-param-nfl-week-filter` to `@components/column-param-nfl-week-selector/column-param-nfl-week-selector.js` (direct file path, no barrel).
  - `data-views-fields/index.js:131-133` mutation updated: `fields[key].column_params.nfl_week_id.component = ColumnParamNflWeekSelector`. Mutation pattern retained (matches the existing code idiom; spread-replacement rewrite is unnecessary scope).
  - Public export surface of `react-table` is not changed. No new exports from that package.
- **Knowledge Base Impact.**
  - Domain entry doc: primary reference remains the project-local `repository/active/league/docs/data-views-system.md`. No user-KB entity needs updating.
  - ABOUT.md files: none present in affected directories; none created.
  - Tag implications: reuse existing `user:tag/league-xo-football.md` and `user:tag/software-task.md`. No new tags.
  - Observation candidates: on task completion add two observations: (1) `[pattern] Custom column-param UI components live in the league client app (not in the isomorphic libs-shared or the generic react-table package), bound via the component field on the client-side field map.` (2) `[pattern] For direct-mutation selectors, merge builder staging state into the component itself rather than introducing an Add-to-Selection step.`
  - Guideline candidates: none at this scale.
  - Relations: this task `relates_to` `user:workflow/write/write-software-implementation-plan.md` (already set); follow-up task will `relates_to` this task back.

## Notes

- Follow-up task (explicit out of scope): route `column_param_definition.component` through `filter-controls-column-param-item.js` in `react-table` so `where`-clause usage of `nfl_week_id` renders with the same selector. Will be created as part of the KB Updates group below.
- Year row is rendered newest -> oldest (matches current builder's `sort((a, b) => b - a)` on `decompose_nfl_weeks({ nfl_weeks: values }).years`).
- Year-button visual state is tri-state: none / partial / full-REG, driven by membership of each year's validated REG identifiers in `static_selected`.
- Postseason week labels inside the per-year popper use `get_postseason_week_label` (Wild Card / Divisional / Conference / Super Bowl).
- History is intentionally ephemeral. When the popper closes or the param panel unmounts, the stack resets. This is a documented UX limitation -- undo/redo is a within-session affordance only.
- Performance: year row <=~27 buttons; per-year popper <=29 checkboxes; no virtualization required.
- File-size soft limit (200 lines): splitting year row, per-year popper, dynamic section, summary, history controls, and the two pure-helper modules into sibling files keeps every file below the threshold. History helpers are extracted (rather than inlined into the top-level component) to keep them pure and independently testable, matching the selection-mode helpers.
- `useReducer` is the deliberate choice for history state even though no existing league component uses it. Rationale: the `{ past, present, future }` shape with three clearly-named dispatch actions (`push`, `undo`, `redo`) is the textbook reducer case, and `useState` would require either nested updaters or a wrapping helper that duplicates what the reducer expresses. Note this briefly in the component file header to signal the choice is deliberate.
- **Mode-purge cue is reactive, not transient.** The summary strip always shows the current active mode (static groups or dynamic entries) derived from the selection shape via `is_dynamic_selection`. There is no "just got purged" transient flag; the user sees the result in the summary because the opposite mode's rows simply disappear. This avoids introducing ephemeral UI state (e.g., a `recently_purged` flag) that would need debouncing and cleanup.
- **Keyboard / ARIA accessibility is explicitly deferred.** The existing `ColumnParamNflWeekFilter` has no keyboard navigation or ARIA attributes; this refactor matches that baseline. A follow-up task can add Tab order, Enter/Space activation, Escape-to-close, and appropriate `role` / `aria-*` attributes across the selector surface. Flagged here so the omission is intentional rather than an oversight.

## Plan

### Core Implementation

- [x] **Export `WEEK_RANGES` from `nfl-week-identifier.mjs`**
  - File: `repository/active/league/libs-shared/nfl-week-identifier.mjs`
  - Purpose: Unblock selection-mode helpers that need max-week counts per season type. `WEEK_RANGES` is currently defined as a module-internal `const` and is referenced by the existing in-module helpers (`validate_nfl_week_identifier`, `get_nfl_week_identifiers_for_year`, `get_max_weeks_for_season_type`). Adding `export` is a pure surface-area widening -- no server-side behavior changes.
  - Changes:
    - Add `export` keyword to the `const WEEK_RANGES` declaration on line 5.
    - Verify with `yarn lint` and by running the league test suite that no prior shadowing breaks (none expected; grep for `WEEK_RANGES` shows only in-file usage today).

- [x] **Rename the component directory and file set**
  - Files: `repository/active/league/app/views/components/column-param-nfl-week-filter/` -> `repository/active/league/app/views/components/column-param-nfl-week-selector/`.
  - Purpose: Establish the new directory; rename the main component and its stylesheet; leave the prior auxiliary files in place temporarily so content migration happens incrementally.
  - Changes:
    - `git mv repository/active/league/app/views/components/column-param-nfl-week-filter repository/active/league/app/views/components/column-param-nfl-week-selector` so git tracks the directory rename and preserves history for the carried-over files.
    - `git mv` the main component file (`column-param-nfl-week-filter.js` -> `column-param-nfl-week-selector.js`) and the stylesheet (`.styl` analog) so renames are recorded as moves rather than delete+add pairs. Update default export symbol to `ColumnParamNflWeekSelector`.
    - Update the `import './...'` line in the component file to reference the renamed stylesheet.
    - Auxiliary new files (year-row, per-year popper, dynamic section, summary, history controls, mode helpers, history helpers) are created fresh inside the renamed directory -- they have no rename analogue, so plain `git add` is correct for them.

- [x] **Extract pure selection-mode helpers**
  - File: `repository/active/league/app/views/components/column-param-nfl-week-selector/nfl-week-selection-mode.js`
  - Purpose: House all pure functions that compute new selections; enforce mutual exclusion by purging the opposite mode before applying static or dynamic changes.
  - Changes:
    - Export `is_dynamic_selection`, `purge_dynamic`, `purge_static`, `purge_static_group({ current_selection, group_key })`.
    - Export `apply_year_selection({ year, current_selection, values })`, `apply_year_range_selection({ from_year, to_year, current_selection, values })`, `apply_per_week_toggle({ year, seas_type, week, current_selection })`, `apply_season_type_bulk({ year, seas_type, action, current_selection, values })`, `apply_dynamic_toggle({ dynamic_type, dynamic_value, current_selection })`.
    - Import `nfl_week_identifier` from `@libs-shared` for `get_nfl_week_identifiers_for_year`, `format_nfl_week_identifier`, `validate_nfl_week_identifier`, `WEEK_RANGES` (exported in the prior task).
    - Every helper filters produced identifiers through `validate_nfl_week_identifier` to avoid emitting weeks not yet played for the current season.
    - `purge_static_group` strips identifiers whose `parse_nfl_week_identifier` result matches `{ year, seas_type }` from `group_key` (format `YEAR_SEAS_TYPE`); it preserves any dynamic entries defensively, even though mutual exclusion prevents mixing in practice.

- [x] **Extract pure undo/redo history helpers**
  - File: `repository/active/league/app/views/components/column-param-nfl-week-selector/nfl-week-selection-history.js`
  - Purpose: Pure functions over `{ past, present, future }` with bounded depth.
  - Changes:
    - Export `create_initial_history`, `push_history`, `undo_history`, `redo_history`, `can_undo`, `can_redo`.
    - Define `NFL_WEEK_SELECTION_HISTORY_LIMIT = 50`.

- [x] **Implement the top-level selector component**
  - File: `repository/active/league/app/views/components/column-param-nfl-week-selector/column-param-nfl-week-selector.js`
  - Purpose: Wire sub-sections together, manage history state, emit `handle_change` on every mutation.
  - Changes:
    - Derive year list via `const { years } = nfl_week_identifier.decompose_nfl_weeks({ nfl_weeks: values })`, then `[...years].sort((a, b) => b - a)`.
    - Manage history state with `useReducer` over `nfl-week-selection-history.js` helpers; each mutating handler pushes the prior selection, then calls `handle_change(next_selection)`. Include a brief file-header comment noting that `useReducer` is the deliberate choice (no other league component uses it) because the `{ past, present, future }` shape with `push` / `undo` / `redo` actions is the textbook reducer case.
    - Shift-click anchor tracked in a `useRef`.
    - Thread `values` (the full `column_param_definition.values` list) as a prop into `NflWeekSelectorSummary` so its "Show full list" toggle has a data source.
    - Render order inside the `FilterBase` popper body: header with Undo/Redo + existing All/Clear/Close controls, then `NflWeekSelectorDynamicValuesSection`, then `NflWeekSelectorYearRow` with its per-year poppers, then `NflWeekSelectorSummary`.
    - Trigger button label logic reuses the existing tri-state ("ALL" / "N selected" / "-") with the same mixed-state behavior; display delegates to `column_param_definition.format_param_values` for tooltip or detail-line rendering if needed.
    - PropTypes: unchanged from the existing component.
    - Default export name: `ColumnParamNflWeekSelector`.

- [x] **Implement the year row with click-drag range selection**
  - File: `repository/active/league/app/views/components/column-param-nfl-week-selector/nfl-week-selector-year-row.js`
  - Purpose: Render year buttons with tri-state visual, handle plain click, shift-click range, click-and-drag range, and caret trigger for the per-year popper.
  - Changes:
    - Each year item renders two clickable targets: main button and adjacent caret icon button.
    - Main-button `onClick` (no shift) -> `apply_year_selection` and update anchor. **Replace semantics, not toggle**: plain click overwrites the entire current static selection with that year's validated REG identifiers. Re-clicking a fully-selected year is an idempotent replace (no toggle-off); the Clear control is the deselect path.
    - Main-button `onClick` (shift, anchor set) -> `apply_year_range_selection({ from_year: anchor, to_year: year, ... })`.
    - Main-button `onMouseDown` + sibling `onMouseEnter` + container `onMouseUp` -> drag range, mirroring the builder's existing week-row drag pattern (drag_ref with mode `select` or `deselect`).
    - **Portal-escape safety:** attach a `mouseup` listener to `document` inside a `useEffect` (removed on unmount) that unconditionally clears `drag_ref.current`. If the user drags from the year row into the per-year popper (rendered in a MUI portal outside the year-row DOM tree) and releases over the popper, the container's `onMouseUp` will not fire -- the document listener is the safety drain that prevents subsequent `onMouseEnter` events from extending the selection unexpectedly.
    - Caret `onClick` opens the per-year popper anchored to the caret element (stop propagation to prevent triggering main-button handlers).
    - Year visual state: none / partial / full based on membership of validated REG identifiers in the current static selection.

- [x] **Implement the per-year popper**
  - File: `repository/active/league/app/views/components/column-param-nfl-week-selector/nfl-week-selector-year-popper.js`
  - Purpose: PRE / REG / POST sections with All / Clear and per-week checkboxes, inheriting the builder's drag + shift-range interactions.
  - Changes:
    - Imports: `Popper` from `@mui/base/Popper`, `ClickAwayListener` from `@mui/material/ClickAwayListener`, `Checkbox` from `@mui/material/Checkbox` -- matches the league app's existing MUI import conventions (do not mirror `react-table`'s `@mui/base/ClickAwayListener` path).
    - Each season-type section: header with the type label and `All` / `Clear` controls bound to `apply_season_type_bulk`.
    - Checkboxes bind to `apply_per_week_toggle`; reuse mouse-down / mouse-enter / mouse-up handlers from the existing builder (lift them into a shared helper inside the year-popper file).
    - POST labels come from `get_postseason_week_label`.
    - Close behavior: `ClickAwayListener` ignoring clicks inside any `.table-popper` ancestor (behavioral model is `column-param-select-filter-with-overrides.js`; the imports themselves come from MUI).

- [x] **Implement the dynamic values section**
  - File: `repository/active/league/app/views/components/column-param-nfl-week-selector/nfl-week-selector-dynamic-values-section.js`
  - Purpose: Render `column_param_definition.dynamic_values` as checkboxes; selecting purges statics (mutual exclusion).
  - Changes:
    - Rebuild today's dynamic rendering from `column-param-nfl-week-filter.js:164-206`; replace the `handle_dynamic_toggle` coexistence semantics with `apply_dynamic_toggle` (which purges statics).
    - Preserve `has_value_field` TextField behavior (`dynamic_values` local state, `handle_dynamic_value_change`).

- [x] **Implement the selection summary strip**
  - File: `repository/active/league/app/views/components/column-param-nfl-week-selector/nfl-week-selector-summary.js`
  - Purpose: Always-visible summary. Renders static groups (via existing `group_nfl_weeks` + `format_week_ranges` / `get_postseason_week_label`) plus dynamic entry labels. Removes the "Add to Selection" pathway.
  - Changes:
    - Port the group rendering from today's `nfl-week-selection-summary.js`.
    - Props: `{ current_selection, values, handle_change }`. The top-level selector MUST thread its `values` prop (`column_param_definition.values` -- the full `get_all_nfl_week_identifiers()` list) into this component so the "Show full list" toggle has a data source.
    - Add a dynamic section that lists each dynamic entry's label (and value if `has_value_field`).
    - Per-group Remove routes through the selection-mode helper: `handle_change(purge_static_group({ current_selection, group_key }))`. Do not duplicate the prefix-strip logic locally -- consume the shared helper so dynamic entries are defensively preserved and the strip behavior is unit-testable via the helper module.
    - Keep the "Show full list" toggle for static values, driven by the threaded `values` prop.

- [x] **Implement the history controls**
  - File: `repository/active/league/app/views/components/column-param-nfl-week-selector/nfl-week-selector-history-controls.js`
  - Purpose: Undo / Redo IconButtons wired to the reducer dispatches; disabled when `!can_undo` / `!can_redo`.

- [x] **Port and extend styling**
  - File: `repository/active/league/app/views/components/column-param-nfl-week-selector/column-param-nfl-week-selector.styl`
  - Purpose: Rename `.nfl-week-filter-*` class prefixes to `.nfl-week-selector-*` where renamed; add new styles.
  - Changes:
    - Rename all existing classes from `.nfl-week-filter*` to `.nfl-week-selector*` and update class references throughout the JS files.
    - Add styles for: year-button caret (split-button layout), per-year popper panel (reuse `.table-filter-item-dropdown-*` conventions from `react-table`), history controls row.
    - Mode cue is reactive (see Notes): style only the always-visible "Static" / "Dynamic" section headers in the summary so the active mode is visually obvious from the selection shape. Do not introduce any transient `recently_purged` state or animation; no extra component state is needed.
    - Preserve drag cursor styling on year buttons (cursor: pointer, user-select: none).

- [x] **Delete the old component files**
  - Files: `repository/active/league/app/views/components/column-param-nfl-week-filter/column-param-nfl-week-filter.js`, `.../nfl-week-builder.js`, `.../nfl-week-selection-summary.js`, `.../column-param-nfl-week-filter.styl`, `.../index.js`.
  - Purpose: Remove the replaced implementation and the barrel file (barrel violates `user:guideline/write/write-javascript.md`). After deletion, remove the `column-param-nfl-week-filter/` directory.

- [x] **Update the component wiring**
  - File: `repository/active/league/app/core/data-views-fields/index.js`
  - Purpose: Point the nfl_week_id binding at the renamed component and the direct file path (no barrel).
  - Changes:
    - Replace the import on line 6 with `import ColumnParamNflWeekSelector from '@components/column-param-nfl-week-selector/column-param-nfl-week-selector.js'`.
    - Replace the assignment on line 132 with `fields[key].column_params.nfl_week_id.component = ColumnParamNflWeekSelector`.

### Configuration & Setup

- [x] **Verify no server-side import regression**
  - Files: `repository/active/league/api/**`, `repository/active/league/libs-server/**`, `repository/active/league/scripts/**`
  - Purpose: Confirm no server entry point imports `data-views-fields/index.js` transitively (which would pull React into a server bundle).
  - Changes:
    - Grep for imports of `app/core/data-views-fields` and of the renamed component path from server paths; none expected.
    - Confirm `react-table` itself is not imported from any server bundle for non-validator purposes.

- [x] **Lint & format**
  - Commands: `yarn lint`, `yarn prettier` in `repository/active/league`.
  - Purpose: Catch stale imports, missing `.js` extensions, and style drift from the rename + file split.

### Knowledge Base Updates

- [x] **Record two completion-time pattern observations on this task**
  - Entity: this task entity.
  - Purpose: Capture the location invariant and the no-Add-to-Selection refactor heuristic for future custom column-param UIs.
  - Changes:
    - `base entity observe <this-task> "[pattern] Custom column-param UI components live in the league client app (not in libs-shared or the generic react-table package), bound via the component field on the client-side field map."`
    - `base entity observe <this-task> "[pattern] Merge builder staging state into the direct-mutation component rather than introducing an Add-to-Selection step."`

- [x] **Create follow-up task for filter-controls parity**
  - Entity: new task at `user:task/league/route-column-param-component-through-filter-controls.md`.
  - Purpose: Extend `FilterControlsColumnParamItem` in `react-table` to route `column_param_definition.component`, so `where`-clause usage of `nfl_week_id` gets the same selector UX.
  - Changes:
    - `base entity create user:task/league/route-column-param-component-through-filter-controls.md --type task --title "Route column_param component through filter-controls" --description "Extend react-table FilterControlsColumnParamItem to honor column_param_definition.component so where-clause usage of nfl_week_id renders with the new ColumnParamNflWeekSelector." --properties '{"status":"Planned","priority":"Medium","tags":["user:tag/league-xo-football.md","user:tag/software-task.md"],"relations":["relates_to [[user:task/league/refactor-nfl-week-id-column-param-ux.md]]"]}'`.

### Testing & Validation

- [ ] **Manual UX verification**
  - Scope: browser test via `yarn dev` in league.
  - Scenarios:
    - Plain year-button click selects that year's validated REG weeks; re-click is idempotent.
    - Shift-click after a plain click selects the inclusive year range (REG only).
    - Click-and-drag across year buttons extends or trims the selection like the week-row drag pattern; mouse-up ends the drag.
    - Caret click opens the per-year popper without triggering a year selection.
    - Per-year popper All / Clear per season type modifies only that year + that season type; selections stay validated for the current incomplete season.
    - Week shift-click and week click-drag continue to work inside the per-year popper.
    - Dynamic value selection purges all static identifiers; any year or week interaction purges all dynamic objects.
    - Selection summary strip updates live, shows both static groups and dynamic entries, and exposes per-group Remove.
    - Undo / Redo navigate selection history; disabled at edges; reset on popper close.
    - Closed trigger-button label reflects current selection with the existing ALL / N selected / mixed-state behavior.
    - Portal-escape drag: start a year-row click-drag, move the cursor over the per-year popper (rendered in a portal), release -- the drag state clears and no further hover extends the selection. Verifies the document-level `mouseup` safety drain.
  - Explicitly deferred: keyboard navigation and ARIA attributes (Tab order, Enter/Space activation, Escape-to-close, `role` / `aria-*`) are not required for this iteration -- the existing component has no such support either. A separate follow-up task will add accessibility if desired.
  - Regression: other column params that still route through `ColumnParamSelectFilter` are unaffected; the old `column-param-nfl-week-filter/` directory is fully removed and no dangling imports remain.
