Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 54 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ in `out/` is served by any static host (GitHub Pages, GitLab Pages, Bitbucket).
**Key Features:**
- Search over curated repository listing (`repositories.txt`)
- Reports rendered with Mustache.js (bundled template is authoritative)
- Enhanced report tables: sticky headers, pagination (>20 rows), sortable
columns, in-table search, CSV export, click/keyboard cell copy with toasts
- Repository submission via pre-filled platform issues — no login, apps or tokens
- Fully static deploy; deep links handled via generateStaticParams + `404.html`

Expand Down Expand Up @@ -60,12 +62,17 @@ app/ # Next.js App Router (static export)
components/ # client components: report-view, repo-list,
# repo-submission-form, search-combobox,
# hero-search, menu-search, menu-toggle,
# workflow-sample, platform-config, sentry-provider
# workflow-sample, platform-config, sentry-provider,
# toast-notification (copy feedback live region)
lib/ # shared logic (client + RSC): routes, fetcher,
# renderer, search, utils, host, rate-limiter,
# cache-manager, error-handler, repo-submission,
# report-view, static-params, widget-loader; the
# Node-side listing loader is lib/repositories.js
# report-view, static-params, widget-loader,
# table-operations (filter/sort/paginate/CSV/copy +
# TABLE_CONFIG + REPORT_TABLES descriptors),
# table-enhancer (binds toolbar/sort/pagination/
# copy onto the rendered report DOM); the Node-side
# listing loader is lib/repositories.js
public/ # static files copied verbatim into out/:
repositories.txt # synced from the repo root (sync-repositories.mjs)
assets/ # mustache template, logo
Expand Down Expand Up @@ -117,12 +124,54 @@ coverage there when introducing new markup patterns.
`sentry-dsn`, `platform-base-url`) plus `NEXT_PUBLIC_HOSTING_ENVIRONMENT`
/ `NEXT_PUBLIC_BASE_PATH` at build time.

## Report Tables (Enhanced)

- Every data table in the report (class/package relationships, disharmony
findings, cycle summary, cycle breakdown) is enhanced: sticky `thead th`,
toolbar (match live region + copy hint left; search + CSV export right —
the `.rf-table-block` wrapper shrink-wraps the table and the toolbar uses
`contain: inline-size` so controls align with the table's right edge),
sortable th buttons with `aria-sort`, pagination below 20+ row tables, and
click/Enter/Space cell copy with toast feedback. The filter's clear control
is an × button (accessible name "Clear the … table filter").
- Horizontal scrollbar: tables wider than the viewport get `overflow-x: auto`
via the `rf-scroll-x-enabled` class, toggled by `lib/table-enhancer.js`
after measuring `wrapper.scrollWidth > clientWidth` (re-measured on each
re-render and on window resize). It MUST stay conditional — any overflow
ancestor becomes the sticky constraint container and breaks the
viewport-sticky `thead th`. Scrolling tables keep their header pinned
anyway: `refreshStickyHeaders` in lib/table-enhancer.js compensates by
translating every `thead th` down by the viewport scroll offset (clamped to
the table's bottom edge) on window scroll/resize; narrow tables keep pure
CSS stickiness and stale transforms are cleared when overflow goes away.
- Pipeline: `prepareReportData(data, tableStates, TABLE_CONFIG)` in
lib/renderer.js applies **filter → sort → paginate** per table and injects
`tableUi` blocks the mustache template renders; `enhanceTables` in
lib/table-enhancer.js binds the controls and reports state changes back to
components/report-view.jsx, which re-renders (widgets only gate the first
render; the search input's focus/caret is restored after each re-render).
The expensive `enhanceReport` pipeline (Chart.js charts, WASM DOT layout)
runs only when the payload changes — table-state re-renders stash the live
chart canvases / graph containers before the innerHTML swap and graft them
back into the fresh DOM (`statefulElementIds`/`stashStatefulDom`/
`graftStatefulDom` in lib/report-view.js), re-binding only the cheap popup
handlers.
- Search `<input>`s are injected by table-enhancer — `<input>` is FORBID in
the renderer's sanitization allow-list, so it must never appear in the
mustache template.
- CSV export honors the current filter + sort but ignores pagination;
filenames are `refactorfirst-<table>-<ISO timestamp>.csv`.

## Current Test Count

~313 unit/integration + 112 E2E (three browsers + basePath leg).
~485 unit/integration + 175 E2E (171 across three browsers + 4 basePath leg).

WCAG 2.2 AA / HTML5 guards live in tests/unit/html5-attributes.test.js,
tests/unit/report-template-wcag.test.js, tests/unit/css-a11y.test.js and
tests/unit/page-titles.test.js — the report mustache keeps a single h1,
scoped table headers, captions, labelled canvases and a named nav; obsolete
presentational attributes are FORBID_ATTR-stripped in lib/renderer.js.
presentational attributes are FORBID_ATTR-stripped in lib/renderer.js. The
report-template-wcag guard also asserts sticky-header CSS, per-table
toolbars/aria-labelled export buttons and pagination navs, valid `aria-sort`
on every enhanced th, sortable keyboard-operable header buttons and live
match-count regions.
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ is a **Next.js static export** (`bun run build` produces `out/`, which any stati
branch fallback — the same report the
[RefactorFirst report viewer](https://github.com/RefactorFirst/RefactorFirst) produces:
class/package maps (vizdom WASM SVGs with pan/zoom, plus Sigma 2D and 3D force-graph popups), relationship-removal
priority tables, Chart.js disharmony bubble charts and class cycle summaries
priority tables, Chart.js disharmony bubble charts and class cycle summaries — with
[enhanced tables](#enhanced-report-tables) (sticky headers, pagination, sorting, search, CSV export, copy)
- **Repository submission** via a pre-filled issue on the hosting platform (no login, apps or tokens on this site): your
platform account is captured as the issue author and validated server-side by the platform's CI
- Reports and submissions work for repositories hosted on the same platform as the deployment (GitHub, GitLab or
Expand All @@ -29,6 +30,7 @@ is a **Next.js static export** (`bun run build` produces `out/`, which any stati
- [Deploying to Bitbucket](#deploying-to-bitbucket)
- [Deploying to GitLab](#deploying-to-gitlab)
- [How repository submission works](#how-repository-submission-works)
- [Enhanced report tables](#enhanced-report-tables)
- [Making Changes (Developer Guide)](#making-changes-developer-guide)
- [Testing](#testing)

Expand Down Expand Up @@ -289,6 +291,37 @@ GitLab Pages deploys from the `pages` job and serves

---

## Enhanced report tables

Large report tables (class/package relationships, disharmony findings, cycle summaries and cycle breakdowns) are
interactive — all WCAG 2.2 AA and keyboard-operable, with no extra dependencies:

- **Sticky headers** — column headers stay pinned to the top of the viewport while you scroll a table.
- **Pagination** — tables with more than 20 matching rows paginate at 20 rows per page ("Page X of Y" + Previous/Next
buttons; disabled on the first/last page). Small tables render in full without controls.
- **Sorting** — click a column header (or focus it and press Enter/Space) to sort the whole table ascending; click
again for descending. `aria-sort` reflects the current direction, sorting happens before pagination, and the sort is
kept while navigating pages.
- **Search/filter** — the filter box sits at the right edge of the table (next to Export) and narrows rows
case-insensitively across all columns (debounced), with the match count announced via a live region and an **×**
button to reset.
- **CSV export** — "Export CSV" downloads the entire table (current filter and sort applied, pagination ignored) with
proper escaping; the filename includes the table name and a timestamp.
- **Horizontal scrolling** — when a table is wider than the screen its wrapper gains a horizontal scrollbar
(`rf-scroll-x-enabled`, toggled by measurement in `lib/table-enhancer.js`); the scrollbar is deliberately conditional
because an unconditional `overflow-x` would break the viewport-sticky table header. Scrolling tables keep their
header pinned anyway: `lib/table-enhancer.js` translates the header cells down by the viewport scroll offset
(clamped to the table's bottom edge) on every window scroll/resize.
- **Copy cells** — click any cell (or focus it and press Enter/Space) to copy its text; an auto-dismissing toast
confirms the copy. Falls back gracefully when the Clipboard API is unavailable.

Implementation: pure table operations live in `lib/table-operations.js` (filter → sort → paginate pipeline, CSV
generation, clipboard helper, `TABLE_CONFIG` defaults: threshold/page size 20, 300&nbsp;ms search debounce, 3&nbsp;s
toast duration), the rendered DOM is wired by `lib/table-enhancer.js`, and table state re-renders through
`prepareReportData` in `lib/renderer.js`.

---

## How repository submission works

No OAuth app, client ID, token or secret is involved on the client side — forks need **zero auth setup**. The flow on
Expand Down
47 changes: 47 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -461,3 +461,50 @@ pre {
grid-template-columns: 1fr;
}
}

/* Toast notifications (copy feedback on report tables). Rendered by
components/toast-notification.jsx as a persistent aria-live region. */
.rf-toast-region {
position: fixed;
left: 50%;
bottom: 1.25rem;
transform: translateX(-50%);
z-index: 2000;
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: center;
pointer-events: none;
max-width: min(90vw, 40rem);
}

.rf-toast {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.6rem 0.9rem;
border-radius: var(--radius);
background: #1f2933;
color: #fff;
box-shadow: var(--shadow);
pointer-events: auto;
}

.rf-toast-message {
overflow-wrap: anywhere;
}

.rf-toast-dismiss {
background: none;
border: none;
color: #fff;
font-size: 1.1rem;
line-height: 1;
cursor: pointer;
padding: 0.15rem 0.45rem;
}

.rf-toast-dismiss:focus-visible {
outline: 2px solid var(--brand-color);
outline-offset: 2px;
}
Loading
Loading