github-repos-react — GitHub Repositories (React)
A Vite + React port of github-repos-js —
feature-identical (GitHub Search REST API, Link: rel="next" continuation-URL
pagination, custom DataManager, toolbar, detail panel, 8 themes, EN/IT i18n,
resizable container, Excel export, multi-selection), all TypeScript. The
github-repos-react ↔ github-repos-js relationship is the same as wikipedia-pages-vue ↔ wikipedia-pages-js: a framework port of a proven
plain-JS app, demonstrating that the zero-dependency components drop into a
framework app consumed as-is via classic script tags.
Built with React 19 (function components + hooks, no state library) on the same Vite setup as wikipedia-pages-vue.
What it shows
- React wrapper layer as thin components over the real custom elements —
src/vanilla/renders the native<vn-grid>/<vn-grid-toolbar>elements directly from JSX (no wrapper host element lands in the DOM; nothing sits between<vn-resize-box>and<vn-grid>, or between the grid and its<vn-grid-column>light-DOM children), and exposes the imperative API throughrefhandles (useImperativeHandle). This is React's idiomatic bridge, deliberately different from wikipedia-pages-vue'sdefineExposewrapper components and northwind-orders-angular'sexportAselement-selector directives — each port demonstrates its framework's shape. VnGrid—dataManagerrouted through the element's imperativesetDataManager()(there is nodataManagerDOM property; handing React the object as a JSX prop would serialize it into a useless attribute), typed callback props bridging the grid's CustomEvents (onLoaded,onSelectionChanged,onRowDblclick,onPersistenceReady— React does not bind arbitrary CustomEvents on its own), and arefhandle delegating the imperative API (initializeGrid,loadRowsAsync,setTheme,refresh,exportToExcel, …) plus anelementescape hatch.VnGridToolbar— owns the template-content problem. React builds DOM imperatively, so children rendered inside a native<template>land inchildNodes, not.content— the toolbar would clone them as empty. The component takes theempty/selectedstate templates as HTML strings (module-level constants inReposTab.tsx), creates realHTMLTemplateElements, and assignsinnerHTML— React's equivalent of wikipedia-pages-vue'sv-htmlbridge. Thegridattribute is set only after the templates are appended; the toolbar re-reads its state templates on relink, so the late-appended templates are picked up (the element is already connected when the effect runs).- JSX typing for custom elements —
src/types/jsx.d.tsaugments thereactmodule'sJSX.IntrinsicElements(the React 19 form) so<vn-grid>,<vn-grid-column>,<vn-resize-box>and<vn-grid-toolbar>type-check in TSX, with declarative config typed as string attributes. <vn-resize-box>needs no wrapper at all — it is purely declarative (attributes only); React renders it as a plain intrinsic element.- No
<StrictMode>, on purpose — its dev-only double-invoked effects would run ReposTab's one-shot boot sequence (initializeGrid()+ the initial GitHub fetch) twice; the grid init is intentionally one-shot, same as github-repos-js (seemain.tsx). - Custom
DataManagerin TypeScript —GitHubReposDataManagerextendswindow.DataManagervia a lazy class factory (the bundler evaluates imported modules beforemain.tsx'sawait window.VanillaGridReadyresolves, so theextendscannot sit at module top level — same wrinkle as github-repos-js and wikipedia-pages-vue), typed against the components' hand-authored.d.tsfiles.
How the components are loaded
index.html loads the three components with classic script tags from
/vendor/ — they are globals that resolve their own feature scripts/CSS
relative to their script URL, so they are not ES imports. src/main.tsx
awaits window.VanillaGridReady before createRoot(...).render(), so
every vn-* element is upgraded and window.DataManager exists before the
first component renders.
/vendor/ is not a real folder in this app: the vendorComponentsPlugin in
vite.config.ts (same design as wikipedia-pages-vue's) streams the sibling
component folders from the repo root in dev, and copies them into
dist/vendor/ on build so the built app stays self-contained.
The base stylesheet (public/styles.css) and the theme stylesheet are linked
directly in index.html — in that order — so the cascade matches github-repos-js. They
are deliberately not imported in main.tsx: Vite would inject imported
CSS at the end of <head>, which would put the base sheet after the theme
link and clobber theme overrides.
Resizing the grid
The grid sits in a src/vanilla-resize-box/ (aria-label="Resizable repositories grid"). Drag the corner handle to resize it — resizing is pointer-driven, with no keyboard path.
The handle carries role="separator" and takes its accessible name from the host's aria-label — here "Resizable repositories grid — resize handle" — but is not focusable, so it adds no tab stop to the page. This app's markup needs nothing beyond the aria-label it already carries.
Endpoint
GET https://api.github.com/search/repositories
?q=language:javascript
&sort=stars
&order=desc
&per_page=100
per_pageis capped at 100. The GitHub Search API silently clamps any larger value to 100. The data manager enforces this cap itself so the actual page size always matches what the server returns.
The grid's infinite-scroll (skip, pageSize) hint is ignored — the next page
is defined entirely by the Link: rel="next" URL captured from the previous
response. GitHub returns the real total_count (millions) so the toolbar
status shows "Loaded N/total", but the Search API only allows pagination
through the first 1,000 results — past that rel="next" stops being
returned and pagination naturally stops.
Sorting
Server-side sorting is wired for the three columns the GitHub Search API supports:
| Grid column | API sort= value |
|---|---|
| Stars | stars |
| Forks | forks |
| Updated | updated |
Every other column is sortable="false" in the markup.
open_issues_count is intentionally non-sortable: the closest GitHub option
(help-wanted-issues) counts only issues labelled "help wanted", which is
not the same as total open issues.
Rate limits
GitHub allows 60 unauthenticated requests/hour per IP. To raise that to 5000/h, set a personal access token in the DevTools console:
localStorage.setItem('github-repos-react:githubToken', 'ghp_yourReadOnlyToken');
location.reload();
The token requires no scopes for public-repo search.
Run
npm install # first time only
npm run dev # serves on http://localhost:5173/
Manual verification checklist:
- Grid loads the first GitHub page (100 rows), infinite-scrolls via
Linkcontinuation, stops cleanly at the 1,000-result API ceiling. - Server-side sort round-trips on Stars/Forks/Updated; other headers inert.
- Toolbar: status counts, auto-fit, reset, clear-filters (enabled only with filter/sort), reload, export (both scopes, theme-styled file), view (single selection only), clear selection.
- Row double-click opens the detail panel; theme switch is live across all 8 themes (app chrome + grid + overlay); EN/IT toggle reloads localized.
github-repos-react:githubTokenin localStorage raises the rate limit.npm run buildoutput works when served from a subdirectory (relativebase), andnode build.js(repo root) integrates it intodist/with shared component paths.
Build
npm run build # typecheck (tsc --noEmit, both tsconfigs) + vite build → dist/
npm run typecheck # typecheck only
npm run preview # serve the built dist/ locally
npm run build type-checks the whole app before bundling — the github-repos-react analogue
of wikipedia-pages-vue's npm run typecheck. The repo-level node build.js delegates to
this build, copies the output to dist/samples/github-repos-react/, rewrites the
vendor/ script paths to the shared ../../vanilla-components/latest/
folder, minifies the public CSS, and drops the copied vendor/ folder — the
same post-process contract wikipedia-pages-vue gets.
Project layout
github-repos-react/
├── index.html # favicon ("GRR" badge), overlay boot script, vendor script tags
├── package.json # react + react-dom only — no other runtime deps
├── vite.config.ts # base "./"; vendorComponentsPlugin (dev middleware + dist copy)
├── tsconfig.json # app typecheck (src/, TSX)
├── tsconfig.node.json # vite.config.ts typecheck
├── public/
│ ├── styles.css # github-repos-js's stylesheet, verbatim
│ └── themes/app-*.css # the 8 app themes + TEMPLATE, verbatim from github-repos-js
└── src/
├── main.tsx # await VanillaGridReady → createRoot (no StrictMode — see header)
├── App.tsx # shell: header, theme/language selectors, tab, overlay handoff
├── i18n.ts # module port of github-repos-js's modules/i18n.js
├── theme.ts # live theme swap: app <link href> swap, resolved name returned
├── vanilla/
│ ├── VnGrid.tsx # ref handle + CustomEvent bridges + setDataManager routing
│ └── VnGridToolbar.tsx # HTML-string state templates → real <template> children
├── components/
│ ├── ReposTab.tsx # toolbar + resize-box + grid + columns + wiring
│ ├── RepoDetailPanel.tsx
│ └── ThemeSwitchOverlay.tsx
├── data/
│ └── github-repos-data-manager.ts # lazy class factory, port of github-repos-js's manager
└── types/
├── global.d.ts # triple-slash refs to the 3 component .d.ts files
├── jsx.d.ts # JSX.IntrinsicElements for the vn-* elements
└── repo.ts # GitHubRepo row interface
Behavior notes (ported 1:1 from github-repos-js)
- Theme switching — live, no reload, no persistence.
switchTheme()swaps the app-theme<link href>(with the shared cache-bust token);Appthen calls the grid handle'ssetTheme()+refresh()viaReposTab.applyTheme(). The boot theme is hard-coded in markup (glow-dark). - Locale — EN/IT; changing persists (
github-repos-react:selectedLocale) and reloads the page (simplest way to re-init the grid's localized formatting), with theoverlayReason/skipOverlaysessionStorage dance kept. The pre-mount overlay inindex.htmlcovers theVanillaGridReadywait;Appswaps it for the React-rendered overlay on first render. - Toolbar — same command set as github-repos-js: standard auto-fit / reset /
clear-filters / reload; export overridden via
registerCommandfor file naming (theme-matched Excel styling is automatic); customviewSelectioncommand gated onhasSingleSelection, routed through the wrapper'sonCommandcallback; autonomous<vn-grid-toolbar-status>row-count line plus host-driven<vn-grid-toolbar-status-message>for transient errors.