x12i-downloads 0.1.1

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.
Files changed (73) hide show
  1. package/.cursor/rules/graphify.mdc +21 -0
  2. package/.openai/hosting.json +1 -0
  3. package/.vinext/fonts/geist-8ac0455e797f/geist-001175b1.woff2 +0 -0
  4. package/.vinext/fonts/geist-8ac0455e797f/geist-52306abf.woff2 +0 -0
  5. package/.vinext/fonts/geist-8ac0455e797f/geist-875ccdd4.woff2 +0 -0
  6. package/.vinext/fonts/geist-8ac0455e797f/geist-98bbbccb.woff2 +0 -0
  7. package/.vinext/fonts/geist-8ac0455e797f/geist-ff2310f5.woff2 +0 -0
  8. package/.vinext/fonts/geist-8ac0455e797f/style.css +45 -0
  9. package/.vinext/fonts/geist-mono-00e989178794/geist-mono-013b2f2f.woff2 +0 -0
  10. package/.vinext/fonts/geist-mono-00e989178794/geist-mono-0638449e.woff2 +0 -0
  11. package/.vinext/fonts/geist-mono-00e989178794/geist-mono-44745446.woff2 +0 -0
  12. package/.vinext/fonts/geist-mono-00e989178794/geist-mono-44e03052.woff2 +0 -0
  13. package/.vinext/fonts/geist-mono-00e989178794/geist-mono-971fb274.woff2 +0 -0
  14. package/.vinext/fonts/geist-mono-00e989178794/geist-mono-f6b33328.woff2 +0 -0
  15. package/.vinext/fonts/geist-mono-00e989178794/style.css +54 -0
  16. package/README.md +54 -0
  17. package/app/api/cron/update-stats/route.ts +23 -0
  18. package/app/api/stats/route.ts +14 -0
  19. package/app/chatgpt-auth.ts +86 -0
  20. package/app/globals.css +301 -0
  21. package/app/layout.tsx +44 -0
  22. package/app/page.tsx +61 -0
  23. package/build/sites-vite-plugin.ts +45 -0
  24. package/components/animated-counter.tsx +70 -0
  25. package/components/cumulative-chart.tsx +82 -0
  26. package/components/downloads-dashboard.tsx +90 -0
  27. package/db/index.ts +13 -0
  28. package/db/schema.ts +4 -0
  29. package/drizzle/meta/_journal.json +5 -0
  30. package/drizzle.config.ts +7 -0
  31. package/eslint.config.mjs +18 -0
  32. package/examples/d1/app/api/notes/route.ts +58 -0
  33. package/examples/d1/db/schema.ts +9 -0
  34. package/graphify-out/.graphify_labels.json +25 -0
  35. package/graphify-out/.graphify_labels.json.sig +1 -0
  36. package/graphify-out/.graphify_root +1 -0
  37. package/graphify-out/GRAPH_REPORT.md +145 -0
  38. package/graphify-out/cache/ast/v0.9.33/0569aed6b68b30e30dcbaf649a48ee821f274b5b12aa100f2c226d15244b3a9b.json +1 -0
  39. package/graphify-out/cache/ast/v0.9.33/159ff1acf176e1a9d83c92e1132a5a9812fcbc84768f811b48ba88a5a46259a7.json +1 -0
  40. package/graphify-out/cache/ast/v0.9.33/2723bc60c1710800853b62fed8b65a07463fa4805580dc215cc84be4ead3f018.json +1 -0
  41. package/graphify-out/cache/ast/v0.9.33/7ce918b99db50a4df6ce9af15ad0386b77f2856b743e7efa1462dded67d5ed03.json +1 -0
  42. package/graphify-out/cache/ast/v0.9.33/7fddf7ae1399ea8c8f341e27419bbc339cf0f817f3422b5c6e3bc0701286abbe.json +1 -0
  43. package/graphify-out/cache/ast/v0.9.33/d77de58cd43e8cec3904e803fc046f584784a062f1520ec24e05212603265829.json +1 -0
  44. package/graphify-out/cache/ast/v0.9.33/d90342d6a3152d909250981198325960406a3887839590300e7f0de09f793116.json +1 -0
  45. package/graphify-out/cache/stat-index.json +1 -0
  46. package/graphify-out/graph.html +320 -0
  47. package/graphify-out/graph.json +6777 -0
  48. package/graphify-out/manifest.json +192 -0
  49. package/lib/config.ts +24 -0
  50. package/lib/npm-stats-calc.mjs +50 -0
  51. package/lib/npm-stats-live.ts +547 -0
  52. package/lib/npm-stats-shared.ts +51 -0
  53. package/next.config.ts +7 -0
  54. package/package.json +44 -0
  55. package/postcss.config.mjs +7 -0
  56. package/public/favicon.svg +6 -0
  57. package/public/file.svg +1 -0
  58. package/public/globe.svg +1 -0
  59. package/public/window.svg +1 -0
  60. package/public/x12i-stats.json +47 -0
  61. package/scripts/build-verified.sh +27 -0
  62. package/scripts/install-ci.sh +189 -0
  63. package/scripts/sites-env.sh +52 -0
  64. package/scripts/update-stats.mjs +290 -0
  65. package/scripts/validate-artifact.sh +37 -0
  66. package/test-npm.mjs +13 -0
  67. package/tests/npm-stats-calc.test.mjs +51 -0
  68. package/tests/rendered-html.test.mjs +33 -0
  69. package/tsconfig.json +34 -0
  70. package/tsconfig.tsbuildinfo +1 -0
  71. package/vite.config.ts +76 -0
  72. package/worker/index.ts +243 -0
  73. package/wrangler.jsonc +34 -0
@@ -0,0 +1,21 @@
1
+ ---
2
+ description: graphify knowledge graph context
3
+ alwaysApply: true
4
+ ---
5
+
6
+ This project has a graphify knowledge graph at graphify-out/.
7
+
8
+ **MANDATORY: Before using Read, Grep, Glob, or Bash to explore the codebase, you MUST run graphify first:**
9
+ - `graphify query "<question>"` — scoped subgraph for any codebase or architecture question
10
+ - `graphify path "<A>" "<B>"` — dependency path between two symbols
11
+ - `graphify explain "<concept>"` — all nodes related to a concept
12
+
13
+ This applies to YOU and to every subagent you spawn. Include this rule explicitly in every subagent prompt that involves code exploration. Do not skip graphify because files are "already known" or because you are executing a plan — the graph surfaces cross-file dependencies and INFERRED edges that grep and Read cannot find.
14
+
15
+ Only use Read/Grep/Glob directly when:
16
+ 1. graphify has already oriented you and you need to modify or debug specific lines
17
+ 2. `graphify-out/graph.json` does not exist yet
18
+
19
+ - If `graphify-out/wiki/index.md` exists, navigate it instead of reading raw files
20
+ - Read `graphify-out/GRAPH_REPORT.md` only for broad architecture review when query/path/explain do not surface enough context
21
+ - After modifying code files, run `graphify update .` to keep the graph current (AST-only, no API cost)
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1,45 @@
1
+ /* cyrillic-ext */
2
+ @font-face {
3
+ font-family: 'Geist';
4
+ font-style: normal;
5
+ font-weight: 100 900;
6
+ font-display: swap;
7
+ src: url(/Users/ami/Documents/prometheus/x12i/x12i-stat/.vinext/fonts/geist-8ac0455e797f/geist-ff2310f5.woff2) format('woff2');
8
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
9
+ }
10
+ /* cyrillic */
11
+ @font-face {
12
+ font-family: 'Geist';
13
+ font-style: normal;
14
+ font-weight: 100 900;
15
+ font-display: swap;
16
+ src: url(/Users/ami/Documents/prometheus/x12i/x12i-stat/.vinext/fonts/geist-8ac0455e797f/geist-875ccdd4.woff2) format('woff2');
17
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
18
+ }
19
+ /* vietnamese */
20
+ @font-face {
21
+ font-family: 'Geist';
22
+ font-style: normal;
23
+ font-weight: 100 900;
24
+ font-display: swap;
25
+ src: url(/Users/ami/Documents/prometheus/x12i/x12i-stat/.vinext/fonts/geist-8ac0455e797f/geist-52306abf.woff2) format('woff2');
26
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
27
+ }
28
+ /* latin-ext */
29
+ @font-face {
30
+ font-family: 'Geist';
31
+ font-style: normal;
32
+ font-weight: 100 900;
33
+ font-display: swap;
34
+ src: url(/Users/ami/Documents/prometheus/x12i/x12i-stat/.vinext/fonts/geist-8ac0455e797f/geist-001175b1.woff2) format('woff2');
35
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
36
+ }
37
+ /* latin */
38
+ @font-face {
39
+ font-family: 'Geist';
40
+ font-style: normal;
41
+ font-weight: 100 900;
42
+ font-display: swap;
43
+ src: url(/Users/ami/Documents/prometheus/x12i/x12i-stat/.vinext/fonts/geist-8ac0455e797f/geist-98bbbccb.woff2) format('woff2');
44
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
45
+ }
@@ -0,0 +1,54 @@
1
+ /* cyrillic-ext */
2
+ @font-face {
3
+ font-family: 'Geist Mono';
4
+ font-style: normal;
5
+ font-weight: 100 900;
6
+ font-display: swap;
7
+ src: url(/Users/ami/Documents/prometheus/x12i/x12i-stat/.vinext/fonts/geist-mono-00e989178794/geist-mono-f6b33328.woff2) format('woff2');
8
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
9
+ }
10
+ /* cyrillic */
11
+ @font-face {
12
+ font-family: 'Geist Mono';
13
+ font-style: normal;
14
+ font-weight: 100 900;
15
+ font-display: swap;
16
+ src: url(/Users/ami/Documents/prometheus/x12i/x12i-stat/.vinext/fonts/geist-mono-00e989178794/geist-mono-44e03052.woff2) format('woff2');
17
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
18
+ }
19
+ /* symbols2 */
20
+ @font-face {
21
+ font-family: 'Geist Mono';
22
+ font-style: normal;
23
+ font-weight: 100 900;
24
+ font-display: swap;
25
+ src: url(/Users/ami/Documents/prometheus/x12i/x12i-stat/.vinext/fonts/geist-mono-00e989178794/geist-mono-0638449e.woff2) format('woff2');
26
+ unicode-range: U+2000-2001, U+2004-2008, U+200A, U+23B8-23BD, U+2500-259F;
27
+ }
28
+ /* vietnamese */
29
+ @font-face {
30
+ font-family: 'Geist Mono';
31
+ font-style: normal;
32
+ font-weight: 100 900;
33
+ font-display: swap;
34
+ src: url(/Users/ami/Documents/prometheus/x12i/x12i-stat/.vinext/fonts/geist-mono-00e989178794/geist-mono-971fb274.woff2) format('woff2');
35
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
36
+ }
37
+ /* latin-ext */
38
+ @font-face {
39
+ font-family: 'Geist Mono';
40
+ font-style: normal;
41
+ font-weight: 100 900;
42
+ font-display: swap;
43
+ src: url(/Users/ami/Documents/prometheus/x12i/x12i-stat/.vinext/fonts/geist-mono-00e989178794/geist-mono-44745446.woff2) format('woff2');
44
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
45
+ }
46
+ /* latin */
47
+ @font-face {
48
+ font-family: 'Geist Mono';
49
+ font-style: normal;
50
+ font-weight: 100 900;
51
+ font-display: swap;
52
+ src: url(/Users/ami/Documents/prometheus/x12i/x12i-stat/.vinext/fonts/geist-mono-00e989178794/geist-mono-013b2f2f.woff2) format('woff2');
53
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
54
+ }
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @x12i Total Downloads
2
+
3
+ A React dashboard that discovers every public package in the `@x12i` npm scope and calculates cumulative downloads since April 1, 2026.
4
+
5
+ ## What it does
6
+
7
+ - Discovers public `@x12i/*` packages anonymously through npm's public registry.
8
+ - Filters npm's fuzzy search results to exact `@x12i/` package names.
9
+ - Removes duplicates before calculating downloads.
10
+ - Serves a generated verified snapshot immediately, so visitors make no npm API calls.
11
+ - Includes a controlled update command with retry/backoff to avoid anonymous npm rate-limit bursts.
12
+ - Aggregates exact daily package ranges into cumulative 14-day chart points.
13
+ - Verifies the chart total against the independently accumulated package total.
14
+ - Replaces the published snapshot only after all package ranges load and the integrity check passes.
15
+ - Omits all horizontal-axis labels as requested.
16
+
17
+ ## Run locally
18
+
19
+ Requirements: Node.js 22.13 or newer.
20
+
21
+ ```bash
22
+ npm install
23
+ npm run dev
24
+ ```
25
+
26
+ Open the local URL printed by the development server.
27
+
28
+ ## Production build
29
+
30
+ ```bash
31
+ npm run build
32
+ ```
33
+
34
+ ## Refresh the statistics
35
+
36
+ Run this before publishing a new snapshot or from a scheduled workflow:
37
+
38
+ ```bash
39
+ npm run update:stats
40
+ ```
41
+
42
+ The updater discovers all current public packages, downloads every daily range, verifies the aggregate, and atomically replaces `public/x12i-stats.json` only after success. Daily package-range work is resumable through an ignored local cache, so an interrupted or throttled run continues instead of starting over.
43
+
44
+ ## Main source files
45
+
46
+ - `app/page.tsx` — page entry point
47
+ - `components/downloads-dashboard.tsx` — verified snapshot display and empty-state UI
48
+ - `components/cumulative-chart.tsx` — responsive accessible SVG chart
49
+ - `public/x12i-stats.json` — generated verified snapshot rendered by React
50
+ - `scripts/update-stats.mjs` — anonymous npm discovery and snapshot calculation
51
+ - `lib/npm-stats-shared.ts` — shared result types and runtime validation
52
+ - `app/globals.css` — responsive visual styling
53
+
54
+ No npm login, token, database or environment variables are required.
@@ -0,0 +1,23 @@
1
+ import { NextResponse } from "next/server";
2
+ import { fetchLiveStats } from "@/lib/npm-stats-live";
3
+
4
+ /**
5
+ * Local / non-Worker fallback. In production the Worker handles this path
6
+ * and persists results to STATS_KV asynchronously (see worker/index.ts).
7
+ */
8
+ export async function GET() {
9
+ try {
10
+ const stats = await fetchLiveStats();
11
+ return NextResponse.json(
12
+ { success: true, accepted: false, stats },
13
+ { headers: { "Cache-Control": "no-store" } },
14
+ );
15
+ } catch (err: unknown) {
16
+ const message = err instanceof Error ? err.message : String(err);
17
+ return NextResponse.json({ success: false, error: message }, { status: 500 });
18
+ }
19
+ }
20
+
21
+ export async function POST() {
22
+ return GET();
23
+ }
@@ -0,0 +1,14 @@
1
+ import { NextResponse } from "next/server";
2
+ import statsJson from "@/public/x12i-stats.json";
3
+ import { isNpmStatsResult } from "@/lib/npm-stats-shared";
4
+
5
+ export async function GET() {
6
+ if (isNpmStatsResult(statsJson)) {
7
+ return NextResponse.json(statsJson, {
8
+ headers: {
9
+ "Cache-Control": "public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800",
10
+ },
11
+ });
12
+ }
13
+ return NextResponse.json({ error: "Invalid stats" }, { status: 500 });
14
+ }
@@ -0,0 +1,86 @@
1
+ import { headers } from "next/headers";
2
+ import { redirect } from "next/navigation";
3
+
4
+ export type ChatGPTUser = {
5
+ displayName: string;
6
+ email: string;
7
+ fullName: string | null;
8
+ };
9
+
10
+ const USER_EMAIL_HEADER = "oai-authenticated-user-email";
11
+ const USER_FULL_NAME_HEADER = "oai-authenticated-user-full-name";
12
+ const USER_FULL_NAME_ENCODING_HEADER =
13
+ "oai-authenticated-user-full-name-encoding";
14
+ const PERCENT_ENCODED_UTF8 = "percent-encoded-utf-8";
15
+ const SIGN_IN_PATH = "/signin-with-chatgpt";
16
+ const SIGN_OUT_PATH = "/signout-with-chatgpt";
17
+ const CALLBACK_PATH = "/callback";
18
+
19
+ export async function getChatGPTUser(): Promise<ChatGPTUser | null> {
20
+ const requestHeaders = await headers();
21
+ const email = requestHeaders.get(USER_EMAIL_HEADER);
22
+ if (!email) return null;
23
+
24
+ const encodedFullName = requestHeaders.get(USER_FULL_NAME_HEADER);
25
+ const fullName =
26
+ encodedFullName &&
27
+ requestHeaders.get(USER_FULL_NAME_ENCODING_HEADER) === PERCENT_ENCODED_UTF8
28
+ ? safeDecodeURIComponent(encodedFullName)
29
+ : null;
30
+
31
+ return {
32
+ displayName: fullName ?? email,
33
+ email,
34
+ fullName,
35
+ };
36
+ }
37
+
38
+ export async function requireChatGPTUser(
39
+ returnTo: string,
40
+ ): Promise<ChatGPTUser> {
41
+ const user = await getChatGPTUser();
42
+ if (user) return user;
43
+
44
+ redirect(chatGPTSignInPath(returnTo));
45
+ }
46
+
47
+ export function chatGPTSignInPath(returnTo: string): string {
48
+ const safeReturnTo = safeRelativeReturnPath(returnTo);
49
+ return `${SIGN_IN_PATH}?return_to=${encodeURIComponent(safeReturnTo)}`;
50
+ }
51
+
52
+ export function chatGPTSignOutPath(returnTo = "/"): string {
53
+ const safeReturnTo = safeRelativeReturnPath(returnTo);
54
+ return `${SIGN_OUT_PATH}?return_to=${encodeURIComponent(safeReturnTo)}`;
55
+ }
56
+
57
+ function safeRelativeReturnPath(value: string): string {
58
+ if (!value.startsWith("/") || value.startsWith("//")) return "/";
59
+
60
+ let url: URL;
61
+ try {
62
+ url = new URL(value, "https://app.local");
63
+ } catch {
64
+ return "/";
65
+ }
66
+ if (url.origin !== "https://app.local") return "/";
67
+ if (isReservedAuthPath(url.pathname)) return "/";
68
+
69
+ return `${url.pathname}${url.search}${url.hash}`;
70
+ }
71
+
72
+ function isReservedAuthPath(pathname: string): boolean {
73
+ return (
74
+ pathname === SIGN_IN_PATH ||
75
+ pathname === SIGN_OUT_PATH ||
76
+ pathname === CALLBACK_PATH
77
+ );
78
+ }
79
+
80
+ function safeDecodeURIComponent(value: string): string | null {
81
+ try {
82
+ return decodeURIComponent(value);
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
@@ -0,0 +1,301 @@
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: #0f172a;
5
+ --surface: #111c30;
6
+ --surface-border: #253149;
7
+ --foreground: #f8fafc;
8
+ --muted: #94a3b8;
9
+ --muted-dark: #64748b;
10
+ --accent: #38bdf8;
11
+ --accent-soft: rgba(56, 189, 248, 0.12);
12
+ --grid: #334155;
13
+ --danger: #f87171;
14
+ --anim-duration: 2s;
15
+ }
16
+
17
+ * {
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ html {
22
+ min-width: 320px;
23
+ background: var(--background);
24
+ }
25
+
26
+ body {
27
+ min-height: 100vh;
28
+ margin: 0;
29
+ background:
30
+ radial-gradient(circle at 50% 0%, rgba(56, 189, 248, 0.08), transparent 32rem),
31
+ var(--background);
32
+ color: var(--foreground);
33
+ font-family: var(--font-geist-sans), Inter, ui-sans-serif, system-ui, sans-serif;
34
+ }
35
+
36
+ button,
37
+ input,
38
+ textarea,
39
+ select {
40
+ font: inherit;
41
+ }
42
+
43
+ .dashboard-shell {
44
+ display: grid;
45
+ min-height: 100vh;
46
+ place-items: center;
47
+ padding: 2rem;
48
+ }
49
+
50
+ .dashboard-card {
51
+ width: min(100%, 44rem);
52
+ overflow: hidden;
53
+ border: 1px solid var(--surface-border);
54
+ border-radius: 1.75rem;
55
+ background: rgba(15, 23, 42, 0.82);
56
+ box-shadow: 0 32px 90px rgba(2, 6, 23, 0.48);
57
+ backdrop-filter: blur(16px);
58
+ }
59
+
60
+ .dashboard-content {
61
+ padding: 2.25rem 2.5rem 2.5rem;
62
+ text-align: center;
63
+ }
64
+
65
+ .eyebrow {
66
+ margin: 0.5rem 0 0;
67
+ color: var(--muted);
68
+ font-size: 0.72rem;
69
+ font-weight: 700;
70
+ letter-spacing: 0.18em;
71
+ text-transform: uppercase;
72
+ }
73
+
74
+ .title {
75
+ margin: 0;
76
+ font-size: clamp(2rem, 7vw, 2.75rem);
77
+ font-weight: 850;
78
+ letter-spacing: -0.045em;
79
+ }
80
+
81
+ .metric-block {
82
+ margin: 2.5rem 0 2rem;
83
+ }
84
+
85
+ .total {
86
+ min-height: 1.05em;
87
+ color: var(--accent);
88
+ font-size: clamp(3.5rem, 12vw, 5.25rem);
89
+ font-variant-numeric: tabular-nums;
90
+ font-weight: 900;
91
+ letter-spacing: -0.065em;
92
+ line-height: 1;
93
+ }
94
+
95
+ .total-pending {
96
+ color: var(--muted);
97
+ font-size: clamp(2.25rem, 8vw, 3.5rem);
98
+ letter-spacing: -0.045em;
99
+ }
100
+
101
+ .metric-description {
102
+ margin: 0.65rem 0 0;
103
+ color: var(--muted-dark);
104
+ font-size: 0.875rem;
105
+ }
106
+
107
+ .status-row {
108
+ display: flex;
109
+ min-height: 1.5rem;
110
+ align-items: center;
111
+ justify-content: center;
112
+ gap: 0.5rem;
113
+ margin-top: 0.4rem;
114
+ color: var(--muted-dark);
115
+ font-size: 0.75rem;
116
+ }
117
+
118
+ .status-row.error {
119
+ color: var(--danger);
120
+ }
121
+
122
+ .spinner {
123
+ width: 0.75rem;
124
+ height: 0.75rem;
125
+ flex: none;
126
+ border: 2px solid rgba(56, 189, 248, 0.2);
127
+ border-top-color: var(--accent);
128
+ border-radius: 999px;
129
+ animation: spin 0.85s linear infinite;
130
+ }
131
+
132
+ .retry-button {
133
+ margin-left: 0.25rem;
134
+ border: 0;
135
+ border-bottom: 1px solid currentColor;
136
+ padding: 0;
137
+ background: transparent;
138
+ color: inherit;
139
+ cursor: pointer;
140
+ font-weight: 700;
141
+ }
142
+
143
+ .retry-button:focus-visible {
144
+ outline: 2px solid var(--accent);
145
+ outline-offset: 4px;
146
+ }
147
+
148
+ /* ── Chart ─────────────────────────────────────────────── */
149
+
150
+ .chart-frame {
151
+ position: relative;
152
+ width: 100%;
153
+ height: 16rem;
154
+ margin-top: 1rem;
155
+ }
156
+
157
+ .chart-frame svg {
158
+ display: block;
159
+ width: 100%;
160
+ height: 100%;
161
+ overflow: visible;
162
+ }
163
+
164
+ .chart-empty {
165
+ display: grid;
166
+ height: 100%;
167
+ place-items: center;
168
+ color: var(--muted-dark);
169
+ font-size: 0.8rem;
170
+ }
171
+
172
+ .chart-grid {
173
+ stroke: var(--grid);
174
+ stroke-width: 1;
175
+ vector-effect: non-scaling-stroke;
176
+ }
177
+
178
+ .chart-y-label {
179
+ fill: var(--muted);
180
+ font-size: 12px;
181
+ font-variant-numeric: tabular-nums;
182
+ }
183
+
184
+ .chart-line {
185
+ fill: none;
186
+ stroke: var(--accent);
187
+ stroke-linecap: round;
188
+ stroke-linejoin: round;
189
+ stroke-width: 3;
190
+ vector-effect: non-scaling-stroke;
191
+ /* Draw animation — uses a very large dasharray; the offset animates to 0 */
192
+ stroke-dasharray: 2000;
193
+ stroke-dashoffset: 2000;
194
+ animation: draw-line var(--anim-duration) ease-out forwards;
195
+ }
196
+
197
+ .chart-area {
198
+ fill: url(#area-gradient);
199
+ opacity: 0;
200
+ animation: fade-area var(--anim-duration) ease-out forwards;
201
+ }
202
+
203
+ /* ── npm link ─────────────────────────────────────────── */
204
+
205
+ .npm-link {
206
+ display: inline-flex;
207
+ align-items: center;
208
+ gap: 0.4rem;
209
+ margin-top: 1.5rem;
210
+ padding: 0.55rem 1.15rem;
211
+ border: 1px solid var(--surface-border);
212
+ border-radius: 0.65rem;
213
+ background: var(--accent-soft);
214
+ color: var(--accent);
215
+ font-size: 0.82rem;
216
+ font-weight: 600;
217
+ text-decoration: none;
218
+ transition: background 0.2s, border-color 0.2s, transform 0.15s;
219
+ }
220
+
221
+ .npm-link:hover {
222
+ border-color: var(--accent);
223
+ background: rgba(56, 189, 248, 0.18);
224
+ transform: translateY(-1px);
225
+ }
226
+
227
+ .npm-link:active {
228
+ transform: translateY(0);
229
+ }
230
+
231
+ .npm-link:focus-visible {
232
+ outline: 2px solid var(--accent);
233
+ outline-offset: 3px;
234
+ }
235
+
236
+ .npm-link-icon {
237
+ width: 0.95rem;
238
+ height: 0.95rem;
239
+ flex: none;
240
+ }
241
+
242
+ /* ── Keyframes ────────────────────────────────────────── */
243
+
244
+ @keyframes spin {
245
+ to { transform: rotate(360deg); }
246
+ }
247
+
248
+ @keyframes draw-line {
249
+ to { stroke-dashoffset: 0; }
250
+ }
251
+
252
+ @keyframes fade-area {
253
+ 0% { opacity: 0; }
254
+ 40% { opacity: 0; }
255
+ 100% { opacity: 1; }
256
+ }
257
+
258
+ /* ── Responsive ───────────────────────────────────────── */
259
+
260
+ @media (max-width: 640px) {
261
+ .dashboard-shell {
262
+ align-items: stretch;
263
+ padding: 0;
264
+ }
265
+
266
+ .dashboard-card {
267
+ min-height: 100vh;
268
+ border: 0;
269
+ border-radius: 0;
270
+ }
271
+
272
+ .dashboard-content {
273
+ padding: 3rem 1.25rem 2rem;
274
+ }
275
+
276
+ .metric-block {
277
+ margin-top: 3rem;
278
+ }
279
+
280
+ .chart-frame {
281
+ height: 14rem;
282
+ margin-top: 2rem;
283
+ }
284
+ }
285
+
286
+ @media (prefers-reduced-motion: reduce) {
287
+ .spinner {
288
+ animation-duration: 2s;
289
+ }
290
+
291
+ .chart-line {
292
+ stroke-dasharray: none;
293
+ stroke-dashoffset: 0;
294
+ animation: none;
295
+ }
296
+
297
+ .chart-area {
298
+ opacity: 1;
299
+ animation: none;
300
+ }
301
+ }
package/app/layout.tsx ADDED
@@ -0,0 +1,44 @@
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import { BRAND_NAME, PACKAGE_SCOPE } from "@/lib/config";
4
+ import "./globals.css";
5
+
6
+ const geistSans = Geist({
7
+ variable: "--font-geist-sans",
8
+ subsets: ["latin"],
9
+ });
10
+
11
+ const geistMono = Geist_Mono({
12
+ variable: "--font-geist-mono",
13
+ subsets: ["latin"],
14
+ });
15
+
16
+ const scopeDisplay = PACKAGE_SCOPE.replace(/\/$/, "");
17
+
18
+ export const metadata: Metadata = {
19
+ title: `${scopeDisplay} Total Downloads`,
20
+ description: `Live cumulative downloads across every public ${scopeDisplay} npm package.`,
21
+ other: {
22
+ "codex-preview": "development",
23
+ },
24
+ icons: {
25
+ icon: "/favicon.svg",
26
+ shortcut: "/favicon.svg",
27
+ },
28
+ };
29
+
30
+ export default function RootLayout({
31
+ children,
32
+ }: Readonly<{
33
+ children: React.ReactNode;
34
+ }>) {
35
+ return (
36
+ <html lang="en">
37
+ <body
38
+ className={`${geistSans.variable} ${geistMono.variable} antialiased`}
39
+ >
40
+ {children}
41
+ </body>
42
+ </html>
43
+ );
44
+ }