@aag1999/plugin-github-trending 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ import * as _backstage_plugin_home_react_alpha from '@backstage/plugin-home-react/alpha';
2
+ import * as _backstage_frontend_plugin_api from '@backstage/frontend-plugin-api';
3
+
4
+ /**
5
+ * New frontend system plugin. Mixed apps should also register the classic
6
+ * plugin so the API factory is discovered.
7
+ * @public
8
+ */
9
+ declare const _default: _backstage_frontend_plugin_api.OverridableFrontendPlugin<{}, {}, {
10
+ "api:github-trending/client": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
11
+ kind: "api";
12
+ name: "client";
13
+ config: {};
14
+ configInput: {};
15
+ output: _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.AnyApiFactory, "core.api.factory", {}>;
16
+ inputs: {};
17
+ params: <TApi, TImpl extends TApi, TDeps extends { [name in string]: unknown; }>(params: _backstage_frontend_plugin_api.ApiFactory<TApi, TImpl, TDeps>) => _backstage_frontend_plugin_api.ExtensionBlueprintParams<_backstage_frontend_plugin_api.AnyApiFactory>;
18
+ }>;
19
+ "home-page-widget:github-trending/ticker": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
20
+ config: {
21
+ since: "daily" | "weekly" | "monthly" | undefined;
22
+ language: string | undefined;
23
+ spokenLanguageCode: string | undefined;
24
+ maxItems: number | undefined;
25
+ pixelsPerSecond: number | undefined;
26
+ refreshIntervalMinutes: number | undefined;
27
+ showSpotlight: boolean | undefined;
28
+ };
29
+ configInput: {
30
+ since?: "daily" | "weekly" | "monthly" | undefined;
31
+ language?: string | undefined;
32
+ spokenLanguageCode?: string | undefined;
33
+ maxItems?: number | undefined;
34
+ pixelsPerSecond?: number | undefined;
35
+ refreshIntervalMinutes?: number | undefined;
36
+ showSpotlight?: boolean | undefined;
37
+ };
38
+ output: _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_plugin_home_react_alpha.HomePageWidgetData, "home.widget.data", {}>;
39
+ inputs: {};
40
+ kind: "home-page-widget";
41
+ name: "ticker";
42
+ params: _backstage_plugin_home_react_alpha.HomePageWidgetBlueprintParams;
43
+ }>;
44
+ }>;
45
+
46
+ export { _default as default };
@@ -0,0 +1,57 @@
1
+ import { createApiFactory } from '@backstage/core-plugin-api';
2
+ import { ApiBlueprint, fetchApiRef, discoveryApiRef, createFrontendPlugin } from '@backstage/frontend-plugin-api';
3
+ import { HomePageWidgetBlueprint } from '@backstage/plugin-home-react/alpha';
4
+ import { z } from 'zod/v4';
5
+ import { GithubTrendingClient, githubTrendingApiRef } from './api.esm.js';
6
+
7
+ const githubTrendingApi = ApiBlueprint.make({
8
+ name: "client",
9
+ params: (defineParams) => defineParams(
10
+ createApiFactory({
11
+ api: githubTrendingApiRef,
12
+ deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
13
+ factory: ({ discoveryApi, fetchApi }) => new GithubTrendingClient({ discoveryApi, fetchApi })
14
+ })
15
+ )
16
+ });
17
+ const githubTrendingTickerWidget = HomePageWidgetBlueprint.makeWithOverrides({
18
+ name: "ticker",
19
+ configSchema: {
20
+ since: z.enum(["daily", "weekly", "monthly"]).optional(),
21
+ language: z.string().optional(),
22
+ spokenLanguageCode: z.string().optional(),
23
+ maxItems: z.number().int().positive().optional(),
24
+ pixelsPerSecond: z.number().positive().optional(),
25
+ refreshIntervalMinutes: z.number().nonnegative().optional(),
26
+ showSpotlight: z.boolean().optional()
27
+ },
28
+ factory(originalFactory, { config }) {
29
+ return originalFactory({
30
+ name: "HomePageGithubTrendingTicker",
31
+ description: "News-channel ticker of GitHub Trending repositories",
32
+ layout: {
33
+ width: { minColumns: 12, defaultColumns: 12 },
34
+ height: { minRows: 2, defaultRows: 2 }
35
+ },
36
+ componentProps: {
37
+ since: config.since,
38
+ language: config.language,
39
+ spokenLanguageCode: config.spokenLanguageCode,
40
+ maxItems: config.maxItems,
41
+ pixelsPerSecond: config.pixelsPerSecond,
42
+ refreshIntervalMinutes: config.refreshIntervalMinutes,
43
+ showSpotlight: config.showSpotlight
44
+ },
45
+ components: () => import('./components/HomePageGithubTrendingTicker.esm.js').then((m) => ({
46
+ Content: m.HomePageGithubTrendingTicker
47
+ }))
48
+ });
49
+ }
50
+ });
51
+ var alpha = createFrontendPlugin({
52
+ pluginId: "github-trending",
53
+ extensions: [githubTrendingApi, githubTrendingTickerWidget]
54
+ });
55
+
56
+ export { alpha as default };
57
+ //# sourceMappingURL=alpha.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alpha.esm.js","sources":["../src/alpha.ts"],"sourcesContent":["/*\n * Copyright 2026 AAG1999 / contributors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n */\n\nimport { createApiFactory } from '@backstage/core-plugin-api';\nimport {\n ApiBlueprint,\n createFrontendPlugin,\n discoveryApiRef,\n fetchApiRef,\n} from '@backstage/frontend-plugin-api';\nimport { HomePageWidgetBlueprint } from '@backstage/plugin-home-react/alpha';\nimport { z } from 'zod/v4';\nimport { githubTrendingApiRef, GithubTrendingClient } from './api';\n\nconst githubTrendingApi = ApiBlueprint.make({\n name: 'client',\n params: defineParams =>\n defineParams(\n createApiFactory({\n api: githubTrendingApiRef,\n deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },\n factory: ({ discoveryApi, fetchApi }) =>\n new GithubTrendingClient({ discoveryApi, fetchApi }),\n }),\n ),\n});\n\n/**\n * Adopters tune the widget from `app-config.yaml` under\n * `app.extensions.home-page-widget:github-trending/ticker.config` rather than\n * forking the component. Presentation stays here; policy belongs to the app.\n */\nconst githubTrendingTickerWidget = HomePageWidgetBlueprint.makeWithOverrides({\n name: 'ticker',\n configSchema: {\n since: z.enum(['daily', 'weekly', 'monthly']).optional(),\n language: z.string().optional(),\n spokenLanguageCode: z.string().optional(),\n maxItems: z.number().int().positive().optional(),\n pixelsPerSecond: z.number().positive().optional(),\n refreshIntervalMinutes: z.number().nonnegative().optional(),\n showSpotlight: z.boolean().optional(),\n },\n factory(originalFactory, { config }) {\n return originalFactory({\n name: 'HomePageGithubTrendingTicker',\n description: 'News-channel ticker of GitHub Trending repositories',\n layout: {\n width: { minColumns: 12, defaultColumns: 12 },\n height: { minRows: 2, defaultRows: 2 },\n },\n componentProps: {\n since: config.since,\n language: config.language,\n spokenLanguageCode: config.spokenLanguageCode,\n maxItems: config.maxItems,\n pixelsPerSecond: config.pixelsPerSecond,\n refreshIntervalMinutes: config.refreshIntervalMinutes,\n showSpotlight: config.showSpotlight,\n },\n components: () =>\n import('./components/HomePageGithubTrendingTicker').then(m => ({\n Content: m.HomePageGithubTrendingTicker,\n })),\n });\n },\n});\n\n/**\n * New frontend system plugin. Mixed apps should also register the classic\n * plugin so the API factory is discovered.\n * @public\n */\nexport default createFrontendPlugin({\n pluginId: 'github-trending',\n extensions: [githubTrendingApi, githubTrendingTickerWidget],\n});\n"],"names":[],"mappings":";;;;;;AAqBA,MAAM,iBAAA,GAAoB,aAAa,IAAA,CAAK;AAAA,EAC1C,IAAA,EAAM,QAAA;AAAA,EACN,QAAQ,CAAA,YAAA,KACN,YAAA;AAAA,IACE,gBAAA,CAAiB;AAAA,MACf,GAAA,EAAK,oBAAA;AAAA,MACL,IAAA,EAAM,EAAE,YAAA,EAAc,eAAA,EAAiB,UAAU,WAAA,EAAY;AAAA,MAC7D,OAAA,EAAS,CAAC,EAAE,YAAA,EAAc,QAAA,EAAS,KACjC,IAAI,oBAAA,CAAqB,EAAE,YAAA,EAAc,QAAA,EAAU;AAAA,KACtD;AAAA;AAEP,CAAC,CAAA;AAOD,MAAM,0BAAA,GAA6B,wBAAwB,iBAAA,CAAkB;AAAA,EAC3E,IAAA,EAAM,QAAA;AAAA,EACN,YAAA,EAAc;AAAA,IACZ,KAAA,EAAO,EAAE,IAAA,CAAK,CAAC,SAAS,QAAA,EAAU,SAAS,CAAC,CAAA,CAAE,QAAA,EAAS;AAAA,IACvD,QAAA,EAAU,CAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,IAC9B,kBAAA,EAAoB,CAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,IACxC,QAAA,EAAU,EAAE,MAAA,EAAO,CAAE,KAAI,CAAE,QAAA,GAAW,QAAA,EAAS;AAAA,IAC/C,iBAAiB,CAAA,CAAE,MAAA,EAAO,CAAE,QAAA,GAAW,QAAA,EAAS;AAAA,IAChD,wBAAwB,CAAA,CAAE,MAAA,EAAO,CAAE,WAAA,GAAc,QAAA,EAAS;AAAA,IAC1D,aAAA,EAAe,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAA;AAAS,GACtC;AAAA,EACA,OAAA,CAAQ,eAAA,EAAiB,EAAE,MAAA,EAAO,EAAG;AACnC,IAAA,OAAO,eAAA,CAAgB;AAAA,MACrB,IAAA,EAAM,8BAAA;AAAA,MACN,WAAA,EAAa,qDAAA;AAAA,MACb,MAAA,EAAQ;AAAA,QACN,KAAA,EAAO,EAAE,UAAA,EAAY,EAAA,EAAI,gBAAgB,EAAA,EAAG;AAAA,QAC5C,MAAA,EAAQ,EAAE,OAAA,EAAS,CAAA,EAAG,aAAa,CAAA;AAAE,OACvC;AAAA,MACA,cAAA,EAAgB;AAAA,QACd,OAAO,MAAA,CAAO,KAAA;AAAA,QACd,UAAU,MAAA,CAAO,QAAA;AAAA,QACjB,oBAAoB,MAAA,CAAO,kBAAA;AAAA,QAC3B,UAAU,MAAA,CAAO,QAAA;AAAA,QACjB,iBAAiB,MAAA,CAAO,eAAA;AAAA,QACxB,wBAAwB,MAAA,CAAO,sBAAA;AAAA,QAC/B,eAAe,MAAA,CAAO;AAAA,OACxB;AAAA,MACA,YAAY,MACV,OAAO,kDAA2C,CAAA,CAAE,KAAK,CAAA,CAAA,MAAM;AAAA,QAC7D,SAAS,CAAA,CAAE;AAAA,OACb,CAAE;AAAA,KACL,CAAA;AAAA,EACH;AACF,CAAC,CAAA;AAOD,YAAe,oBAAA,CAAqB;AAAA,EAClC,QAAA,EAAU,iBAAA;AAAA,EACV,UAAA,EAAY,CAAC,iBAAA,EAAmB,0BAA0B;AAC5D,CAAC,CAAA;;"}
@@ -0,0 +1,36 @@
1
+ import { createApiRef } from '@backstage/core-plugin-api';
2
+
3
+ const githubTrendingApiRef = createApiRef({
4
+ id: "plugin.github-trending.service"
5
+ });
6
+ class GithubTrendingClient {
7
+ discoveryApi;
8
+ fetchApi;
9
+ constructor(options) {
10
+ this.discoveryApi = options.discoveryApi;
11
+ this.fetchApi = options.fetchApi;
12
+ }
13
+ async getRepositories(query = {}) {
14
+ const base = await this.discoveryApi.getBaseUrl("github-trending");
15
+ const params = new URLSearchParams();
16
+ if (query.since) {
17
+ params.set("since", query.since);
18
+ }
19
+ if (query.language) {
20
+ params.set("language", query.language);
21
+ }
22
+ if (query.spokenLanguageCode) {
23
+ params.set("spoken_language_code", query.spokenLanguageCode);
24
+ }
25
+ const qs = params.toString();
26
+ const url = qs ? `${base}/repositories?${qs}` : `${base}/repositories`;
27
+ const res = await this.fetchApi.fetch(url);
28
+ if (!res.ok) {
29
+ throw new Error(`GitHub Trending request failed: ${res.status}`);
30
+ }
31
+ return await res.json();
32
+ }
33
+ }
34
+
35
+ export { GithubTrendingClient, githubTrendingApiRef };
36
+ //# sourceMappingURL=api.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.esm.js","sources":["../src/api.ts"],"sourcesContent":["/*\n * Copyright 2026 AAG1999 / contributors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n */\n\nimport {\n createApiRef,\n DiscoveryApi,\n FetchApi,\n} from '@backstage/core-plugin-api';\nimport type {\n TrendingQuery,\n TrendingResponse,\n} from '@aag1999/plugin-github-trending-common';\n\nexport const githubTrendingApiRef = createApiRef<GithubTrendingApi>({\n id: 'plugin.github-trending.service',\n});\n\nexport interface GithubTrendingApi {\n getRepositories(query?: TrendingQuery): Promise<TrendingResponse>;\n}\n\nexport class GithubTrendingClient implements GithubTrendingApi {\n private readonly discoveryApi: DiscoveryApi;\n private readonly fetchApi: FetchApi;\n\n constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }) {\n this.discoveryApi = options.discoveryApi;\n this.fetchApi = options.fetchApi;\n }\n\n async getRepositories(query: TrendingQuery = {}): Promise<TrendingResponse> {\n const base = await this.discoveryApi.getBaseUrl('github-trending');\n const params = new URLSearchParams();\n if (query.since) {\n params.set('since', query.since);\n }\n if (query.language) {\n params.set('language', query.language);\n }\n if (query.spokenLanguageCode) {\n params.set('spoken_language_code', query.spokenLanguageCode);\n }\n const qs = params.toString();\n const url = qs ? `${base}/repositories?${qs}` : `${base}/repositories`;\n const res = await this.fetchApi.fetch(url);\n if (!res.ok) {\n throw new Error(`GitHub Trending request failed: ${res.status}`);\n }\n return (await res.json()) as TrendingResponse;\n }\n}\n"],"names":[],"mappings":";;AAoBO,MAAM,uBAAuB,YAAA,CAAgC;AAAA,EAClE,EAAA,EAAI;AACN,CAAC;AAMM,MAAM,oBAAA,CAAkD;AAAA,EAC5C,YAAA;AAAA,EACA,QAAA;AAAA,EAEjB,YAAY,OAAA,EAA6D;AACvE,IAAA,IAAA,CAAK,eAAe,OAAA,CAAQ,YAAA;AAC5B,IAAA,IAAA,CAAK,WAAW,OAAA,CAAQ,QAAA;AAAA,EAC1B;AAAA,EAEA,MAAM,eAAA,CAAgB,KAAA,GAAuB,EAAC,EAA8B;AAC1E,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,YAAA,CAAa,WAAW,iBAAiB,CAAA;AACjE,IAAA,MAAM,MAAA,GAAS,IAAI,eAAA,EAAgB;AACnC,IAAA,IAAI,MAAM,KAAA,EAAO;AACf,MAAA,MAAA,CAAO,GAAA,CAAI,OAAA,EAAS,KAAA,CAAM,KAAK,CAAA;AAAA,IACjC;AACA,IAAA,IAAI,MAAM,QAAA,EAAU;AAClB,MAAA,MAAA,CAAO,GAAA,CAAI,UAAA,EAAY,KAAA,CAAM,QAAQ,CAAA;AAAA,IACvC;AACA,IAAA,IAAI,MAAM,kBAAA,EAAoB;AAC5B,MAAA,MAAA,CAAO,GAAA,CAAI,sBAAA,EAAwB,KAAA,CAAM,kBAAkB,CAAA;AAAA,IAC7D;AACA,IAAA,MAAM,EAAA,GAAK,OAAO,QAAA,EAAS;AAC3B,IAAA,MAAM,GAAA,GAAM,KAAK,CAAA,EAAG,IAAI,iBAAiB,EAAE,CAAA,CAAA,GAAK,GAAG,IAAI,CAAA,aAAA,CAAA;AACvD,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,QAAA,CAAS,MAAM,GAAG,CAAA;AACzC,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,gCAAA,EAAmC,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAAA,IACjE;AACA,IAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,EACzB;AACF;;"}
@@ -0,0 +1,705 @@
1
+ import React, { useState, useEffect, useMemo, useRef, useLayoutEffect, Fragment } from 'react';
2
+ import { makeStyles } from '@material-ui/core/styles';
3
+ import IconButton from '@material-ui/core/IconButton';
4
+ import Tooltip from '@material-ui/core/Tooltip';
5
+ import Typography from '@material-ui/core/Typography';
6
+ import PauseIcon from '@material-ui/icons/Pause';
7
+ import PlayArrowIcon from '@material-ui/icons/PlayArrow';
8
+ import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp';
9
+ import TrendingUpIcon from '@material-ui/icons/TrendingUp';
10
+ import ChevronLeftIcon from '@material-ui/icons/ChevronLeft';
11
+ import ChevronRightIcon from '@material-ui/icons/ChevronRight';
12
+ import OpenInNewIcon from '@material-ui/icons/OpenInNew';
13
+ import { ResponseErrorPanel } from '@backstage/core-components';
14
+ import { useApi } from '@backstage/core-plugin-api';
15
+ import { githubTrendingApiRef } from '../api.esm.js';
16
+
17
+ const LANGUAGE_COLORS = {
18
+ JavaScript: "#f1e05a",
19
+ TypeScript: "#3178c6",
20
+ Python: "#3572A5",
21
+ Java: "#b07219",
22
+ Go: "#00ADD8",
23
+ Rust: "#dea584",
24
+ C: "#555555",
25
+ "C++": "#f34b7d",
26
+ "C#": "#178600",
27
+ Ruby: "#701516",
28
+ PHP: "#4F5D95",
29
+ Swift: "#F05138",
30
+ Kotlin: "#A97BFF",
31
+ Dart: "#00B4AB",
32
+ Shell: "#89e051",
33
+ HTML: "#e34c26",
34
+ CSS: "#563d7c",
35
+ SCSS: "#c6538c",
36
+ Vue: "#41b883",
37
+ Scala: "#c22d40",
38
+ "Objective-C": "#438eff",
39
+ R: "#198CE7",
40
+ Elixir: "#6e4a7e",
41
+ Clojure: "#db5855",
42
+ Haskell: "#5e5086",
43
+ Lua: "#000080",
44
+ Perl: "#0298c3",
45
+ Julia: "#a270ba",
46
+ Zig: "#ec915c",
47
+ Solidity: "#AA6746",
48
+ "Jupyter Notebook": "#DA5B0B",
49
+ Nix: "#7e7eff",
50
+ MDX: "#fcb32c"
51
+ };
52
+ const DEFAULT_REFRESH_MINUTES = 15;
53
+ const DEFAULT_PIXELS_PER_SECOND = 50;
54
+ const MIN_LOOP_SECONDS = 20;
55
+ const CLOCK_MS = 60 * 1e3;
56
+ const SPOTLIGHT_INTERVAL_MS = 9e3;
57
+ const SPOTLIGHT_DESC_MAX = 120;
58
+ const useStyles = makeStyles((theme) => ({
59
+ root: {
60
+ display: "flex",
61
+ flexDirection: "column",
62
+ justifyContent: "center",
63
+ gap: theme.spacing(0.5),
64
+ padding: theme.spacing(0.5, 1.5),
65
+ overflow: "hidden",
66
+ background: theme.palette.type === "dark" ? theme.palette.background.paper : theme.palette.grey[50],
67
+ borderRadius: theme.shape.borderRadius,
68
+ // A coloured left rail + icon reads as live editorial content, not an ad —
69
+ // the main defence against banner-blindness for a top-of-page strip.
70
+ borderLeft: `3px solid ${theme.palette.primary.main}`
71
+ },
72
+ // The crawl row: label + pause + moving viewport.
73
+ topRow: {
74
+ display: "flex",
75
+ alignItems: "center",
76
+ gap: theme.spacing(1.5),
77
+ minHeight: 44
78
+ },
79
+ label: {
80
+ flexShrink: 0,
81
+ display: "flex",
82
+ alignItems: "center",
83
+ gap: theme.spacing(0.75)
84
+ },
85
+ labelText: {
86
+ display: "flex",
87
+ flexDirection: "column",
88
+ lineHeight: 1.1
89
+ },
90
+ labelTitle: {
91
+ fontWeight: 800,
92
+ letterSpacing: 0.5,
93
+ textTransform: "uppercase",
94
+ fontSize: 11,
95
+ color: theme.palette.text.primary,
96
+ whiteSpace: "nowrap"
97
+ },
98
+ // The feed title doubles as the "see the full list" source link. Kept visually
99
+ // near-identical at rest so the strip does not read as a row of links; the
100
+ // affordance arrives on hover (plus a persistent, muted external-link glyph).
101
+ labelLink: {
102
+ fontWeight: 800,
103
+ letterSpacing: 0.5,
104
+ textTransform: "uppercase",
105
+ fontSize: 11,
106
+ color: theme.palette.text.primary,
107
+ whiteSpace: "nowrap",
108
+ display: "inline-flex",
109
+ alignItems: "center",
110
+ gap: 3,
111
+ textDecoration: "none",
112
+ borderRadius: 2,
113
+ "&:hover": {
114
+ color: theme.palette.primary.main,
115
+ textDecoration: "underline"
116
+ },
117
+ "&:hover $labelExternal": {
118
+ opacity: 1
119
+ },
120
+ "&:focus-visible": {
121
+ outline: `2px solid ${theme.palette.primary.main}`,
122
+ outlineOffset: 2
123
+ }
124
+ },
125
+ labelExternal: {
126
+ fontSize: 11,
127
+ opacity: 0.45,
128
+ transition: "opacity 120ms ease"
129
+ },
130
+ labelMeta: {
131
+ fontSize: 10,
132
+ color: theme.palette.text.secondary,
133
+ display: "flex",
134
+ alignItems: "center",
135
+ gap: 4,
136
+ whiteSpace: "nowrap"
137
+ },
138
+ liveDot: {
139
+ width: 6,
140
+ height: 6,
141
+ borderRadius: "50%",
142
+ backgroundColor: theme.palette.success.main,
143
+ display: "inline-block"
144
+ },
145
+ trendIcon: {
146
+ color: theme.palette.primary.main
147
+ },
148
+ viewport: {
149
+ flex: 1,
150
+ overflow: "hidden",
151
+ maskImage: "linear-gradient(to right, transparent, black 16px, black calc(100% - 16px), transparent)",
152
+ WebkitMaskImage: "linear-gradient(to right, transparent, black 16px, black calc(100% - 16px), transparent)"
153
+ },
154
+ // Reduced-motion / no-JS fallback: a single row the user scrolls by hand.
155
+ // Control without motion, and it never wraps into the cards below it.
156
+ staticViewport: {
157
+ flex: 1,
158
+ display: "flex",
159
+ alignItems: "center",
160
+ overflowX: "auto",
161
+ scrollbarWidth: "thin"
162
+ },
163
+ track: {
164
+ display: "flex",
165
+ alignItems: "stretch",
166
+ width: "max-content",
167
+ animationName: "$githubTrendingTicker",
168
+ animationTimingFunction: "linear",
169
+ animationIterationCount: "infinite",
170
+ willChange: "transform"
171
+ },
172
+ staticTrack: {
173
+ display: "flex",
174
+ alignItems: "stretch",
175
+ animation: "none"
176
+ },
177
+ paused: {
178
+ animationPlayState: "paused"
179
+ },
180
+ item: {
181
+ display: "inline-flex",
182
+ alignItems: "center",
183
+ gap: theme.spacing(0.75),
184
+ padding: theme.spacing(0, 2),
185
+ whiteSpace: "nowrap",
186
+ textDecoration: "none",
187
+ color: theme.palette.text.primary,
188
+ borderRadius: theme.shape.borderRadius,
189
+ transition: "background-color 120ms ease",
190
+ "&:hover": {
191
+ backgroundColor: theme.palette.type === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)"
192
+ },
193
+ "&:focus-visible": {
194
+ outline: `2px solid ${theme.palette.primary.main}`,
195
+ outlineOffset: -2
196
+ }
197
+ },
198
+ rank: {
199
+ color: theme.palette.text.disabled,
200
+ fontVariantNumeric: "tabular-nums",
201
+ fontSize: 11,
202
+ fontWeight: 700,
203
+ minWidth: 22
204
+ },
205
+ rankTop: {
206
+ color: theme.palette.warning.main
207
+ // top 3 get a warm accent (Von Restorff)
208
+ },
209
+ repo: {
210
+ fontSize: 13
211
+ },
212
+ owner: {
213
+ color: theme.palette.text.secondary,
214
+ fontWeight: 400
215
+ },
216
+ name: {
217
+ fontWeight: 700
218
+ },
219
+ langDot: {
220
+ width: 9,
221
+ height: 9,
222
+ borderRadius: "50%",
223
+ display: "inline-block",
224
+ flexShrink: 0
225
+ },
226
+ lang: {
227
+ fontSize: 11,
228
+ color: theme.palette.text.secondary
229
+ },
230
+ delta: {
231
+ display: "inline-flex",
232
+ alignItems: "center",
233
+ fontSize: 13,
234
+ fontWeight: 700,
235
+ fontVariantNumeric: "tabular-nums",
236
+ color: theme.palette.success.main
237
+ // velocity is the hero metric
238
+ },
239
+ deltaIcon: {
240
+ fontSize: 18,
241
+ marginLeft: -4,
242
+ marginRight: -2
243
+ },
244
+ stars: {
245
+ fontSize: 11,
246
+ color: theme.palette.text.secondary,
247
+ fontVariantNumeric: "tabular-nums"
248
+ },
249
+ sep: {
250
+ alignSelf: "center",
251
+ width: 1,
252
+ height: 18,
253
+ backgroundColor: theme.palette.divider,
254
+ flexShrink: 0
255
+ },
256
+ visuallyHidden: {
257
+ position: "absolute",
258
+ width: 1,
259
+ height: 1,
260
+ padding: 0,
261
+ margin: -1,
262
+ overflow: "hidden",
263
+ clip: "rect(0, 0, 0, 0)",
264
+ whiteSpace: "nowrap",
265
+ border: 0
266
+ },
267
+ empty: {
268
+ fontSize: 13,
269
+ color: theme.palette.text.secondary
270
+ },
271
+ // Skeleton: matches the final layout so the strip never looks broken while
272
+ // loading (skeleton screens feel faster than a spinner — Doherty threshold).
273
+ skeletonItem: {
274
+ display: "inline-flex",
275
+ alignItems: "center",
276
+ gap: theme.spacing(1),
277
+ padding: theme.spacing(0, 2)
278
+ },
279
+ skeletonBar: {
280
+ height: 12,
281
+ borderRadius: 6,
282
+ background: theme.palette.action.hover,
283
+ animation: "$githubTrendingShimmer 1.4s ease-in-out infinite"
284
+ },
285
+ // Spotlight rail: a static, slowly-rotating feature row under the crawl.
286
+ // Stillness for depth (readable description) vs the crawl's motion for breadth.
287
+ spotlightRow: {
288
+ display: "flex",
289
+ alignItems: "center",
290
+ gap: theme.spacing(1),
291
+ minHeight: 40,
292
+ borderTop: `1px solid ${theme.palette.divider}`,
293
+ paddingTop: theme.spacing(0.5)
294
+ },
295
+ spotTag: {
296
+ flexShrink: 0,
297
+ fontSize: 10,
298
+ fontWeight: 700,
299
+ letterSpacing: 0.5,
300
+ textTransform: "uppercase",
301
+ color: theme.palette.text.disabled
302
+ },
303
+ spotInner: {
304
+ flex: 1,
305
+ minWidth: 0,
306
+ display: "flex",
307
+ alignItems: "center",
308
+ gap: theme.spacing(0.75),
309
+ overflow: "hidden"
310
+ },
311
+ spotFade: {
312
+ animation: "$githubTrendingFade 380ms ease"
313
+ },
314
+ spotName: {
315
+ flexShrink: 0,
316
+ fontSize: 13,
317
+ textDecoration: "none",
318
+ color: theme.palette.text.primary,
319
+ "&:hover": { textDecoration: "underline" },
320
+ "&:focus-visible": {
321
+ outline: `2px solid ${theme.palette.primary.main}`,
322
+ outlineOffset: 2
323
+ }
324
+ },
325
+ spotDesc: {
326
+ flex: 1,
327
+ minWidth: 0,
328
+ overflow: "hidden",
329
+ textOverflow: "ellipsis",
330
+ whiteSpace: "nowrap",
331
+ fontSize: 12,
332
+ color: theme.palette.text.secondary
333
+ },
334
+ spotDescEmpty: {
335
+ fontStyle: "italic",
336
+ color: theme.palette.text.disabled
337
+ },
338
+ spotNav: {
339
+ flexShrink: 0,
340
+ display: "flex",
341
+ alignItems: "center",
342
+ gap: 2
343
+ },
344
+ spotCount: {
345
+ fontSize: 10,
346
+ color: theme.palette.text.disabled,
347
+ fontVariantNumeric: "tabular-nums",
348
+ minWidth: 30,
349
+ textAlign: "center"
350
+ },
351
+ "@keyframes githubTrendingTicker": {
352
+ "0%": { transform: "translateX(0)" },
353
+ "100%": { transform: "translateX(-50%)" }
354
+ },
355
+ "@keyframes githubTrendingShimmer": {
356
+ "0%, 100%": { opacity: 0.4 },
357
+ "50%": { opacity: 0.9 }
358
+ },
359
+ "@keyframes githubTrendingFade": {
360
+ from: { opacity: 0, transform: "translateY(2px)" },
361
+ to: { opacity: 1, transform: "none" }
362
+ }
363
+ }));
364
+ function formatCount(n) {
365
+ return n.toLocaleString();
366
+ }
367
+ function trimDescription(desc, max = SPOTLIGHT_DESC_MAX) {
368
+ if (!desc) {
369
+ return "";
370
+ }
371
+ if (desc.length <= max) {
372
+ return desc;
373
+ }
374
+ const slice = desc.slice(0, max);
375
+ const lastSpace = slice.lastIndexOf(" ");
376
+ return `${slice.slice(0, lastSpace > 40 ? lastSpace : max).trimEnd()}\u2026`;
377
+ }
378
+ function windowLabel(since) {
379
+ switch (since) {
380
+ case "weekly":
381
+ return "This week";
382
+ case "monthly":
383
+ return "This month";
384
+ default:
385
+ return "Today";
386
+ }
387
+ }
388
+ function agoLabel(fetchedAt, now) {
389
+ if (!fetchedAt) {
390
+ return "live";
391
+ }
392
+ const then = new Date(fetchedAt).getTime();
393
+ if (!Number.isFinite(then)) {
394
+ return "live";
395
+ }
396
+ const mins = Math.max(0, Math.round((now - then) / 6e4));
397
+ if (mins < 1) {
398
+ return "updated just now";
399
+ }
400
+ if (mins < 60) {
401
+ return `updated ${mins}m ago`;
402
+ }
403
+ const hrs = Math.round(mins / 60);
404
+ return `updated ${hrs}h ago`;
405
+ }
406
+ const TickerItem = ({
407
+ repo,
408
+ classes
409
+ }) => {
410
+ const color = repo.language ? LANGUAGE_COLORS[repo.language] : void 0;
411
+ const aria = `#${repo.rank} ${repo.owner}/${repo.name}${repo.language ? `, ${repo.language}` : ""}, up ${formatCount(repo.starsInPeriod)} stars ${repo.periodLabel}, ${formatCount(repo.stars)} total stars${repo.description ? `. ${repo.description}` : ""}`;
412
+ return /* @__PURE__ */ React.createElement(
413
+ "a",
414
+ {
415
+ className: classes.item,
416
+ href: repo.url,
417
+ target: "_blank",
418
+ rel: "noopener noreferrer",
419
+ title: repo.description || `${repo.owner}/${repo.name}`,
420
+ "aria-label": aria
421
+ },
422
+ /* @__PURE__ */ React.createElement(
423
+ "span",
424
+ {
425
+ className: `${classes.rank} ${repo.rank <= 3 ? classes.rankTop : ""}`
426
+ },
427
+ "#",
428
+ repo.rank
429
+ ),
430
+ /* @__PURE__ */ React.createElement("span", { className: classes.repo }, /* @__PURE__ */ React.createElement("span", { className: classes.owner }, repo.owner, "/"), /* @__PURE__ */ React.createElement("span", { className: classes.name }, repo.name)),
431
+ repo.language ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
432
+ "span",
433
+ {
434
+ className: classes.langDot,
435
+ style: { backgroundColor: color || "#8b949e" },
436
+ "aria-hidden": "true"
437
+ }
438
+ ), /* @__PURE__ */ React.createElement("span", { className: classes.lang }, repo.language)) : null,
439
+ /* @__PURE__ */ React.createElement("span", { className: classes.delta }, /* @__PURE__ */ React.createElement(ArrowDropUpIcon, { className: classes.deltaIcon, "aria-hidden": "true" }), formatCount(repo.starsInPeriod)),
440
+ /* @__PURE__ */ React.createElement("span", { className: classes.stars }, formatCount(repo.stars), "\u2605")
441
+ );
442
+ };
443
+ const Spotlight = ({
444
+ repo,
445
+ index,
446
+ total,
447
+ classes,
448
+ animate,
449
+ onPrev,
450
+ onNext,
451
+ onHoverChange
452
+ }) => {
453
+ const color = repo.language ? LANGUAGE_COLORS[repo.language] : void 0;
454
+ const desc = trimDescription(repo.description);
455
+ return /* @__PURE__ */ React.createElement(
456
+ "div",
457
+ {
458
+ className: classes.spotlightRow,
459
+ onMouseEnter: () => onHoverChange(true),
460
+ onMouseLeave: () => onHoverChange(false)
461
+ },
462
+ /* @__PURE__ */ React.createElement("span", { className: classes.spotTag }, "Spotlight"),
463
+ /* @__PURE__ */ React.createElement(
464
+ "div",
465
+ {
466
+ key: index,
467
+ className: `${classes.spotInner} ${animate ? classes.spotFade : ""}`
468
+ },
469
+ /* @__PURE__ */ React.createElement(
470
+ "span",
471
+ {
472
+ className: `${classes.rank} ${repo.rank <= 3 ? classes.rankTop : ""}`
473
+ },
474
+ "#",
475
+ repo.rank
476
+ ),
477
+ /* @__PURE__ */ React.createElement(
478
+ "a",
479
+ {
480
+ className: classes.spotName,
481
+ href: repo.url,
482
+ target: "_blank",
483
+ rel: "noopener noreferrer"
484
+ },
485
+ /* @__PURE__ */ React.createElement("span", { className: classes.owner }, repo.owner, "/"),
486
+ /* @__PURE__ */ React.createElement("span", { className: classes.name }, repo.name)
487
+ ),
488
+ repo.language ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
489
+ "span",
490
+ {
491
+ className: classes.langDot,
492
+ style: { backgroundColor: color || "#8b949e" },
493
+ "aria-hidden": "true"
494
+ }
495
+ ), /* @__PURE__ */ React.createElement("span", { className: classes.lang }, repo.language)) : null,
496
+ /* @__PURE__ */ React.createElement(
497
+ "span",
498
+ {
499
+ className: `${classes.spotDesc} ${desc ? "" : classes.spotDescEmpty}`
500
+ },
501
+ desc || "No description provided."
502
+ ),
503
+ /* @__PURE__ */ React.createElement("span", { className: classes.delta }, /* @__PURE__ */ React.createElement(ArrowDropUpIcon, { className: classes.deltaIcon, "aria-hidden": "true" }), formatCount(repo.starsInPeriod)),
504
+ /* @__PURE__ */ React.createElement("span", { className: classes.stars }, formatCount(repo.stars), "\u2605")
505
+ ),
506
+ /* @__PURE__ */ React.createElement("div", { className: classes.spotNav }, /* @__PURE__ */ React.createElement(
507
+ IconButton,
508
+ {
509
+ size: "small",
510
+ "aria-label": "Previous spotlight repository",
511
+ onClick: onPrev
512
+ },
513
+ /* @__PURE__ */ React.createElement(ChevronLeftIcon, { fontSize: "small" })
514
+ ), /* @__PURE__ */ React.createElement("span", { className: classes.spotCount, "aria-hidden": "true" }, index + 1, "/", total), /* @__PURE__ */ React.createElement(
515
+ IconButton,
516
+ {
517
+ size: "small",
518
+ "aria-label": "Next spotlight repository",
519
+ onClick: onNext
520
+ },
521
+ /* @__PURE__ */ React.createElement(ChevronRightIcon, { fontSize: "small" })
522
+ ))
523
+ );
524
+ };
525
+ const Skeleton = ({ classes }) => /* @__PURE__ */ React.createElement("div", { className: classes.viewport, "aria-hidden": "true" }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex" } }, Array.from({ length: 6 }).map((_, i) => /* @__PURE__ */ React.createElement("div", { className: classes.skeletonItem, key: i }, /* @__PURE__ */ React.createElement("span", { className: classes.skeletonBar, style: { width: 18 } }), /* @__PURE__ */ React.createElement("span", { className: classes.skeletonBar, style: { width: 120 } }), /* @__PURE__ */ React.createElement("span", { className: classes.skeletonBar, style: { width: 40 } })))));
526
+ const HomePageGithubTrendingTicker = (props = {}) => {
527
+ const {
528
+ since,
529
+ language,
530
+ spokenLanguageCode,
531
+ maxItems,
532
+ pixelsPerSecond = DEFAULT_PIXELS_PER_SECOND,
533
+ refreshIntervalMinutes = DEFAULT_REFRESH_MINUTES,
534
+ showSpotlight = true
535
+ } = props;
536
+ const classes = useStyles();
537
+ const api = useApi(githubTrendingApiRef);
538
+ const [response, setResponse] = useState();
539
+ const [error, setError] = useState();
540
+ const [userPaused, setUserPaused] = useState(false);
541
+ const [hoverPaused, setHoverPaused] = useState(false);
542
+ const [reduceMotion, setReduceMotion] = useState(false);
543
+ const [now, setNow] = useState(() => Date.now());
544
+ const [spotIndex, setSpotIndex] = useState(0);
545
+ const [spotHover, setSpotHover] = useState(false);
546
+ const paused = userPaused || hoverPaused;
547
+ const spotPaused = userPaused || spotHover;
548
+ useEffect(() => {
549
+ if (typeof window.matchMedia !== "function") {
550
+ return void 0;
551
+ }
552
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
553
+ const apply = () => setReduceMotion(mq.matches);
554
+ apply();
555
+ mq.addEventListener?.("change", apply);
556
+ return () => mq.removeEventListener?.("change", apply);
557
+ }, []);
558
+ useEffect(() => {
559
+ let cancelled = false;
560
+ const load = () => {
561
+ api.getRepositories({ since, language, spokenLanguageCode }).then((data) => {
562
+ if (!cancelled) {
563
+ setResponse(data);
564
+ setError(void 0);
565
+ setNow(Date.now());
566
+ }
567
+ }).catch((err) => {
568
+ if (!cancelled) {
569
+ setError(err instanceof Error ? err : new Error(String(err)));
570
+ }
571
+ });
572
+ };
573
+ load();
574
+ const refresh = refreshIntervalMinutes > 0 ? setInterval(load, refreshIntervalMinutes * 60 * 1e3) : void 0;
575
+ const clock = setInterval(() => setNow(Date.now()), CLOCK_MS);
576
+ return () => {
577
+ cancelled = true;
578
+ if (refresh) {
579
+ clearInterval(refresh);
580
+ }
581
+ clearInterval(clock);
582
+ };
583
+ }, [api, since, language, spokenLanguageCode, refreshIntervalMinutes]);
584
+ const repos = useMemo(() => {
585
+ const all = response?.repositories;
586
+ return maxItems && all ? all.slice(0, maxItems) : all;
587
+ }, [response, maxItems]);
588
+ const loop = useMemo(
589
+ () => repos && repos.length > 0 ? [...repos, ...repos] : [],
590
+ [repos]
591
+ );
592
+ const trackRef = useRef(null);
593
+ const [setWidthPx, setSetWidthPx] = useState(0);
594
+ useLayoutEffect(() => {
595
+ const el = trackRef.current;
596
+ if (!el) {
597
+ return void 0;
598
+ }
599
+ const measure = () => setSetWidthPx(el.scrollWidth / 2);
600
+ measure();
601
+ let ro;
602
+ if (typeof ResizeObserver === "function") {
603
+ ro = new ResizeObserver(measure);
604
+ ro.observe(el);
605
+ }
606
+ return () => ro?.disconnect();
607
+ }, [loop]);
608
+ const durationSec = Math.max(
609
+ MIN_LOOP_SECONDS,
610
+ setWidthPx > 0 ? setWidthPx / pixelsPerSecond : (repos?.length ?? 0) * 7
611
+ // pre-measurement fallback (~50 px/s)
612
+ );
613
+ const repoCount = repos?.length ?? 0;
614
+ useEffect(() => {
615
+ if (!showSpotlight || reduceMotion || spotPaused || repoCount <= 1) {
616
+ return void 0;
617
+ }
618
+ const id = setInterval(
619
+ () => setSpotIndex((i) => (i + 1) % repoCount),
620
+ SPOTLIGHT_INTERVAL_MS
621
+ );
622
+ return () => clearInterval(id);
623
+ }, [showSpotlight, reduceMotion, spotPaused, repoCount]);
624
+ if (error) {
625
+ return /* @__PURE__ */ React.createElement(ResponseErrorPanel, { error });
626
+ }
627
+ const header = /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: classes.label }, /* @__PURE__ */ React.createElement(TrendingUpIcon, { className: classes.trendIcon, fontSize: "small" }), /* @__PURE__ */ React.createElement("span", { className: classes.labelText }, response?.sourceUrl ? /* @__PURE__ */ React.createElement(Tooltip, { title: "Open GitHub Trending" }, /* @__PURE__ */ React.createElement(
628
+ "a",
629
+ {
630
+ className: classes.labelLink,
631
+ href: response.sourceUrl,
632
+ target: "_blank",
633
+ rel: "noopener noreferrer",
634
+ "aria-label": "GitHub Trending, opens the full list on github.com in a new tab"
635
+ },
636
+ "GitHub Trending",
637
+ /* @__PURE__ */ React.createElement(
638
+ OpenInNewIcon,
639
+ {
640
+ className: classes.labelExternal,
641
+ "aria-hidden": "true"
642
+ }
643
+ )
644
+ )) : /* @__PURE__ */ React.createElement("span", { className: classes.labelTitle }, "GitHub Trending"), /* @__PURE__ */ React.createElement("span", { className: classes.labelMeta }, /* @__PURE__ */ React.createElement("span", { className: classes.liveDot, "aria-hidden": "true" }), windowLabel(response?.since), " \xB7 ", agoLabel(response?.fetchedAt, now)))), /* @__PURE__ */ React.createElement(Tooltip, { title: paused ? "Resume ticker" : "Pause ticker" }, /* @__PURE__ */ React.createElement(
645
+ IconButton,
646
+ {
647
+ size: "small",
648
+ "aria-label": paused ? "Resume ticker" : "Pause ticker",
649
+ onClick: () => setUserPaused((value) => !value),
650
+ disabled: reduceMotion
651
+ },
652
+ paused ? /* @__PURE__ */ React.createElement(PlayArrowIcon, { fontSize: "small" }) : /* @__PURE__ */ React.createElement(PauseIcon, { fontSize: "small" })
653
+ )));
654
+ if (!response) {
655
+ return /* @__PURE__ */ React.createElement("div", { className: classes.root }, /* @__PURE__ */ React.createElement("div", { className: classes.topRow }, header, /* @__PURE__ */ React.createElement(Skeleton, { classes })));
656
+ }
657
+ const items = repos ?? [];
658
+ const spotlightRepo = items.length ? items[spotIndex % items.length] : void 0;
659
+ const gotoPrev = () => setSpotIndex((i) => (i - 1 + items.length) % items.length);
660
+ const gotoNext = () => setSpotIndex((i) => (i + 1) % items.length);
661
+ let crawl;
662
+ if (items.length === 0) {
663
+ crawl = /* @__PURE__ */ React.createElement(Typography, { className: classes.empty }, "No trending repositories right now.");
664
+ } else if (reduceMotion) {
665
+ crawl = /* @__PURE__ */ React.createElement("div", { className: classes.staticViewport }, /* @__PURE__ */ React.createElement("div", { className: classes.staticTrack }, items.map((repo, index) => /* @__PURE__ */ React.createElement(Fragment, { key: `${repo.owner}/${repo.name}` }, index > 0 ? /* @__PURE__ */ React.createElement("span", { className: classes.sep, "aria-hidden": "true" }) : null, /* @__PURE__ */ React.createElement(TickerItem, { repo, classes })))));
666
+ } else {
667
+ crawl = /* @__PURE__ */ React.createElement(
668
+ "div",
669
+ {
670
+ className: classes.viewport,
671
+ onMouseEnter: () => setHoverPaused(true),
672
+ onMouseLeave: () => setHoverPaused(false)
673
+ },
674
+ /* @__PURE__ */ React.createElement(
675
+ "div",
676
+ {
677
+ ref: trackRef,
678
+ className: `${classes.track} ${paused ? classes.paused : ""}`,
679
+ style: { animationDuration: `${durationSec}s` },
680
+ "data-testid": "github-trending-track"
681
+ },
682
+ loop.map((repo, index) => /* @__PURE__ */ React.createElement(Fragment, { key: `${repo.owner}/${repo.name}-${index}` }, index > 0 ? /* @__PURE__ */ React.createElement("span", { className: classes.sep, "aria-hidden": "true" }) : null, /* @__PURE__ */ React.createElement(TickerItem, { repo, classes })))
683
+ )
684
+ );
685
+ }
686
+ return /* @__PURE__ */ React.createElement("div", { className: classes.root }, /* @__PURE__ */ React.createElement("div", { className: classes.topRow }, header, crawl), showSpotlight && spotlightRepo ? /* @__PURE__ */ React.createElement(
687
+ Spotlight,
688
+ {
689
+ repo: spotlightRepo,
690
+ index: spotIndex % items.length,
691
+ total: items.length,
692
+ classes,
693
+ animate: !reduceMotion,
694
+ onPrev: gotoPrev,
695
+ onNext: gotoNext,
696
+ onHoverChange: setSpotHover
697
+ }
698
+ ) : null, /* @__PURE__ */ React.createElement("ul", { className: classes.visuallyHidden }, items.map((repo) => /* @__PURE__ */ React.createElement("li", { key: `${repo.owner}/${repo.name}` }, "#", repo.rank, " ", repo.owner, "/", repo.name, repo.language ? ` (${repo.language})` : "", " +", formatCount(repo.starsInPeriod), " stars ", repo.periodLabel, ",", " ", formatCount(repo.stars), " total", repo.description ? `. ${repo.description}` : ""))));
699
+ };
700
+ HomePageGithubTrendingTicker.data = {
701
+ "core.extensionName": "HomePageGithubTrendingTicker"
702
+ };
703
+
704
+ export { HomePageGithubTrendingTicker };
705
+ //# sourceMappingURL=HomePageGithubTrendingTicker.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HomePageGithubTrendingTicker.esm.js","sources":["../../src/components/HomePageGithubTrendingTicker.tsx"],"sourcesContent":["/*\n * Copyright 2026 AAG1999 / contributors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n */\n\nimport React, {\n Fragment,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { makeStyles } from '@material-ui/core/styles';\nimport IconButton from '@material-ui/core/IconButton';\nimport Tooltip from '@material-ui/core/Tooltip';\nimport Typography from '@material-ui/core/Typography';\nimport PauseIcon from '@material-ui/icons/Pause';\nimport PlayArrowIcon from '@material-ui/icons/PlayArrow';\nimport ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp';\nimport TrendingUpIcon from '@material-ui/icons/TrendingUp';\nimport ChevronLeftIcon from '@material-ui/icons/ChevronLeft';\nimport ChevronRightIcon from '@material-ui/icons/ChevronRight';\nimport OpenInNewIcon from '@material-ui/icons/OpenInNew';\nimport { ResponseErrorPanel } from '@backstage/core-components';\nimport { useApi } from '@backstage/core-plugin-api';\nimport type {\n TrendingRepository,\n TrendingResponse,\n TrendingSince,\n} from '@aag1999/plugin-github-trending-common';\nimport { githubTrendingApiRef } from '../api';\n\n/**\n * GitHub's own language colours (subset of linguist). A colour dot is parsed\n * far faster than a word — recognition over recall — so we lean on the palette\n * developers already know. Unknown languages fall back to a neutral dot.\n */\nconst LANGUAGE_COLORS: Record<string, string> = {\n JavaScript: '#f1e05a',\n TypeScript: '#3178c6',\n Python: '#3572A5',\n Java: '#b07219',\n Go: '#00ADD8',\n Rust: '#dea584',\n C: '#555555',\n 'C++': '#f34b7d',\n 'C#': '#178600',\n Ruby: '#701516',\n PHP: '#4F5D95',\n Swift: '#F05138',\n Kotlin: '#A97BFF',\n Dart: '#00B4AB',\n Shell: '#89e051',\n HTML: '#e34c26',\n CSS: '#563d7c',\n SCSS: '#c6538c',\n Vue: '#41b883',\n Scala: '#c22d40',\n 'Objective-C': '#438eff',\n R: '#198CE7',\n Elixir: '#6e4a7e',\n Clojure: '#db5855',\n Haskell: '#5e5086',\n Lua: '#000080',\n Perl: '#0298c3',\n Julia: '#a270ba',\n Zig: '#ec915c',\n Solidity: '#AA6746',\n 'Jupyter Notebook': '#DA5B0B',\n Nix: '#7e7eff',\n MDX: '#fcb32c',\n};\n\nconst DEFAULT_REFRESH_MINUTES = 15; // matches backend cache TTL order of magnitude\n// Industry practice for readable tickers is 30-80 px/s (>100 px/s is\n// considered unreadable); reading research puts the optimum for scrolling text\n// near ~6 characters/s, which is ~50 px/s at this font size. So default to 50.\n// Refs: crowntv-us.com/blog/scrolling-ticker-tape-display,\n// Frontiers in Psychology 2017 (10.3389/fpsyg.2017.01390).\nconst DEFAULT_PIXELS_PER_SECOND = 50;\nconst MIN_LOOP_SECONDS = 20; // guard against a near-empty, hyper-short loop\nconst CLOCK_MS = 60 * 1000; // re-render the \"updated Xm ago\" label\nconst SPOTLIGHT_INTERVAL_MS = 9000; // discrete dwell so a description is readable\nconst SPOTLIGHT_DESC_MAX = 120; // characters before trimming the description\n\n/**\n * Presentation is owned by this plugin; policy is not. Everything an adopter\n * might reasonably want different is a prop, so nobody has to fork the widget.\n *\n * @public\n */\nexport type HomePageGithubTrendingTickerProps = {\n /** Trending window to request. Defaults to the backend's configured window. */\n since?: TrendingSince;\n /** Restrict to a programming language, e.g. `typescript`. */\n language?: string;\n /** Restrict to a spoken language code, e.g. `en`. */\n spokenLanguageCode?: string;\n /** Cap the number of repositories shown. Defaults to everything returned. */\n maxItems?: number;\n /**\n * Scroll speed in pixels per second. Industry-comfortable range is ~30-80;\n * above ~100 is unreadable. Defaults to 50 (~6 characters/second here).\n */\n pixelsPerSecond?: number;\n /** How often to re-fetch. Set to 0 to disable polling. */\n refreshIntervalMinutes?: number;\n /**\n * Show the static \"spotlight\" rail beneath the crawl that features one repo\n * at a time with its trimmed description. Defaults to true.\n */\n showSpotlight?: boolean;\n};\n\nconst useStyles = makeStyles(theme => ({\n root: {\n display: 'flex',\n flexDirection: 'column',\n justifyContent: 'center',\n gap: theme.spacing(0.5),\n padding: theme.spacing(0.5, 1.5),\n overflow: 'hidden',\n background:\n theme.palette.type === 'dark'\n ? theme.palette.background.paper\n : theme.palette.grey[50],\n borderRadius: theme.shape.borderRadius,\n // A coloured left rail + icon reads as live editorial content, not an ad —\n // the main defence against banner-blindness for a top-of-page strip.\n borderLeft: `3px solid ${theme.palette.primary.main}`,\n },\n // The crawl row: label + pause + moving viewport.\n topRow: {\n display: 'flex',\n alignItems: 'center',\n gap: theme.spacing(1.5),\n minHeight: 44,\n },\n label: {\n flexShrink: 0,\n display: 'flex',\n alignItems: 'center',\n gap: theme.spacing(0.75),\n },\n labelText: {\n display: 'flex',\n flexDirection: 'column',\n lineHeight: 1.1,\n },\n labelTitle: {\n fontWeight: 800,\n letterSpacing: 0.5,\n textTransform: 'uppercase',\n fontSize: 11,\n color: theme.palette.text.primary,\n whiteSpace: 'nowrap',\n },\n // The feed title doubles as the \"see the full list\" source link. Kept visually\n // near-identical at rest so the strip does not read as a row of links; the\n // affordance arrives on hover (plus a persistent, muted external-link glyph).\n labelLink: {\n fontWeight: 800,\n letterSpacing: 0.5,\n textTransform: 'uppercase',\n fontSize: 11,\n color: theme.palette.text.primary,\n whiteSpace: 'nowrap',\n display: 'inline-flex',\n alignItems: 'center',\n gap: 3,\n textDecoration: 'none',\n borderRadius: 2,\n '&:hover': {\n color: theme.palette.primary.main,\n textDecoration: 'underline',\n },\n '&:hover $labelExternal': {\n opacity: 1,\n },\n '&:focus-visible': {\n outline: `2px solid ${theme.palette.primary.main}`,\n outlineOffset: 2,\n },\n },\n labelExternal: {\n fontSize: 11,\n opacity: 0.45,\n transition: 'opacity 120ms ease',\n },\n labelMeta: {\n fontSize: 10,\n color: theme.palette.text.secondary,\n display: 'flex',\n alignItems: 'center',\n gap: 4,\n whiteSpace: 'nowrap',\n },\n liveDot: {\n width: 6,\n height: 6,\n borderRadius: '50%',\n backgroundColor: theme.palette.success.main,\n display: 'inline-block',\n },\n trendIcon: {\n color: theme.palette.primary.main,\n },\n viewport: {\n flex: 1,\n overflow: 'hidden',\n maskImage:\n 'linear-gradient(to right, transparent, black 16px, black calc(100% - 16px), transparent)',\n WebkitMaskImage:\n 'linear-gradient(to right, transparent, black 16px, black calc(100% - 16px), transparent)',\n },\n // Reduced-motion / no-JS fallback: a single row the user scrolls by hand.\n // Control without motion, and it never wraps into the cards below it.\n staticViewport: {\n flex: 1,\n display: 'flex',\n alignItems: 'center',\n overflowX: 'auto',\n scrollbarWidth: 'thin',\n },\n track: {\n display: 'flex',\n alignItems: 'stretch',\n width: 'max-content',\n animationName: '$githubTrendingTicker',\n animationTimingFunction: 'linear',\n animationIterationCount: 'infinite',\n willChange: 'transform',\n },\n staticTrack: {\n display: 'flex',\n alignItems: 'stretch',\n animation: 'none',\n },\n paused: {\n animationPlayState: 'paused',\n },\n item: {\n display: 'inline-flex',\n alignItems: 'center',\n gap: theme.spacing(0.75),\n padding: theme.spacing(0, 2),\n whiteSpace: 'nowrap',\n textDecoration: 'none',\n color: theme.palette.text.primary,\n borderRadius: theme.shape.borderRadius,\n transition: 'background-color 120ms ease',\n '&:hover': {\n backgroundColor:\n theme.palette.type === 'dark'\n ? 'rgba(255,255,255,0.06)'\n : 'rgba(0,0,0,0.04)',\n },\n '&:focus-visible': {\n outline: `2px solid ${theme.palette.primary.main}`,\n outlineOffset: -2,\n },\n },\n rank: {\n color: theme.palette.text.disabled,\n fontVariantNumeric: 'tabular-nums',\n fontSize: 11,\n fontWeight: 700,\n minWidth: 22,\n },\n rankTop: {\n color: theme.palette.warning.main, // top 3 get a warm accent (Von Restorff)\n },\n repo: {\n fontSize: 13,\n },\n owner: {\n color: theme.palette.text.secondary,\n fontWeight: 400,\n },\n name: {\n fontWeight: 700,\n },\n langDot: {\n width: 9,\n height: 9,\n borderRadius: '50%',\n display: 'inline-block',\n flexShrink: 0,\n },\n lang: {\n fontSize: 11,\n color: theme.palette.text.secondary,\n },\n delta: {\n display: 'inline-flex',\n alignItems: 'center',\n fontSize: 13,\n fontWeight: 700,\n fontVariantNumeric: 'tabular-nums',\n color: theme.palette.success.main, // velocity is the hero metric\n },\n deltaIcon: {\n fontSize: 18,\n marginLeft: -4,\n marginRight: -2,\n },\n stars: {\n fontSize: 11,\n color: theme.palette.text.secondary,\n fontVariantNumeric: 'tabular-nums',\n },\n sep: {\n alignSelf: 'center',\n width: 1,\n height: 18,\n backgroundColor: theme.palette.divider,\n flexShrink: 0,\n },\n visuallyHidden: {\n position: 'absolute',\n width: 1,\n height: 1,\n padding: 0,\n margin: -1,\n overflow: 'hidden',\n clip: 'rect(0, 0, 0, 0)',\n whiteSpace: 'nowrap',\n border: 0,\n },\n empty: {\n fontSize: 13,\n color: theme.palette.text.secondary,\n },\n // Skeleton: matches the final layout so the strip never looks broken while\n // loading (skeleton screens feel faster than a spinner — Doherty threshold).\n skeletonItem: {\n display: 'inline-flex',\n alignItems: 'center',\n gap: theme.spacing(1),\n padding: theme.spacing(0, 2),\n },\n skeletonBar: {\n height: 12,\n borderRadius: 6,\n background: theme.palette.action.hover,\n animation: '$githubTrendingShimmer 1.4s ease-in-out infinite',\n },\n // Spotlight rail: a static, slowly-rotating feature row under the crawl.\n // Stillness for depth (readable description) vs the crawl's motion for breadth.\n spotlightRow: {\n display: 'flex',\n alignItems: 'center',\n gap: theme.spacing(1),\n minHeight: 40,\n borderTop: `1px solid ${theme.palette.divider}`,\n paddingTop: theme.spacing(0.5),\n },\n spotTag: {\n flexShrink: 0,\n fontSize: 10,\n fontWeight: 700,\n letterSpacing: 0.5,\n textTransform: 'uppercase',\n color: theme.palette.text.disabled,\n },\n spotInner: {\n flex: 1,\n minWidth: 0,\n display: 'flex',\n alignItems: 'center',\n gap: theme.spacing(0.75),\n overflow: 'hidden',\n },\n spotFade: {\n animation: '$githubTrendingFade 380ms ease',\n },\n spotName: {\n flexShrink: 0,\n fontSize: 13,\n textDecoration: 'none',\n color: theme.palette.text.primary,\n '&:hover': { textDecoration: 'underline' },\n '&:focus-visible': {\n outline: `2px solid ${theme.palette.primary.main}`,\n outlineOffset: 2,\n },\n },\n spotDesc: {\n flex: 1,\n minWidth: 0,\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n fontSize: 12,\n color: theme.palette.text.secondary,\n },\n spotDescEmpty: {\n fontStyle: 'italic',\n color: theme.palette.text.disabled,\n },\n spotNav: {\n flexShrink: 0,\n display: 'flex',\n alignItems: 'center',\n gap: 2,\n },\n spotCount: {\n fontSize: 10,\n color: theme.palette.text.disabled,\n fontVariantNumeric: 'tabular-nums',\n minWidth: 30,\n textAlign: 'center',\n },\n '@keyframes githubTrendingTicker': {\n '0%': { transform: 'translateX(0)' },\n '100%': { transform: 'translateX(-50%)' },\n },\n '@keyframes githubTrendingShimmer': {\n '0%, 100%': { opacity: 0.4 },\n '50%': { opacity: 0.9 },\n },\n '@keyframes githubTrendingFade': {\n from: { opacity: 0, transform: 'translateY(2px)' },\n to: { opacity: 1, transform: 'none' },\n },\n}));\n\ntype Classes = ReturnType<typeof useStyles>;\n\nfunction formatCount(n: number): string {\n return n.toLocaleString();\n}\n\nfunction trimDescription(desc: string | undefined, max = SPOTLIGHT_DESC_MAX): string {\n if (!desc) {\n return '';\n }\n if (desc.length <= max) {\n return desc;\n }\n const slice = desc.slice(0, max);\n const lastSpace = slice.lastIndexOf(' ');\n return `${slice.slice(0, lastSpace > 40 ? lastSpace : max).trimEnd()}…`;\n}\n\nfunction windowLabel(since: TrendingSince | undefined): string {\n switch (since) {\n case 'weekly':\n return 'This week';\n case 'monthly':\n return 'This month';\n default:\n return 'Today';\n }\n}\n\nfunction agoLabel(fetchedAt: string | undefined, now: number): string {\n if (!fetchedAt) {\n return 'live';\n }\n const then = new Date(fetchedAt).getTime();\n if (!Number.isFinite(then)) {\n return 'live';\n }\n const mins = Math.max(0, Math.round((now - then) / 60000));\n if (mins < 1) {\n return 'updated just now';\n }\n if (mins < 60) {\n return `updated ${mins}m ago`;\n }\n const hrs = Math.round(mins / 60);\n return `updated ${hrs}h ago`;\n}\n\nconst TickerItem = ({\n repo,\n classes,\n}: {\n repo: TrendingRepository;\n classes: Classes;\n}) => {\n const color = repo.language ? LANGUAGE_COLORS[repo.language] : undefined;\n const aria =\n `#${repo.rank} ${repo.owner}/${repo.name}` +\n `${repo.language ? `, ${repo.language}` : ''}` +\n `, up ${formatCount(repo.starsInPeriod)} stars ${repo.periodLabel}` +\n `, ${formatCount(repo.stars)} total stars` +\n `${repo.description ? `. ${repo.description}` : ''}`;\n return (\n <a\n className={classes.item}\n href={repo.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n title={repo.description || `${repo.owner}/${repo.name}`}\n aria-label={aria}\n >\n <span\n className={`${classes.rank} ${repo.rank <= 3 ? classes.rankTop : ''}`}\n >\n #{repo.rank}\n </span>\n <span className={classes.repo}>\n <span className={classes.owner}>{repo.owner}/</span>\n <span className={classes.name}>{repo.name}</span>\n </span>\n {repo.language ? (\n <>\n <span\n className={classes.langDot}\n style={{ backgroundColor: color || '#8b949e' }}\n aria-hidden=\"true\"\n />\n <span className={classes.lang}>{repo.language}</span>\n </>\n ) : null}\n <span className={classes.delta}>\n <ArrowDropUpIcon className={classes.deltaIcon} aria-hidden=\"true\" />\n {formatCount(repo.starsInPeriod)}\n </span>\n <span className={classes.stars}>{formatCount(repo.stars)}★</span>\n </a>\n );\n};\n\nconst Spotlight = ({\n repo,\n index,\n total,\n classes,\n animate,\n onPrev,\n onNext,\n onHoverChange,\n}: {\n repo: TrendingRepository;\n index: number;\n total: number;\n classes: Classes;\n animate: boolean;\n onPrev: () => void;\n onNext: () => void;\n onHoverChange: (hovered: boolean) => void;\n}) => {\n const color = repo.language ? LANGUAGE_COLORS[repo.language] : undefined;\n const desc = trimDescription(repo.description);\n return (\n <div\n className={classes.spotlightRow}\n onMouseEnter={() => onHoverChange(true)}\n onMouseLeave={() => onHoverChange(false)}\n >\n <span className={classes.spotTag}>Spotlight</span>\n {/* key remounts the row on change so the fade re-triggers */}\n <div\n key={index}\n className={`${classes.spotInner} ${animate ? classes.spotFade : ''}`}\n >\n <span\n className={`${classes.rank} ${repo.rank <= 3 ? classes.rankTop : ''}`}\n >\n #{repo.rank}\n </span>\n <a\n className={classes.spotName}\n href={repo.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n <span className={classes.owner}>{repo.owner}/</span>\n <span className={classes.name}>{repo.name}</span>\n </a>\n {repo.language ? (\n <>\n <span\n className={classes.langDot}\n style={{ backgroundColor: color || '#8b949e' }}\n aria-hidden=\"true\"\n />\n <span className={classes.lang}>{repo.language}</span>\n </>\n ) : null}\n <span\n className={`${classes.spotDesc} ${desc ? '' : classes.spotDescEmpty}`}\n >\n {desc || 'No description provided.'}\n </span>\n <span className={classes.delta}>\n <ArrowDropUpIcon className={classes.deltaIcon} aria-hidden=\"true\" />\n {formatCount(repo.starsInPeriod)}\n </span>\n <span className={classes.stars}>{formatCount(repo.stars)}★</span>\n </div>\n <div className={classes.spotNav}>\n <IconButton\n size=\"small\"\n aria-label=\"Previous spotlight repository\"\n onClick={onPrev}\n >\n <ChevronLeftIcon fontSize=\"small\" />\n </IconButton>\n <span className={classes.spotCount} aria-hidden=\"true\">\n {index + 1}/{total}\n </span>\n <IconButton\n size=\"small\"\n aria-label=\"Next spotlight repository\"\n onClick={onNext}\n >\n <ChevronRightIcon fontSize=\"small\" />\n </IconButton>\n </div>\n </div>\n );\n};\n\nconst Skeleton = ({ classes }: { classes: Classes }) => (\n <div className={classes.viewport} aria-hidden=\"true\">\n <div style={{ display: 'flex' }}>\n {Array.from({ length: 6 }).map((_, i) => (\n <div className={classes.skeletonItem} key={i}>\n <span className={classes.skeletonBar} style={{ width: 18 }} />\n <span className={classes.skeletonBar} style={{ width: 120 }} />\n <span className={classes.skeletonBar} style={{ width: 40 }} />\n </div>\n ))}\n </div>\n </div>\n);\n\n/**\n * Stock-ticker / news-crawl of GitHub Trending.\n *\n * Design intent (human-factors):\n * - Velocity (\"+stars today\") is the hero — that is the \"what's hot now\" signal.\n * - A GitHub language colour dot is glanceable where a word is not.\n * - The one-line description (biggest click driver) lives in the hover tooltip.\n * - Motion is pausable (WCAG 2.2.2), pauses on hover so you can read, and is\n * replaced by a manually-scrollable row under prefers-reduced-motion.\n * Remove the widget from the Home layout to hide it entirely.\n */\nexport const HomePageGithubTrendingTicker = (\n props: HomePageGithubTrendingTickerProps = {},\n) => {\n const {\n since,\n language,\n spokenLanguageCode,\n maxItems,\n pixelsPerSecond = DEFAULT_PIXELS_PER_SECOND,\n refreshIntervalMinutes = DEFAULT_REFRESH_MINUTES,\n showSpotlight = true,\n } = props;\n const classes = useStyles();\n const api = useApi(githubTrendingApiRef);\n const [response, setResponse] = useState<TrendingResponse | undefined>();\n const [error, setError] = useState<Error>();\n const [userPaused, setUserPaused] = useState(false);\n const [hoverPaused, setHoverPaused] = useState(false);\n const [reduceMotion, setReduceMotion] = useState(false);\n const [now, setNow] = useState(() => Date.now());\n const [spotIndex, setSpotIndex] = useState(0);\n const [spotHover, setSpotHover] = useState(false);\n const paused = userPaused || hoverPaused;\n const spotPaused = userPaused || spotHover;\n\n useEffect(() => {\n if (typeof window.matchMedia !== 'function') {\n return undefined;\n }\n const mq = window.matchMedia('(prefers-reduced-motion: reduce)');\n const apply = () => setReduceMotion(mq.matches);\n apply();\n mq.addEventListener?.('change', apply);\n return () => mq.removeEventListener?.('change', apply);\n }, []);\n\n useEffect(() => {\n let cancelled = false;\n const load = () => {\n api\n .getRepositories({ since, language, spokenLanguageCode })\n .then(data => {\n if (!cancelled) {\n setResponse(data);\n setError(undefined);\n setNow(Date.now());\n }\n })\n .catch(err => {\n if (!cancelled) {\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n };\n load();\n const refresh =\n refreshIntervalMinutes > 0\n ? setInterval(load, refreshIntervalMinutes * 60 * 1000)\n : undefined;\n const clock = setInterval(() => setNow(Date.now()), CLOCK_MS);\n return () => {\n cancelled = true;\n if (refresh) {\n clearInterval(refresh);\n }\n clearInterval(clock);\n };\n }, [api, since, language, spokenLanguageCode, refreshIntervalMinutes]);\n\n const repos = useMemo(() => {\n const all = response?.repositories;\n return maxItems && all ? all.slice(0, maxItems) : all;\n }, [response, maxItems]);\n\n const loop = useMemo(\n () => (repos && repos.length > 0 ? [...repos, ...repos] : []),\n [repos],\n );\n\n // Speed is defined in px/s (the unit the readability standards use), so it\n // stays constant no matter how wide individual repo rows happen to be. To do\n // that we measure the rendered width of one set and derive the loop duration.\n const trackRef = useRef<HTMLDivElement | null>(null);\n const [setWidthPx, setSetWidthPx] = useState(0);\n\n useLayoutEffect(() => {\n const el = trackRef.current;\n if (!el) {\n return undefined;\n }\n const measure = () => setSetWidthPx(el.scrollWidth / 2); // track is duplicated\n measure();\n let ro: ResizeObserver | undefined;\n if (typeof ResizeObserver === 'function') {\n ro = new ResizeObserver(measure);\n ro.observe(el);\n }\n return () => ro?.disconnect();\n }, [loop]);\n\n const durationSec = Math.max(\n MIN_LOOP_SECONDS,\n setWidthPx > 0\n ? setWidthPx / pixelsPerSecond\n : (repos?.length ?? 0) * 7, // pre-measurement fallback (~50 px/s)\n );\n\n // Discrete rotation for the spotlight: readable dwell, not scrolling text.\n // Under reduced-motion it never auto-advances — the nav arrows drive it.\n const repoCount = repos?.length ?? 0;\n useEffect(() => {\n if (!showSpotlight || reduceMotion || spotPaused || repoCount <= 1) {\n return undefined;\n }\n const id = setInterval(\n () => setSpotIndex(i => (i + 1) % repoCount),\n SPOTLIGHT_INTERVAL_MS,\n );\n return () => clearInterval(id);\n }, [showSpotlight, reduceMotion, spotPaused, repoCount]);\n\n if (error) {\n return <ResponseErrorPanel error={error} />;\n }\n\n const header = (\n <>\n <div className={classes.label}>\n <TrendingUpIcon className={classes.trendIcon} fontSize=\"small\" />\n <span className={classes.labelText}>\n {response?.sourceUrl ? (\n <Tooltip title=\"Open GitHub Trending\">\n <a\n className={classes.labelLink}\n href={response.sourceUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label=\"GitHub Trending, opens the full list on github.com in a new tab\"\n >\n GitHub Trending\n <OpenInNewIcon\n className={classes.labelExternal}\n aria-hidden=\"true\"\n />\n </a>\n </Tooltip>\n ) : (\n <span className={classes.labelTitle}>GitHub Trending</span>\n )}\n <span className={classes.labelMeta}>\n <span className={classes.liveDot} aria-hidden=\"true\" />\n {windowLabel(response?.since)} · {agoLabel(response?.fetchedAt, now)}\n </span>\n </span>\n </div>\n <Tooltip title={paused ? 'Resume ticker' : 'Pause ticker'}>\n <IconButton\n size=\"small\"\n aria-label={paused ? 'Resume ticker' : 'Pause ticker'}\n onClick={() => setUserPaused(value => !value)}\n disabled={reduceMotion}\n >\n {paused ? (\n <PlayArrowIcon fontSize=\"small\" />\n ) : (\n <PauseIcon fontSize=\"small\" />\n )}\n </IconButton>\n </Tooltip>\n </>\n );\n\n if (!response) {\n return (\n <div className={classes.root}>\n <div className={classes.topRow}>\n {header}\n <Skeleton classes={classes} />\n </div>\n </div>\n );\n }\n\n const items = repos ?? [];\n\n const spotlightRepo = items.length ? items[spotIndex % items.length] : undefined;\n const gotoPrev = () =>\n setSpotIndex(i => (i - 1 + items.length) % items.length);\n const gotoNext = () => setSpotIndex(i => (i + 1) % items.length);\n\n let crawl;\n if (items.length === 0) {\n crawl = (\n <Typography className={classes.empty}>\n No trending repositories right now.\n </Typography>\n );\n } else if (reduceMotion) {\n crawl = (\n <div className={classes.staticViewport}>\n <div className={classes.staticTrack}>\n {items.map((repo, index) => (\n <Fragment key={`${repo.owner}/${repo.name}`}>\n {index > 0 ? (\n <span className={classes.sep} aria-hidden=\"true\" />\n ) : null}\n <TickerItem repo={repo} classes={classes} />\n </Fragment>\n ))}\n </div>\n </div>\n );\n } else {\n crawl = (\n <div\n className={classes.viewport}\n onMouseEnter={() => setHoverPaused(true)}\n onMouseLeave={() => setHoverPaused(false)}\n >\n <div\n ref={trackRef}\n className={`${classes.track} ${paused ? classes.paused : ''}`}\n style={{ animationDuration: `${durationSec}s` }}\n data-testid=\"github-trending-track\"\n >\n {loop.map((repo, index) => (\n <Fragment key={`${repo.owner}/${repo.name}-${index}`}>\n {index > 0 ? (\n <span className={classes.sep} aria-hidden=\"true\" />\n ) : null}\n <TickerItem repo={repo} classes={classes} />\n </Fragment>\n ))}\n </div>\n </div>\n );\n }\n\n return (\n <div className={classes.root}>\n <div className={classes.topRow}>\n {header}\n {crawl}\n </div>\n {showSpotlight && spotlightRepo ? (\n <Spotlight\n repo={spotlightRepo}\n index={spotIndex % items.length}\n total={items.length}\n classes={classes}\n animate={!reduceMotion}\n onPrev={gotoPrev}\n onNext={gotoNext}\n onHoverChange={setSpotHover}\n />\n ) : null}\n <ul className={classes.visuallyHidden}>\n {items.map(repo => (\n <li key={`${repo.owner}/${repo.name}`}>\n #{repo.rank} {repo.owner}/{repo.name}\n {repo.language ? ` (${repo.language})` : ''} +\n {formatCount(repo.starsInPeriod)} stars {repo.periodLabel},{' '}\n {formatCount(repo.stars)} total\n {repo.description ? `. ${repo.description}` : ''}\n </li>\n ))}\n </ul>\n </div>\n );\n};\n\n(HomePageGithubTrendingTicker as any).data = {\n 'core.extensionName': 'HomePageGithubTrendingTicker',\n};\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ import * as _backstage_core_plugin_api from '@backstage/core-plugin-api';
2
+ import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
3
+ import * as _backstage_frontend_plugin_api from '@backstage/frontend-plugin-api';
4
+ import { TrendingQuery, TrendingResponse, TrendingSince } from '@aag1999/plugin-github-trending-common';
5
+ import React from 'react';
6
+
7
+ declare const githubTrendingPlugin: _backstage_core_plugin_api.BackstagePlugin<{}, {}, {}>;
8
+
9
+ declare const githubTrendingApiRef: _backstage_frontend_plugin_api.ApiRef<GithubTrendingApi>;
10
+ interface GithubTrendingApi {
11
+ getRepositories(query?: TrendingQuery): Promise<TrendingResponse>;
12
+ }
13
+ declare class GithubTrendingClient implements GithubTrendingApi {
14
+ private readonly discoveryApi;
15
+ private readonly fetchApi;
16
+ constructor(options: {
17
+ discoveryApi: DiscoveryApi;
18
+ fetchApi: FetchApi;
19
+ });
20
+ getRepositories(query?: TrendingQuery): Promise<TrendingResponse>;
21
+ }
22
+
23
+ /**
24
+ * Presentation is owned by this plugin; policy is not. Everything an adopter
25
+ * might reasonably want different is a prop, so nobody has to fork the widget.
26
+ *
27
+ * @public
28
+ */
29
+ type HomePageGithubTrendingTickerProps = {
30
+ /** Trending window to request. Defaults to the backend's configured window. */
31
+ since?: TrendingSince;
32
+ /** Restrict to a programming language, e.g. `typescript`. */
33
+ language?: string;
34
+ /** Restrict to a spoken language code, e.g. `en`. */
35
+ spokenLanguageCode?: string;
36
+ /** Cap the number of repositories shown. Defaults to everything returned. */
37
+ maxItems?: number;
38
+ /**
39
+ * Scroll speed in pixels per second. Industry-comfortable range is ~30-80;
40
+ * above ~100 is unreadable. Defaults to 50 (~6 characters/second here).
41
+ */
42
+ pixelsPerSecond?: number;
43
+ /** How often to re-fetch. Set to 0 to disable polling. */
44
+ refreshIntervalMinutes?: number;
45
+ /**
46
+ * Show the static "spotlight" rail beneath the crawl that features one repo
47
+ * at a time with its trimmed description. Defaults to true.
48
+ */
49
+ showSpotlight?: boolean;
50
+ };
51
+ /**
52
+ * Stock-ticker / news-crawl of GitHub Trending.
53
+ *
54
+ * Design intent (human-factors):
55
+ * - Velocity ("+stars today") is the hero — that is the "what's hot now" signal.
56
+ * - A GitHub language colour dot is glanceable where a word is not.
57
+ * - The one-line description (biggest click driver) lives in the hover tooltip.
58
+ * - Motion is pausable (WCAG 2.2.2), pauses on hover so you can read, and is
59
+ * replaced by a manually-scrollable row under prefers-reduced-motion.
60
+ * Remove the widget from the Home layout to hide it entirely.
61
+ */
62
+ declare const HomePageGithubTrendingTicker: (props?: HomePageGithubTrendingTickerProps) => React.JSX.Element;
63
+
64
+ export { GithubTrendingClient, HomePageGithubTrendingTicker, githubTrendingApiRef, githubTrendingPlugin };
65
+ export type { GithubTrendingApi, HomePageGithubTrendingTickerProps };
@@ -0,0 +1,4 @@
1
+ export { githubTrendingPlugin } from './plugin.esm.js';
2
+ export { GithubTrendingClient, githubTrendingApiRef } from './api.esm.js';
3
+ export { HomePageGithubTrendingTicker } from './components/HomePageGithubTrendingTicker.esm.js';
4
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
@@ -0,0 +1,16 @@
1
+ import { createPlugin, createApiFactory, fetchApiRef, discoveryApiRef } from '@backstage/core-plugin-api';
2
+ import { GithubTrendingClient, githubTrendingApiRef } from './api.esm.js';
3
+
4
+ const githubTrendingPlugin = createPlugin({
5
+ id: "github-trending",
6
+ apis: [
7
+ createApiFactory({
8
+ api: githubTrendingApiRef,
9
+ deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
10
+ factory: ({ discoveryApi, fetchApi }) => new GithubTrendingClient({ discoveryApi, fetchApi })
11
+ })
12
+ ]
13
+ });
14
+
15
+ export { githubTrendingPlugin };
16
+ //# sourceMappingURL=plugin.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.esm.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2026 AAG1999 / contributors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n */\n\nimport {\n createApiFactory,\n createPlugin,\n discoveryApiRef,\n fetchApiRef,\n} from '@backstage/core-plugin-api';\nimport { githubTrendingApiRef, GithubTrendingClient } from './api';\n\nexport const githubTrendingPlugin = createPlugin({\n id: 'github-trending',\n apis: [\n createApiFactory({\n api: githubTrendingApiRef,\n deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },\n factory: ({ discoveryApi, fetchApi }) =>\n new GithubTrendingClient({ discoveryApi, fetchApi }),\n }),\n ],\n});\n"],"names":[],"mappings":";;;AAkBO,MAAM,uBAAuB,YAAA,CAAa;AAAA,EAC/C,EAAA,EAAI,iBAAA;AAAA,EACJ,IAAA,EAAM;AAAA,IACJ,gBAAA,CAAiB;AAAA,MACf,GAAA,EAAK,oBAAA;AAAA,MACL,IAAA,EAAM,EAAE,YAAA,EAAc,eAAA,EAAiB,UAAU,WAAA,EAAY;AAAA,MAC7D,OAAA,EAAS,CAAC,EAAE,YAAA,EAAc,QAAA,EAAS,KACjC,IAAI,oBAAA,CAAqB,EAAE,YAAA,EAAc,QAAA,EAAU;AAAA,KACtD;AAAA;AAEL,CAAC;;"}
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "@aag1999/plugin-github-trending",
3
+ "version": "0.1.0",
4
+ "description": "GitHub Trending home ticker for Backstage",
5
+ "backstage": {
6
+ "role": "frontend-plugin",
7
+ "pluginId": "github-trending",
8
+ "pluginPackages": [
9
+ "@aag1999/plugin-github-trending",
10
+ "@aag1999/plugin-github-trending-backend",
11
+ "@aag1999/plugin-github-trending-common"
12
+ ],
13
+ "features": {
14
+ "./alpha": "@backstage/FrontendPlugin"
15
+ }
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/AAG1999/backstage-plugin-github-trending",
23
+ "directory": "plugins/github-trending"
24
+ },
25
+ "license": "Apache-2.0",
26
+ "sideEffects": false,
27
+ "main": "./dist/index.esm.js",
28
+ "types": "./dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "import": "./dist/index.esm.js",
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.esm.js"
34
+ },
35
+ "./alpha": {
36
+ "backstage": "@backstage/FrontendPlugin",
37
+ "import": "./dist/alpha.esm.js",
38
+ "types": "./dist/alpha.d.ts",
39
+ "default": "./dist/alpha.esm.js"
40
+ },
41
+ "./package.json": "./package.json"
42
+ },
43
+ "typesVersions": {
44
+ "*": {
45
+ "alpha": [
46
+ "dist/alpha.d.ts"
47
+ ],
48
+ "package.json": [
49
+ "package.json"
50
+ ]
51
+ }
52
+ },
53
+ "files": [
54
+ "dist"
55
+ ],
56
+ "scripts": {
57
+ "start": "backstage-cli package start",
58
+ "build": "backstage-cli package build",
59
+ "lint": "backstage-cli package lint",
60
+ "test": "backstage-cli package test",
61
+ "clean": "backstage-cli package clean",
62
+ "prepack": "backstage-cli package prepack",
63
+ "postpack": "backstage-cli package postpack"
64
+ },
65
+ "dependencies": {
66
+ "@aag1999/plugin-github-trending-common": "workspace:^",
67
+ "@backstage/core-components": "^0.18.13",
68
+ "@backstage/core-plugin-api": "^1.12.9",
69
+ "@backstage/frontend-plugin-api": "^0.18.0",
70
+ "@backstage/plugin-home-react": "^0.1.41",
71
+ "@material-ui/core": "^4.12.2",
72
+ "@material-ui/icons": "^4.11.3",
73
+ "zod": "^4.0.0"
74
+ },
75
+ "peerDependencies": {
76
+ "react": "^17.0.0 || ^18.0.0",
77
+ "react-dom": "^17.0.0 || ^18.0.0",
78
+ "react-router-dom": "^6.3.0"
79
+ },
80
+ "devDependencies": {
81
+ "@backstage/cli": "^0.36.5",
82
+ "@backstage/dev-utils": "^1.1.0",
83
+ "@backstage/test-utils": "^1.7.21",
84
+ "@testing-library/dom": "^10.0.0",
85
+ "@testing-library/jest-dom": "^6.0.0",
86
+ "@testing-library/react": "^16.0.0",
87
+ "@types/react": "^18.0.0",
88
+ "jest-environment-jsdom": "^30.0.0",
89
+ "react": "^18.0.2",
90
+ "react-dom": "^18.0.2",
91
+ "react-router-dom": "^6.30.2"
92
+ },
93
+ "module": "./dist/index.esm.js"
94
+ }