@shiinasaku/github-card 2.0.0 → 4.2.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/README.md +159 -261
- package/dist/card.d.ts +16 -0
- package/dist/lib.d.ts +13 -0
- package/dist/lib.js +1347 -0
- package/dist/types.d.ts +37 -0
- package/dist/utils/colors.d.ts +2 -0
- package/dist/utils/font.d.ts +4 -0
- package/dist/utils/format.d.ts +10 -0
- package/dist/utils/icons.d.ts +11 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/languages.d.ts +2 -0
- package/dist/utils/themes.d.ts +16 -0
- package/package.json +30 -20
- package/src/card.ts +537 -113
- package/src/env.d.ts +4 -0
- package/src/github.ts +406 -480
- package/src/index.ts +148 -134
- package/src/input.css +1 -0
- package/src/lib.ts +1 -1
- package/src/tailwind.css +3837 -0
- package/src/types.ts +1 -0
- package/src/utils/colors.ts +85 -0
- package/src/utils/font.ts +5 -19
- package/src/utils/format.ts +54 -16
- package/src/utils/github-client.ts +198 -0
- package/src/utils/icons.ts +27 -9
- package/src/utils/index.ts +2 -1
- package/src/utils/themes.ts +154 -19
- package/fonts/Roboto-subset.woff2 +0 -0
package/src/types.ts
CHANGED
|
@@ -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,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
/** System font stack — no shipped font, respects the user's OS. */
|
|
2
|
+
export const FONT_FAMILY =
|
|
3
|
+
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
|
|
4
|
+
/** @deprecated No shipped font — kept for API backwards compatibility. */
|
|
5
|
+
export const FONT_FACE = "";
|
package/src/utils/format.ts
CHANGED
|
@@ -1,7 +1,46 @@
|
|
|
1
1
|
export function kFormat(num: number): string {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
const sign = num < 0 ? "-" : "";
|
|
3
|
+
const abs = Math.abs(num);
|
|
4
|
+
const trim = (v: number) => v.toFixed(1).replace(/\.0$/, "");
|
|
5
|
+
if (abs >= 999_950_000) return sign + trim(abs / 1_000_000_000) + "B";
|
|
6
|
+
if (abs >= 999_950) return sign + trim(abs / 1_000_000) + "M";
|
|
7
|
+
if (abs >= 999.95) return sign + trim(abs / 1_000) + "k";
|
|
8
|
+
return sign + String(Math.round(abs));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Approximate rendered width of a string in SVG user units for the default
|
|
13
|
+
* sans-serif stack. Widths are per-character averages tuned to the card's
|
|
14
|
+
* font sizes and weights, so layout can reserve enough space without relying
|
|
15
|
+
* on browser text measurement (which is unavailable server-side).
|
|
16
|
+
*/
|
|
17
|
+
export function measureText(text: string, size: number, weight = 400): number {
|
|
18
|
+
if (!text) return 0;
|
|
19
|
+
const factor = size / 16;
|
|
20
|
+
const weightFactor = weight >= 700 ? 1.08 : weight >= 600 ? 1.04 : 1;
|
|
21
|
+
let width = 0;
|
|
22
|
+
for (const ch of text) {
|
|
23
|
+
const code = ch.codePointAt(0)!;
|
|
24
|
+
let w: number;
|
|
25
|
+
if (ch === " ") w = 0.28;
|
|
26
|
+
else if (ch === "." || ch === "," || ch === ":" || ch === ";") w = 0.3;
|
|
27
|
+
else if (ch === "i" || ch === "l" || ch === "I" || ch === "t" || ch === "j" || ch === "|")
|
|
28
|
+
w = 0.3;
|
|
29
|
+
else if (ch === "m" || ch === "w" || ch === "W" || ch === "M" || ch === "@") w = 0.95;
|
|
30
|
+
else if (code >= 0x1100 && code <= 0x11ff)
|
|
31
|
+
w = 1.0; // Hangul
|
|
32
|
+
else if (code >= 0x4e00 && code <= 0x9fff)
|
|
33
|
+
w = 1.0; // CJK
|
|
34
|
+
else if (code >= 0x3040 && code <= 0x30ff)
|
|
35
|
+
w = 1.0; // Kana
|
|
36
|
+
else if (code >= 0xac00 && code <= 0xd7af)
|
|
37
|
+
w = 1.0; // Hangul syllables
|
|
38
|
+
else if (code > 0x7f)
|
|
39
|
+
w = 0.72; // other non-ASCII (emoji, accents)
|
|
40
|
+
else w = 0.58; // default latin
|
|
41
|
+
width += w;
|
|
42
|
+
}
|
|
43
|
+
return width * factor * weightFactor;
|
|
5
44
|
}
|
|
6
45
|
|
|
7
46
|
export function escapeXml(str: string): string {
|
|
@@ -17,26 +56,25 @@ export function wrapText(input: string, maxLen: number, maxLines: number): strin
|
|
|
17
56
|
const words = input.split(/\s+/).filter(Boolean);
|
|
18
57
|
const lines: string[] = [];
|
|
19
58
|
let current = "";
|
|
20
|
-
|
|
21
|
-
|
|
59
|
+
|
|
60
|
+
for (const word of words) {
|
|
61
|
+
const next = current ? `${current} ${word}` : word;
|
|
22
62
|
if (next.length <= maxLen) {
|
|
23
63
|
current = next;
|
|
24
64
|
continue;
|
|
25
65
|
}
|
|
26
66
|
if (current) lines.push(current);
|
|
27
|
-
|
|
28
|
-
|
|
67
|
+
if (lines.length === maxLines) break;
|
|
68
|
+
current = word.length > maxLen ? word.slice(0, maxLen) : word;
|
|
29
69
|
}
|
|
30
70
|
if (current && lines.length < maxLines) lines.push(current);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
lines[lines.length - 1] = last.slice(0, Math.max(0, maxLen - 1)) + "…";
|
|
39
|
-
}
|
|
71
|
+
|
|
72
|
+
// Truncated? Mark the last line with an ellipsis.
|
|
73
|
+
const consumed = lines.join(" ").length;
|
|
74
|
+
if (lines.length > 0 && consumed < input.trim().length) {
|
|
75
|
+
const last = lines[lines.length - 1]!;
|
|
76
|
+
lines[lines.length - 1] =
|
|
77
|
+
(last.length >= maxLen ? last.slice(0, Math.max(0, maxLen - 1)) : last) + "…";
|
|
40
78
|
}
|
|
41
79
|
return lines;
|
|
42
80
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
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 = unknown>(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: unknown) {
|
|
157
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
158
|
+
const name = error instanceof Error ? error.name : "";
|
|
159
|
+
if (name === "AbortError" || error instanceof TimeoutError) {
|
|
160
|
+
throw new TimeoutError(message || "Request timed out");
|
|
161
|
+
}
|
|
162
|
+
throw new GitHubError(`Network error: ${message || "Unknown error"}`);
|
|
163
|
+
} finally {
|
|
164
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
165
|
+
if (userSignal) {
|
|
166
|
+
userSignal.removeEventListener("abort", onAbort!);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
const errorText = await response.text().catch(() => "");
|
|
172
|
+
this.classifyError(errorText || response.statusText, response.status, response.headers);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const body = (await response.json().catch(() => ({}))) as {
|
|
176
|
+
data?: T;
|
|
177
|
+
errors?: { message: string }[];
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
if (body.errors && body.errors.length > 0) {
|
|
181
|
+
const firstError = body.errors[0];
|
|
182
|
+
const message = firstError?.message ?? "GraphQL error";
|
|
183
|
+
this.classifyError(message, response.status, response.headers);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!body.data) {
|
|
187
|
+
throw new GitHubError(
|
|
188
|
+
"Invalid response: No data returned from GraphQL endpoint",
|
|
189
|
+
response.status,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return body.data as T;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Export a default singleton instance for convenience
|
|
198
|
+
export const githubClient = new GitHubClient();
|
package/src/utils/icons.ts
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
export const icons = {
|
|
2
|
-
star: `<path
|
|
2
|
+
star: `<path d="M10.92 2.868a1.25 1.25 0 0 1 2.16 0l2.795 4.798l5.428 1.176a1.25 1.25 0 0 1 .667 2.054l-3.7 4.141l.56 5.525a1.25 1.25 0 0 1-1.748 1.27L12 19.592l-5.082 2.24a1.25 1.25 0 0 1-1.748-1.27l.56-5.525l-3.7-4.14a1.25 1.25 0 0 1 .667-2.055l5.428-1.176z"/>`,
|
|
3
3
|
commit: `<path fill-rule="evenodd" d="M10.5 7.75a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm1.43.75a4.002 4.002 0 01-7.86 0H.75a.75.75 0 110-1.5h3.32a4.001 4.001 0 017.86 0h3.32a.75.75 0 110 1.5h-3.32z"/>`,
|
|
4
|
-
pr: `<path
|
|
5
|
-
issue: `<path
|
|
6
|
-
repo: `<path
|
|
4
|
+
pr: `<path d="M4 18a2 2 0 1 0 4 0a2 2 0 1 0-4 0M4 6a2 2 0 1 0 4 0a2 2 0 1 0-4 0m12 12a2 2 0 1 0 4 0a2 2 0 1 0-4 0M6 8v8"/><path d="M11 6h5a2 2 0 0 1 2 2v8"/><path d="m14 9l-3-3l3-3"/>`,
|
|
5
|
+
issue: `<path d="M15.314 2a2 2 0 0 1 1.414.586l4.686 4.686A2 2 0 0 1 22 8.686v6.628a2 2 0 0 1-.586 1.414l-4.686 4.686a2 2 0 0 1-1.414.586H8.686a2 2 0 0 1-1.414-.586l-4.686-4.686A2 2 0 0 1 2 15.314V8.686a2 2 0 0 1 .586-1.414l4.686-4.686A2 2 0 0 1 8.686 2zm0 2H8.686L4 8.686v6.628L8.686 20h6.628L20 15.314V8.686zM12 15a1 1 0 1 1 0 2a1 1 0 0 1 0-2m0-9a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V7a1 1 0 0 1 1-1"/>`,
|
|
6
|
+
repo: `<path d="M6 19.623c-.69-.195-1.232-.491-1.682-.941C3 17.364 3 15.242 3 11s0-6.364 1.318-7.682S7.758 2 12 2s6.364 0 7.682 1.318S21 6.758 21 11s0 6.364-1.318 7.682c-.45.45-.993.746-1.682.941"/><path d="M12 20.193c-.414 0-.748.32-1.414.958c-.647.619-.97.929-1.242.831a.5.5 0 0 1-.096-.046C9 21.779 9 21.31 9 20.376v-3.125c0-1.532 0-2.299.44-2.775C9.878 14 10.585 14 12 14s2.121 0 2.56.476s.44 1.243.44 2.775v3.125c0 .935 0 1.403-.248 1.56a.5.5 0 0 1-.096.046c-.272.098-.595-.212-1.242-.831c-.666-.639-1-.958-1.414-.958M8 10h8M8 6h4"/>`,
|
|
7
7
|
x: `<path d="M4 4l11.733 16h4.267l-11.733 -16l-4.267 0" /><path d="M4 20l6.768 -6.768m2.46 -2.46l6.772 -6.772" />`,
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
type IconName = keyof typeof icons;
|
|
11
|
+
|
|
12
|
+
const iconMeta: Record<
|
|
13
|
+
IconName,
|
|
14
|
+
{ viewBox: string; kind: "fill" | "stroke"; strokeWidth?: string }
|
|
15
|
+
> = {
|
|
16
|
+
star: { viewBox: "0 0 24 24", kind: "fill" },
|
|
17
|
+
commit: { viewBox: "0 0 16 16", kind: "fill" },
|
|
18
|
+
pr: { viewBox: "0 0 24 24", kind: "stroke", strokeWidth: "2" },
|
|
19
|
+
issue: { viewBox: "0 0 24 24", kind: "fill" },
|
|
20
|
+
repo: { viewBox: "0 0 24 24", kind: "stroke", strokeWidth: "1.5" },
|
|
21
|
+
x: { viewBox: "0 0 24 24", kind: "stroke", strokeWidth: "2" },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function icon(name: IconName, color: string, size = 16): string {
|
|
11
25
|
const path = icons[name];
|
|
12
26
|
if (!path) return "";
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
27
|
+
const meta = iconMeta[name];
|
|
28
|
+
const viewBox = meta?.viewBox ?? "0 0 16 16";
|
|
29
|
+
const colorStr = color.startsWith("#") ? color : `#${color}`;
|
|
30
|
+
const attrs =
|
|
31
|
+
meta?.kind === "stroke"
|
|
32
|
+
? `fill="none" stroke="${colorStr}" stroke-width="${meta.strokeWidth ?? "1.8"}" stroke-linecap="round" stroke-linejoin="round"`
|
|
33
|
+
: `fill="${colorStr}"`;
|
|
34
|
+
return `<svg width="${size}" height="${size}" viewBox="${viewBox}" ${attrs} role="img">${path}</svg>`;
|
|
17
35
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { resolveColors, themes, type Theme } from "./themes";
|
|
2
|
-
export { kFormat, escapeXml, wrapText } from "./format";
|
|
2
|
+
export { kFormat, escapeXml, wrapText, measureText } 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";
|
package/src/utils/themes.ts
CHANGED
|
@@ -7,25 +7,160 @@ export type Theme = {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
export const themes: Record<string, Theme> = {
|
|
10
|
-
default: {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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: {
|
|
Binary file
|