@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/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,5 @@
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
+ /** 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 = "";
@@ -1,7 +1,46 @@
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 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
- for (const w of words) {
21
- const next = current ? `${current} ${w}` : w;
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
- current = w;
28
- if (lines.length >= maxLines - 1) break;
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
- 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
- }
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();
@@ -1,17 +1,35 @@
1
1
  export const icons = {
2
- star: `<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>`,
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 fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"/>`,
5
- issue: `<path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"/>`,
6
- repo: `<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></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
- export function icon(name: keyof typeof icons, color: string, size = 16): string {
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 viewBox = name === "x" ? "0 0 24 24" : "0 0 16 16";
14
- const fill =
15
- name === "x" ? `stroke="#${color}" stroke-width="2" fill="none"` : `fill="#${color}"`;
16
- return `<svg width="${size}" height="${size}" viewBox="${viewBox}" ${fill} role="img">${path}</svg>`;
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
  }
@@ -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";
@@ -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: {
Binary file