@shiinasaku/github-card 2.0.0 → 4.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.
package/src/types.ts CHANGED
@@ -37,4 +37,5 @@ export interface CardOptions {
37
37
  hide_border?: boolean;
38
38
  compact?: boolean;
39
39
  hide?: string[];
40
+ animate?: boolean;
40
41
  }
@@ -0,0 +1,85 @@
1
+ export const twColors: Record<string, string> = {
2
+ transparent: "transparent",
3
+ current: "currentColor",
4
+ white: "#ffffff",
5
+ black: "#000000",
6
+ "slate-50": "#f8fafc",
7
+ "slate-100": "#f1f5f9",
8
+ "slate-200": "#e2e8f0",
9
+ "slate-300": "#cbd5e1",
10
+ "slate-400": "#94a3b8",
11
+ "slate-500": "#64748b",
12
+ "slate-600": "#475569",
13
+ "slate-700": "#334155",
14
+ "slate-800": "#1e293b",
15
+ "slate-900": "#0f172a",
16
+ "slate-950": "#020617",
17
+ "neutral-800": "#262626",
18
+ "neutral-950": "#0a0a0a",
19
+ "zinc-100": "#f4f4f5",
20
+ "zinc-400": "#a1a1aa",
21
+ "zinc-800": "#27272a",
22
+ "zinc-900": "#18181b",
23
+ "stone-300": "#d6d3d1",
24
+ "stone-700": "#44403c",
25
+ "stone-800": "#292524",
26
+ "rose-200": "#fecdd3",
27
+ "rose-300": "#fda4af",
28
+ "rose-400": "#fb7185",
29
+ "rose-900": "#881337",
30
+ "rose-950": "#4c0519",
31
+ "pink-400": "#f472b6",
32
+ "pink-500": "#ec4899",
33
+ "fuchsia-400": "#e879f9",
34
+ "purple-400": "#c084fc",
35
+ "purple-900": "#581c87",
36
+ "purple-950": "#3b0764",
37
+ "indigo-300": "#a5b4fc",
38
+ "indigo-400": "#818cf8",
39
+ "indigo-500": "#6366f1",
40
+ "indigo-900": "#312e81",
41
+ "violet-300": "#c4b5fd",
42
+ "violet-400": "#a78bfa",
43
+ "blue-300": "#93c5fd",
44
+ "blue-400": "#60a5fa",
45
+ "blue-500": "#3b82f6",
46
+ "blue-600": "#2563eb",
47
+ "sky-400": "#38bdf8",
48
+ "sky-900": "#0c4a6e",
49
+ "sky-950": "#082f49",
50
+ "cyan-300": "#67e8f9",
51
+ "cyan-400": "#22d3ee",
52
+ "teal-400": "#2dd4bf",
53
+ "emerald-200": "#a7f3d0",
54
+ "emerald-300": "#6ee7b7",
55
+ "emerald-400": "#34d399",
56
+ "emerald-500": "#10b981",
57
+ "emerald-900": "#064e3b",
58
+ "emerald-950": "#022c22",
59
+ "green-400": "#4ade80",
60
+ "lime-300": "#bef264",
61
+ "lime-400": "#a3e635",
62
+ "yellow-300": "#fde047",
63
+ "yellow-400": "#facc15",
64
+ "amber-300": "#fcd34d",
65
+ "amber-400": "#fbbf24",
66
+ "amber-600": "#d97706",
67
+ "amber-700": "#b45309",
68
+ "amber-900": "#78350f",
69
+ "orange-50": "#fff7ed",
70
+ "orange-200": "#fed7aa",
71
+ "orange-400": "#fb923c",
72
+ "orange-500": "#f97316",
73
+ };
74
+
75
+ export function resolveTw(token: string, property: "fill" | "stroke"): string {
76
+ const value = token.trim();
77
+ // Raw hex, with or without "#": only valid SVG lengths (3/4/6/8)
78
+ const hex = value.startsWith("#") ? value.slice(1) : value;
79
+ if (/^(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(hex)) {
80
+ return `${property}="#${hex}"`;
81
+ }
82
+ // Tailwind-style token: fill-rose-400 / stroke-slate-200 / rose-400
83
+ const colorName = value.replace(/^fill-/, "").replace(/^stroke-/, "");
84
+ return `${property}="${twColors[colorName] ?? value}"`;
85
+ }
package/src/utils/font.ts CHANGED
@@ -1,19 +1,3 @@
1
- /**
2
- * Roboto variable font embedded as base64 WOFF2 for SVG @font-face.
3
- *
4
- * The subset (~47KB / ~63KB base64) covers:
5
- * - ASCII printable (U+0020-007E)
6
- * - Latin-1 Supplement (U+00A0-00FF)
7
- * - Common typographic chars (dashes, quotes, ellipsis, bullets)
8
- * - Variable weight axis (400-700)
9
- *
10
- * Loaded once at startup via Bun.file — zero per-request cost.
11
- */
12
-
13
- const fontPath = new URL("../../fonts/Roboto-subset.woff2", import.meta.url).pathname;
14
- const fontBytes = await Bun.file(fontPath).arrayBuffer();
15
- const fontBase64 = Buffer.from(fontBytes).toString("base64");
16
-
17
- export const FONT_FAMILY = "'Roboto', sans-serif";
18
-
19
- export const FONT_FACE = `@font-face{font-family:'Roboto';font-style:normal;font-weight:100 900;font-display:swap;src:url(data:font/woff2;base64,${fontBase64}) format('woff2')}`;
1
+ export const FONT_FAMILY =
2
+ 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
3
+ export const FONT_FACE = ""; // Kept for API backwards compatibility
@@ -1,7 +1,8 @@
1
1
  export function kFormat(num: number): string {
2
- if (num >= 1000000) return (num / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
3
- if (num >= 1000) return (num / 1000).toFixed(1).replace(/\.0$/, "") + "k";
4
- return String(num);
2
+ const trim = (v: number) => v.toFixed(1).replace(/\.0$/, "");
3
+ if (num >= 999_950) return trim(num / 1_000_000) + "M";
4
+ if (num >= 999.95) return trim(num / 1_000) + "k";
5
+ return String(Math.round(num));
5
6
  }
6
7
 
7
8
  export function escapeXml(str: string): string {
@@ -17,26 +18,25 @@ export function wrapText(input: string, maxLen: number, maxLines: number): strin
17
18
  const words = input.split(/\s+/).filter(Boolean);
18
19
  const lines: string[] = [];
19
20
  let current = "";
20
- for (const w of words) {
21
- const next = current ? `${current} ${w}` : w;
21
+
22
+ for (const word of words) {
23
+ const next = current ? `${current} ${word}` : word;
22
24
  if (next.length <= maxLen) {
23
25
  current = next;
24
26
  continue;
25
27
  }
26
28
  if (current) lines.push(current);
27
- current = w;
28
- if (lines.length >= maxLines - 1) break;
29
+ if (lines.length === maxLines) break;
30
+ current = word.length > maxLen ? word.slice(0, maxLen) : word;
29
31
  }
30
32
  if (current && lines.length < maxLines) lines.push(current);
31
- if (lines.length === 0 && input) lines.push(input.slice(0, maxLen));
32
- if (lines.length === maxLines && words.length > 0) {
33
- const last = lines[lines.length - 1];
34
- if (!last) return lines;
35
- if (last.length > maxLen) {
36
- lines[lines.length - 1] = last.slice(0, Math.max(0, maxLen - 1)) + "…";
37
- } else if (words.join(" ").length > lines.join(" ").length) {
38
- lines[lines.length - 1] = last.slice(0, Math.max(0, maxLen - 1)) + "…";
39
- }
33
+
34
+ // Truncated? Mark the last line with an ellipsis.
35
+ const consumed = lines.join(" ").length;
36
+ if (lines.length > 0 && consumed < input.trim().length) {
37
+ const last = lines[lines.length - 1]!;
38
+ lines[lines.length - 1] =
39
+ (last.length >= maxLen ? last.slice(0, Math.max(0, maxLen - 1)) : last) + "…";
40
40
  }
41
41
  return lines;
42
42
  }
@@ -0,0 +1,193 @@
1
+ export class GitHubError extends Error {
2
+ constructor(
3
+ message: string,
4
+ public httpStatus?: number,
5
+ public errorCode?: string,
6
+ ) {
7
+ super(message);
8
+ this.name = "GitHubError";
9
+ }
10
+ }
11
+
12
+ export class NotFoundError extends GitHubError {
13
+ constructor(message = "Resource not found") {
14
+ super(message, 404, "NOT_FOUND");
15
+ this.name = "NotFoundError";
16
+ }
17
+ }
18
+
19
+ export class RateLimitError extends GitHubError {
20
+ constructor(
21
+ message = "GitHub API rate limit exceeded",
22
+ public resetAt?: Date,
23
+ ) {
24
+ super(message, 429, "RATE_LIMITED");
25
+ this.name = "RateLimitError";
26
+ }
27
+ }
28
+
29
+ export class AuthError extends GitHubError {
30
+ constructor(message = "GitHub authentication failed") {
31
+ super(message, 401, "UNAUTHENTICATED");
32
+ this.name = "AuthError";
33
+ }
34
+ }
35
+
36
+ export class TimeoutError extends GitHubError {
37
+ constructor(message = "GitHub API request timed out") {
38
+ super(message, 408, "TIMEOUT");
39
+ this.name = "TimeoutError";
40
+ }
41
+ }
42
+
43
+ export interface GitHubClientOptions {
44
+ token?: string;
45
+ userAgent?: string;
46
+ timeoutMs?: number;
47
+ baseUrl?: string;
48
+ }
49
+
50
+ export interface RequestOptions extends Omit<RequestInit, "body"> {
51
+ variables?: Record<string, unknown>;
52
+ timeoutMs?: number;
53
+ }
54
+
55
+ /**
56
+ * A next-generation, generic GitHub GraphQL client.
57
+ * Designed to be robust, strongly typed, and publishable.
58
+ */
59
+ export class GitHubClient {
60
+ private readonly token: string;
61
+ private readonly userAgent: string;
62
+ private readonly timeoutMs: number;
63
+ private readonly baseUrl: string;
64
+
65
+ constructor(options: GitHubClientOptions = {}) {
66
+ const token =
67
+ options.token || (typeof process !== "undefined" ? process.env?.GITHUB_TOKEN : "") || "";
68
+ if (!token) {
69
+ console.warn(
70
+ "GitHubClient initialized without a token. Requests requiring authentication will fail.",
71
+ );
72
+ }
73
+
74
+ this.token = token;
75
+ this.userAgent = options.userAgent || "nextgen-github-client/1.0";
76
+ this.timeoutMs = options.timeoutMs || 10000;
77
+ this.baseUrl = options.baseUrl || "https://api.github.com/graphql";
78
+ }
79
+
80
+ /**
81
+ * Classifies fetch or API errors into distinct custom error classes.
82
+ */
83
+ private classifyError(message: string, status: number, headers?: Headers): never {
84
+ const lower = message.toLowerCase();
85
+
86
+ if (status === 404 || lower.includes("not resolve to a user") || lower.includes("not found")) {
87
+ throw new NotFoundError(message);
88
+ }
89
+
90
+ if (
91
+ status === 429 ||
92
+ status === 403 ||
93
+ lower.includes("rate limit") ||
94
+ lower.includes("abuse")
95
+ ) {
96
+ const resetHeader = headers?.get("x-ratelimit-reset");
97
+ const resetAt = resetHeader ? new Date(parseInt(resetHeader, 10) * 1000) : undefined;
98
+ throw new RateLimitError(message, resetAt);
99
+ }
100
+
101
+ if (
102
+ status === 401 ||
103
+ lower.includes("bad credentials") ||
104
+ lower.includes("requires authentication")
105
+ ) {
106
+ throw new AuthError(message);
107
+ }
108
+
109
+ throw new GitHubError(message || `GitHub API error (${status})`, status);
110
+ }
111
+
112
+ /**
113
+ * Executes a GraphQL query against the GitHub API.
114
+ *
115
+ * @param query The GraphQL query string.
116
+ * @param options Request options including variables and fetch options.
117
+ * @returns Strongly typed response data.
118
+ */
119
+ public async request<T = any>(query: string, options: RequestOptions = {}): Promise<T> {
120
+ const { variables, timeoutMs = this.timeoutMs, signal: userSignal, ...fetchOptions } = options;
121
+
122
+ const headers = new Headers(fetchOptions.headers);
123
+ if (this.token && !headers.has("Authorization")) {
124
+ headers.set("Authorization", `bearer ${this.token}`);
125
+ }
126
+ if (!headers.has("Content-Type")) {
127
+ headers.set("Content-Type", "application/json");
128
+ }
129
+ if (!headers.has("User-Agent")) {
130
+ headers.set("User-Agent", this.userAgent);
131
+ }
132
+
133
+ const abortController = new AbortController();
134
+ const timeoutId =
135
+ timeoutMs > 0
136
+ ? setTimeout(() => abortController.abort(new TimeoutError()), timeoutMs)
137
+ : undefined;
138
+
139
+ // Link user-provided signal if present
140
+ const onAbort = userSignal ? () => abortController.abort(userSignal.reason) : undefined;
141
+ if (userSignal) {
142
+ userSignal.addEventListener("abort", onAbort!);
143
+ if (userSignal.aborted) abortController.abort(userSignal.reason);
144
+ }
145
+
146
+ let response: Response;
147
+
148
+ try {
149
+ response = await fetch(this.baseUrl, {
150
+ ...fetchOptions,
151
+ method: "POST",
152
+ headers,
153
+ body: JSON.stringify({ query, variables }),
154
+ signal: abortController.signal,
155
+ });
156
+ } catch (error: any) {
157
+ if (error.name === "AbortError" || error instanceof TimeoutError) {
158
+ throw new TimeoutError(error.message || "Request timed out");
159
+ }
160
+ throw new GitHubError(`Network error: ${error.message || "Unknown error"}`);
161
+ } finally {
162
+ if (timeoutId) clearTimeout(timeoutId);
163
+ if (userSignal) {
164
+ userSignal.removeEventListener("abort", onAbort!);
165
+ }
166
+ }
167
+
168
+ if (!response.ok) {
169
+ const errorText = await response.text().catch(() => "");
170
+ this.classifyError(errorText || response.statusText, response.status, response.headers);
171
+ }
172
+
173
+ const body = (await response.json().catch(() => ({}))) as any;
174
+
175
+ if (body.errors && body.errors.length > 0) {
176
+ // Throw the first error specifically, but could be enhanced to aggregate
177
+ const firstError = body.errors[0];
178
+ this.classifyError(firstError.message, response.status, response.headers);
179
+ }
180
+
181
+ if (!body.data) {
182
+ throw new GitHubError(
183
+ "Invalid response: No data returned from GraphQL endpoint",
184
+ response.status,
185
+ );
186
+ }
187
+
188
+ return body.data as T;
189
+ }
190
+ }
191
+
192
+ // Export a default singleton instance for convenience
193
+ export const githubClient = new GitHubClient();
@@ -11,7 +11,8 @@ export function icon(name: keyof typeof icons, color: string, size = 16): string
11
11
  const path = icons[name];
12
12
  if (!path) return "";
13
13
  const viewBox = name === "x" ? "0 0 24 24" : "0 0 16 16";
14
+ const colorStr = color.startsWith("#") ? color : `#${color}`;
14
15
  const fill =
15
- name === "x" ? `stroke="#${color}" stroke-width="2" fill="none"` : `fill="#${color}"`;
16
+ name === "x" ? `stroke="${colorStr}" stroke-width="2" fill="none"` : `fill="${colorStr}"`;
16
17
  return `<svg width="${size}" height="${size}" viewBox="${viewBox}" ${fill} role="img">${path}</svg>`;
17
18
  }
@@ -3,3 +3,4 @@ export { kFormat, escapeXml, wrapText } from "./format";
3
3
  export { getLangColor } from "./languages";
4
4
  export { icons, icon } from "./icons";
5
5
  export { FONT_FACE, FONT_FAMILY } from "./font";
6
+ export * from "./colors";
@@ -7,25 +7,160 @@ export type Theme = {
7
7
  };
8
8
 
9
9
  export const themes: Record<string, Theme> = {
10
- default: { bg: "fffefe", title: "2f80ed", text: "434d58", icon: "4c71f2", border: "e4e2e2" },
11
- dark: { bg: "151515", title: "fff", text: "9f9f9f", icon: "79ff97", border: "2a2a2a" },
12
- radical: { bg: "141321", title: "fe428e", text: "a9fef7", icon: "f8d847", border: "2a2a40" },
13
- merko: { bg: "0a0f0b", title: "abd200", text: "68b587", icon: "b7d364", border: "1a2f1a" },
14
- gruvbox: { bg: "282828", title: "fabd2f", text: "ebdbb2", icon: "fe8019", border: "3c3836" },
15
- tokyonight: { bg: "1a1b27", title: "70a5fd", text: "38bdae", icon: "bf91f3", border: "2a2b3d" },
16
- onedark: { bg: "282c34", title: "e4bf7a", text: "abb2bf", icon: "8eb573", border: "3e4451" },
17
- cobalt: { bg: "193549", title: "e683d9", text: "75eeb2", icon: "0480ef", border: "2a4a6a" },
18
- synthwave: { bg: "2b213a", title: "e2e9ec", text: "e5289e", icon: "ef8539", border: "3e2f5a" },
19
- highcontrast: { bg: "000", title: "e7f216", text: "fff", icon: "00ffff", border: "333" },
20
- dracula: { bg: "282a36", title: "ff6e96", text: "f8f8f2", icon: "bd93f9", border: "44475a" },
21
- monokai: { bg: "272822", title: "f92672", text: "f8f8f2", icon: "a6e22e", border: "3e3d32" },
22
- nord: { bg: "2e3440", title: "88c0d0", text: "d8dee9", icon: "81a1c1", border: "3b4252" },
23
- github_dark: { bg: "0d1117", title: "58a6ff", text: "c9d1d9", icon: "1f6feb", border: "21262d" },
24
- pearl: { bg: "f7f7f5", title: "1f2328", text: "3d444d", icon: "0969da", border: "e6e8eb" },
25
- slate: { bg: "0b1220", title: "e2e8f0", text: "94a3b8", icon: "38bdf8", border: "1f2937" },
26
- forest: { bg: "0f1b17", title: "b7f7d9", text: "86a995", icon: "34d399", border: "17332a" },
27
- rose: { bg: "1a0f14", title: "ffd0dd", text: "e6b0c0", icon: "fb7185", border: "2b1a22" },
28
- sand: { bg: "fbf7f0", title: "6b4e2e", text: "7a6754", icon: "d97706", border: "eadfce" },
10
+ default: {
11
+ bg: "fill-white",
12
+ title: "fill-blue-600",
13
+ text: "fill-slate-700",
14
+ icon: "fill-blue-500",
15
+ border: "stroke-slate-200",
16
+ },
17
+ dark: {
18
+ bg: "fill-zinc-900",
19
+ title: "fill-white",
20
+ text: "fill-zinc-400",
21
+ icon: "fill-emerald-400",
22
+ border: "stroke-zinc-800",
23
+ },
24
+ radical: {
25
+ bg: "fill-slate-900",
26
+ title: "fill-pink-500",
27
+ text: "fill-cyan-300",
28
+ icon: "fill-yellow-400",
29
+ border: "stroke-slate-800",
30
+ },
31
+ merko: {
32
+ bg: "fill-neutral-950",
33
+ title: "fill-lime-400",
34
+ text: "fill-emerald-400",
35
+ icon: "fill-lime-300",
36
+ border: "stroke-neutral-800",
37
+ },
38
+ gruvbox: {
39
+ bg: "fill-stone-800",
40
+ title: "fill-amber-400",
41
+ text: "fill-stone-300",
42
+ icon: "fill-orange-500",
43
+ border: "stroke-stone-700",
44
+ },
45
+ tokyonight: {
46
+ bg: "fill-slate-900",
47
+ title: "fill-blue-400",
48
+ text: "fill-teal-400",
49
+ icon: "fill-fuchsia-400",
50
+ border: "stroke-slate-800",
51
+ },
52
+ onedark: {
53
+ bg: "fill-slate-800",
54
+ title: "fill-amber-300",
55
+ text: "fill-slate-400",
56
+ icon: "fill-green-400",
57
+ border: "stroke-slate-700",
58
+ },
59
+ cobalt: {
60
+ bg: "fill-sky-950",
61
+ title: "fill-fuchsia-400",
62
+ text: "fill-emerald-300",
63
+ icon: "fill-blue-500",
64
+ border: "stroke-sky-900",
65
+ },
66
+ synthwave: {
67
+ bg: "fill-purple-950",
68
+ title: "fill-slate-200",
69
+ text: "fill-pink-500",
70
+ icon: "fill-orange-400",
71
+ border: "stroke-purple-900",
72
+ },
73
+ highcontrast: {
74
+ bg: "fill-black",
75
+ title: "fill-yellow-300",
76
+ text: "fill-white",
77
+ icon: "fill-cyan-400",
78
+ border: "stroke-zinc-800",
79
+ },
80
+ dracula: {
81
+ bg: "fill-slate-900",
82
+ title: "fill-pink-400",
83
+ text: "fill-slate-100",
84
+ icon: "fill-purple-400",
85
+ border: "stroke-slate-700",
86
+ },
87
+ monokai: {
88
+ bg: "fill-zinc-900",
89
+ title: "fill-pink-500",
90
+ text: "fill-zinc-100",
91
+ icon: "fill-lime-400",
92
+ border: "stroke-zinc-800",
93
+ },
94
+ nord: {
95
+ bg: "fill-slate-800",
96
+ title: "fill-cyan-400",
97
+ text: "fill-slate-300",
98
+ icon: "fill-blue-400",
99
+ border: "stroke-slate-700",
100
+ },
101
+ github_dark: {
102
+ bg: "fill-slate-900",
103
+ title: "fill-blue-400",
104
+ text: "fill-slate-300",
105
+ icon: "fill-blue-500",
106
+ border: "stroke-slate-800",
107
+ },
108
+ pearl: {
109
+ bg: "fill-slate-50",
110
+ title: "fill-slate-900",
111
+ text: "fill-slate-600",
112
+ icon: "fill-blue-600",
113
+ border: "stroke-slate-200",
114
+ },
115
+ slate: {
116
+ bg: "fill-slate-950",
117
+ title: "fill-slate-200",
118
+ text: "fill-slate-400",
119
+ icon: "fill-sky-400",
120
+ border: "stroke-slate-800",
121
+ },
122
+ forest: {
123
+ bg: "fill-emerald-950",
124
+ title: "fill-emerald-200",
125
+ text: "fill-emerald-400",
126
+ icon: "fill-emerald-500",
127
+ border: "stroke-emerald-900",
128
+ },
129
+ rose: {
130
+ bg: "fill-rose-950",
131
+ title: "fill-rose-200",
132
+ text: "fill-rose-300",
133
+ icon: "fill-rose-400",
134
+ border: "stroke-rose-900",
135
+ },
136
+ sand: {
137
+ bg: "fill-orange-50",
138
+ title: "fill-amber-900",
139
+ text: "fill-amber-700",
140
+ icon: "fill-amber-600",
141
+ border: "stroke-orange-200",
142
+ },
143
+ shiina: {
144
+ title: "fill-pink-400",
145
+ text: "fill-slate-100",
146
+ icon: "fill-sky-400",
147
+ bg: "#0B0C10",
148
+ border: "stroke-slate-800",
149
+ },
150
+ aurora: {
151
+ title: "fill-emerald-300",
152
+ text: "fill-slate-100",
153
+ icon: "fill-teal-400",
154
+ bg: "#020617",
155
+ border: "stroke-emerald-900",
156
+ },
157
+ oled: {
158
+ title: "fill-rose-400",
159
+ text: "fill-zinc-400",
160
+ icon: "fill-indigo-400",
161
+ bg: "#000000",
162
+ border: "stroke-zinc-900",
163
+ },
29
164
  };
30
165
 
31
166
  export function resolveColors(opts: {