grab-url 0.9.135 → 0.9.137

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="200" height="200" style="shape-rendering: auto; display: block; " xmlns:xlink="http://www.w3.org/1999/xlink"><g><circle r="20" fill="#e90c59" cy="50" cx="30">
2
+ <animate begin="-0.5s" values="30;70;30" keyTimes="0;0.5;1" dur="1s" repeatCount="indefinite" attributeName="cx"></animate>
3
+ </circle>
4
+ <circle r="20" fill="#0f3bc6" cy="50" cx="70">
5
+ <animate begin="0s" values="30;70;30" keyTimes="0;0.5;1" dur="1s" repeatCount="indefinite" attributeName="cx"></animate>
6
+ </circle>
7
+ <circle r="20" fill="#e90c59" cy="50" cx="30">
8
+ <animate begin="-0.5s" values="30;70;30" keyTimes="0;0.5;1" dur="1s" repeatCount="indefinite" attributeName="cx"></animate>
9
+ <animate repeatCount="indefinite" dur="1s" keyTimes="0;0.499;0.5;1" calcMode="discrete" values="0;0;1;1" attributeName="fill-opacity"></animate>
10
+ </circle><g></g></g></svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="200" height="200" style="shape-rendering: auto; display: block; " xmlns:xlink="http://www.w3.org/1999/xlink"><g><g>
2
+ <path stroke-width="12" stroke="#e1945b" fill="none" d="M50 15A35 35 0 1 0 74.74873734152916 25.251262658470843"></path>
3
+ <path fill="#e1945b" d="M49 3L49 27L61 15L49 3"></path>
4
+ <animateTransform keyTimes="0;1" values="0 50 50;360 50 50" dur="1s" repeatCount="indefinite" type="rotate" attributeName="transform"></animateTransform>
5
+ </g><g></g></g></svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="200" height="200" style="shape-rendering: auto; display: block; " xmlns:xlink="http://www.w3.org/1999/xlink"><g><circle stroke-linecap="round" fill="none" stroke-dasharray="50.26548245743669 50.26548245743669" stroke="#fe718d" stroke-width="8" r="32" cy="50" cx="50">
2
+ <animateTransform values="0 50 50;360 50 50" keyTimes="0;1" dur="1s" repeatCount="indefinite" type="rotate" attributeName="transform"></animateTransform>
3
+ </circle><g></g></g></svg>
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="200" height="200" style="shape-rendering: auto; display: block" xmlns:xlink="http://www.w3.org/1999/xlink"><g><circle stroke-width="2" stroke="#cbb953" fill="none" r="0" cy="50" cx="50">
2
+ <animate begin="0s" calcMode="spline" keySplines="0 0.2 0.8 1" keyTimes="0;1" values="0;40" dur="1s" repeatCount="indefinite" attributeName="r"></animate>
3
+ <animate begin="0s" calcMode="spline" keySplines="0.2 0 0.8 1" keyTimes="0;1" values="1;0" dur="1s" repeatCount="indefinite" attributeName="opacity"></animate>
4
+ </circle><circle stroke-width="2" stroke="#cbb953" fill="none" r="0" cy="50" cx="50">
5
+ <animate begin="-0.5s" calcMode="spline" keySplines="0 0.2 0.8 1" keyTimes="0;1" values="0;40" dur="1s" repeatCount="indefinite" attributeName="r"></animate>
6
+ <animate begin="-0.5s" calcMode="spline" keySplines="0.2 0 0.8 1" keyTimes="0;1" values="1;0" dur="1s" repeatCount="indefinite" attributeName="opacity"></animate>
7
+ </circle><g></g></g></svg>
@@ -0,0 +1,49 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="200" height="200" style="shape-rendering: auto; display: block; " xmlns:xlink="http://www.w3.org/1999/xlink"><g><g transform="rotate(0 50 50)">
2
+ <rect fill="#0099e5" height="12" width="6" ry="6" rx="3" y="24" x="47">
3
+ <animate repeatCount="indefinite" begin="-0.9166666666666666s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
4
+ </rect>
5
+ </g><g transform="rotate(30 50 50)">
6
+ <rect fill="#0099e5" height="12" width="6" ry="6" rx="3" y="24" x="47">
7
+ <animate repeatCount="indefinite" begin="-0.8333333333333334s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
8
+ </rect>
9
+ </g><g transform="rotate(60 50 50)">
10
+ <rect fill="#0099e5" height="12" width="6" ry="6" rx="3" y="24" x="47">
11
+ <animate repeatCount="indefinite" begin="-0.75s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
12
+ </rect>
13
+ </g><g transform="rotate(90 50 50)">
14
+ <rect fill="#0099e5" height="12" width="6" ry="6" rx="3" y="24" x="47">
15
+ <animate repeatCount="indefinite" begin="-0.6666666666666666s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
16
+ </rect>
17
+ </g><g transform="rotate(120 50 50)">
18
+ <rect fill="#0099e5" height="12" width="6" ry="6" rx="3" y="24" x="47">
19
+ <animate repeatCount="indefinite" begin="-0.5833333333333334s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
20
+ </rect>
21
+ </g><g transform="rotate(150 50 50)">
22
+ <rect fill="#0099e5" height="12" width="6" ry="6" rx="3" y="24" x="47">
23
+ <animate repeatCount="indefinite" begin="-0.5s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
24
+ </rect>
25
+ </g><g transform="rotate(180 50 50)">
26
+ <rect fill="#0099e5" height="12" width="6" ry="6" rx="3" y="24" x="47">
27
+ <animate repeatCount="indefinite" begin="-0.4166666666666667s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
28
+ </rect>
29
+ </g><g transform="rotate(210 50 50)">
30
+ <rect fill="#0099e5" height="12" width="6" ry="6" rx="3" y="24" x="47">
31
+ <animate repeatCount="indefinite" begin="-0.3333333333333333s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
32
+ </rect>
33
+ </g><g transform="rotate(240 50 50)">
34
+ <rect fill="#0099e5" height="12" width="6" ry="6" rx="3" y="24" x="47">
35
+ <animate repeatCount="indefinite" begin="-0.25s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
36
+ </rect>
37
+ </g><g transform="rotate(270 50 50)">
38
+ <rect fill="#0099e5" height="12" width="6" ry="6" rx="3" y="24" x="47">
39
+ <animate repeatCount="indefinite" begin="-0.16666666666666666s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
40
+ </rect>
41
+ </g><g transform="rotate(300 50 50)">
42
+ <rect fill="#0099e5" height="12" width="6" ry="6" rx="3" y="24" x="47">
43
+ <animate repeatCount="indefinite" begin="-0.08333333333333333s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
44
+ </rect>
45
+ </g><g transform="rotate(330 50 50)">
46
+ <rect fill="#0099e5" height="12" width="6" ry="6" rx="3" y="24" x="47">
47
+ <animate repeatCount="indefinite" begin="0s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
48
+ </rect>
49
+ </g><g></g></g></svg>
@@ -0,0 +1,57 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="200" height="200" style="shape-rendering: auto; display: block; " xmlns:xlink="http://www.w3.org/1999/xlink"><g><g transform="translate(80,50)">
2
+ <g transform="rotate(0)">
3
+ <circle fill-opacity="1" fill="#0099e5" r="6" cy="0" cx="0">
4
+ <animateTransform repeatCount="indefinite" dur="1s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.875s" type="scale" attributeName="transform"></animateTransform>
5
+ <animate begin="-0.875s" values="1;0" repeatCount="indefinite" dur="1s" keyTimes="0;1" attributeName="fill-opacity"></animate>
6
+ </circle>
7
+ </g>
8
+ </g><g transform="translate(71.21320343559643,71.21320343559643)">
9
+ <g transform="rotate(45)">
10
+ <circle fill-opacity="0.875" fill="#0099e5" r="6" cy="0" cx="0">
11
+ <animateTransform repeatCount="indefinite" dur="1s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.75s" type="scale" attributeName="transform"></animateTransform>
12
+ <animate begin="-0.75s" values="1;0" repeatCount="indefinite" dur="1s" keyTimes="0;1" attributeName="fill-opacity"></animate>
13
+ </circle>
14
+ </g>
15
+ </g><g transform="translate(50,80)">
16
+ <g transform="rotate(90)">
17
+ <circle fill-opacity="0.75" fill="#0099e5" r="6" cy="0" cx="0">
18
+ <animateTransform repeatCount="indefinite" dur="1s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.625s" type="scale" attributeName="transform"></animateTransform>
19
+ <animate begin="-0.625s" values="1;0" repeatCount="indefinite" dur="1s" keyTimes="0;1" attributeName="fill-opacity"></animate>
20
+ </circle>
21
+ </g>
22
+ </g><g transform="translate(28.786796564403577,71.21320343559643)">
23
+ <g transform="rotate(135)">
24
+ <circle fill-opacity="0.625" fill="#0099e5" r="6" cy="0" cx="0">
25
+ <animateTransform repeatCount="indefinite" dur="1s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.5s" type="scale" attributeName="transform"></animateTransform>
26
+ <animate begin="-0.5s" values="1;0" repeatCount="indefinite" dur="1s" keyTimes="0;1" attributeName="fill-opacity"></animate>
27
+ </circle>
28
+ </g>
29
+ </g><g transform="translate(20,50.00000000000001)">
30
+ <g transform="rotate(180)">
31
+ <circle fill-opacity="0.5" fill="#0099e5" r="6" cy="0" cx="0">
32
+ <animateTransform repeatCount="indefinite" dur="1s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.375s" type="scale" attributeName="transform"></animateTransform>
33
+ <animate begin="-0.375s" values="1;0" repeatCount="indefinite" dur="1s" keyTimes="0;1" attributeName="fill-opacity"></animate>
34
+ </circle>
35
+ </g>
36
+ </g><g transform="translate(28.78679656440357,28.786796564403577)">
37
+ <g transform="rotate(225)">
38
+ <circle fill-opacity="0.375" fill="#0099e5" r="6" cy="0" cx="0">
39
+ <animateTransform repeatCount="indefinite" dur="1s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.25s" type="scale" attributeName="transform"></animateTransform>
40
+ <animate begin="-0.25s" values="1;0" repeatCount="indefinite" dur="1s" keyTimes="0;1" attributeName="fill-opacity"></animate>
41
+ </circle>
42
+ </g>
43
+ </g><g transform="translate(49.99999999999999,20)">
44
+ <g transform="rotate(270)">
45
+ <circle fill-opacity="0.25" fill="#0099e5" r="6" cy="0" cx="0">
46
+ <animateTransform repeatCount="indefinite" dur="1s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.125s" type="scale" attributeName="transform"></animateTransform>
47
+ <animate begin="-0.125s" values="1;0" repeatCount="indefinite" dur="1s" keyTimes="0;1" attributeName="fill-opacity"></animate>
48
+ </circle>
49
+ </g>
50
+ </g><g transform="translate(71.21320343559643,28.78679656440357)">
51
+ <g transform="rotate(315)">
52
+ <circle fill-opacity="0.125" fill="#0099e5" r="6" cy="0" cx="0">
53
+ <animateTransform repeatCount="indefinite" dur="1s" keyTimes="0;1" values="1.5 1.5;1 1" begin="0s" type="scale" attributeName="transform"></animateTransform>
54
+ <animate begin="0s" values="1;0" repeatCount="indefinite" dur="1s" keyTimes="0;1" attributeName="fill-opacity"></animate>
55
+ </circle>
56
+ </g>
57
+ </g><g></g></g></svg>
@@ -0,0 +1,17 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="200" height="200" style="shape-rendering: auto; display: block; " xmlns:xlink="http://www.w3.org/1999/xlink"><g><rect fill="#e15b64" height="20" width="20" y="19" x="19">
2
+ <animate calcMode="discrete" begin="0s" repeatCount="indefinite" dur="1s" keyTimes="0;0.125;1" values="#f8b26a;#e15b64;#e15b64" attributeName="fill"></animate>
3
+ </rect><rect fill="#e15b64" height="20" width="20" y="19" x="40">
4
+ <animate calcMode="discrete" begin="0.125s" repeatCount="indefinite" dur="1s" keyTimes="0;0.125;1" values="#f8b26a;#e15b64;#e15b64" attributeName="fill"></animate>
5
+ </rect><rect fill="#e15b64" height="20" width="20" y="19" x="61">
6
+ <animate calcMode="discrete" begin="0.25s" repeatCount="indefinite" dur="1s" keyTimes="0;0.125;1" values="#f8b26a;#e15b64;#e15b64" attributeName="fill"></animate>
7
+ </rect><rect fill="#e15b64" height="20" width="20" y="40" x="19">
8
+ <animate calcMode="discrete" begin="0.875s" repeatCount="indefinite" dur="1s" keyTimes="0;0.125;1" values="#f8b26a;#e15b64;#e15b64" attributeName="fill"></animate>
9
+ </rect><rect fill="#e15b64" height="20" width="20" y="40" x="61">
10
+ <animate calcMode="discrete" begin="0.375s" repeatCount="indefinite" dur="1s" keyTimes="0;0.125;1" values="#f8b26a;#e15b64;#e15b64" attributeName="fill"></animate>
11
+ </rect><rect fill="#e15b64" height="20" width="20" y="61" x="19">
12
+ <animate calcMode="discrete" begin="0.75s" repeatCount="indefinite" dur="1s" keyTimes="0;0.125;1" values="#f8b26a;#e15b64;#e15b64" attributeName="fill"></animate>
13
+ </rect><rect fill="#e15b64" height="20" width="20" y="61" x="40">
14
+ <animate calcMode="discrete" begin="0.625s" repeatCount="indefinite" dur="1s" keyTimes="0;0.125;1" values="#f8b26a;#e15b64;#e15b64" attributeName="fill"></animate>
15
+ </rect><rect fill="#e15b64" height="20" width="20" y="61" x="61">
16
+ <animate calcMode="discrete" begin="0.5s" repeatCount="indefinite" dur="1s" keyTimes="0;0.125;1" values="#f8b26a;#e15b64;#e15b64" attributeName="fill"></animate>
17
+ </rect><g></g></g></svg>
package/src/log.ts ADDED
@@ -0,0 +1,309 @@
1
+ /**
2
+ * ### Colorized Log With JSON Structure
3
+ * ![Debug log](https://i.imgur.com/R8Qp6Vg.png)
4
+ * Logs messages to the console with custom styling,
5
+ * prints JSON with description of structure layout,
6
+ * and showing debug output in development only.
7
+ * @param {string|object} message - The message to log. If an object is provided, it will be stringified.
8
+ * @param {string|string[]} [options.style] default='color: blue; font-size: 11pt;' - CSS style string
9
+ * @param {boolean} [options.hideInProduction] - default = auto-detects based on hostname.
10
+ * If true, uses `console.debug` (hidden in production). If false, uses `console.log`.
11
+ *
12
+ */
13
+ export function log(message = "", options: LogOptions = {}) {
14
+ let {
15
+ color = null,
16
+ style = "color: blue; font-size: 11pt;",
17
+ hideInProduction = undefined,
18
+ startSpinner = false,
19
+ stopSpinner = false,
20
+ } = options;
21
+
22
+ // Auto-detect if we should hide logs in production based on hostname
23
+ if (typeof hideInProduction === "undefined")
24
+ hideInProduction =
25
+ typeof window !== "undefined" &&
26
+ window?.location.hostname.includes("localhost");
27
+
28
+ // For objects, print both the structure visualization and full JSON
29
+ if (typeof message === "object")
30
+ message =
31
+ printJSONStructure(message) + "\n\n" + JSON.stringify(message, null, 2);
32
+
33
+ //colorize in terminal (%c is only in browser)
34
+ if (color && typeof process !== undefined)
35
+ message = (colors[color] || "") + message + colors.reset;
36
+
37
+ // Displays an animated spinner in the terminal with the provided text.
38
+ var i = 0;
39
+
40
+ if (startSpinner)
41
+ (global || globalThis).interval = setInterval(() => {
42
+ process.stdout.write(
43
+ (colors[color] || "") +
44
+ "\r" +
45
+ "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏".split("")[(i = ++i % 10)] +
46
+ " " +
47
+ message +
48
+ colors.reset
49
+ );
50
+ }, 50);
51
+ else if (stopSpinner) {
52
+ clearInterval((global || globalThis).interval);
53
+ process.stdout.write(
54
+ "\r" + (message || "✔ Done") + " ".repeat(message.length + 20) + "\n"
55
+ );
56
+ } else if (typeof style === "string") {
57
+ // check if style is a one word color code or named color
58
+ //test if style is valid as a CSS color name
59
+ if (style.split(" ").length == 1 || color) {
60
+ style = `color: ${color || style}; font-size: 11pt;`;
61
+ } else {
62
+ // check if style is valid as a CSS color code
63
+ if (style.match(/^#[0-9a-fA-F]{6}$/)) {
64
+ style = `color: ${style}; font-size: 11pt;`;
65
+ }
66
+ }
67
+ // Use console.debug for production-hidden logs, console.log otherwise
68
+ if (hideInProduction)
69
+ console.debug((style ? "%c" : "") + (message || ""), style);
70
+ else console.log((style ? "%c" : "") + (message || ""), style);
71
+ } else if (typeof style === "object") console.log(message, ...(style as any));
72
+ return true;
73
+ }
74
+
75
+ export interface LogOptions {
76
+ /** CSS style string or array of CSS strings for browser console styling */
77
+ style?: string | string[];
78
+ /** Optional color name or code for terminal environments */
79
+ color?: keyof typeof colors | null;
80
+ /** If true, hides log in production (auto-detects by hostname if undefined) */
81
+ hideInProduction?: boolean;
82
+ /** Start a spinner (for CLI tools, optional) */
83
+ startSpinner?: boolean;
84
+ /** Stop a spinner (for CLI tools, optional) */
85
+ stopSpinner?: boolean;
86
+ }
87
+
88
+ // ANSI escape codes for terminal colors when running in Node.js
89
+ export const colors = {
90
+ reset: "\x1b[0m", // Reset to default color
91
+ black: "\x1b[30m",
92
+ red: "\x1b[31m", // Functions, errors
93
+ green: "\x1b[32m", // Object braces, success
94
+ yellow: "\x1b[33m", // Strings, warnings
95
+ blue: "\x1b[34m", // Array brackets, info
96
+ magenta: "\x1b[35m", // Booleans
97
+ cyan: "\x1b[36m", // Numbers
98
+ white: "\x1b[37m", // Default color, plain text
99
+ gray: "\x1b[90m", // Null, undefined, subtle
100
+ // Bright variants
101
+ brightRed: "\x1b[91m",
102
+ brightGreen: "\x1b[92m",
103
+ brightYellow: "\x1b[93m",
104
+ brightBlue: "\x1b[94m",
105
+ brightMagenta: "\x1b[95m",
106
+ brightCyan: "\x1b[96m",
107
+ brightWhite: "\x1b[97m",
108
+ // Background colors (optional)
109
+ bgRed: "\x1b[41m",
110
+ bgGreen: "\x1b[42m",
111
+ bgYellow: "\x1b[43m",
112
+ bgBlue: "\x1b[44m",
113
+ bgMagenta: "\x1b[45m",
114
+ bgCyan: "\x1b[46m",
115
+ bgWhite: "\x1b[47m",
116
+ bgGray: "\x1b[100m",
117
+ };
118
+
119
+ /**
120
+ * Determines the appropriate color code for a given value type
121
+ * Used for consistent color coding in the structure visualization
122
+ */
123
+ function getColorForType(value) {
124
+ if (typeof value === "string") return colors.yellow;
125
+ if (typeof value === "number") return colors.cyan;
126
+ if (typeof value === "boolean") return colors.magenta;
127
+ if (typeof value === "function") return colors.red;
128
+ if (value === null) return colors.gray;
129
+ if (Array.isArray(value)) return colors.blue;
130
+ if (typeof value === "object") return colors.green;
131
+ return colors.white;
132
+ }
133
+
134
+ /**
135
+ * Returns a string representation of the value's type
136
+ * Used to show simplified type information in the structure visualization
137
+ */
138
+ function getTypeString(value) {
139
+ if (typeof value === "string") return '""';
140
+ if (typeof value === "number") return "number";
141
+ if (typeof value === "boolean") return "bool";
142
+ if (typeof value === "function") return "function";
143
+ if (value === null) return "null";
144
+ if (Array.isArray(value)) {
145
+ if (value.length) return "[" + getTypeString(value[0]) + "]";
146
+ else return "[]";
147
+ }
148
+ if (typeof value === "object") return "{...}";
149
+ return typeof value;
150
+ }
151
+
152
+ /**
153
+ * Creates a colored visualization of a JSON object's structure
154
+ * Shows the shape and types of the data rather than actual values
155
+ * Recursively processes nested objects and arrays
156
+ */
157
+ export function printJSONStructure(obj, indent = 0) {
158
+ const pad = " ".repeat(indent);
159
+
160
+ // Handle primitive values and null
161
+ if (typeof obj !== "object" || obj === null) {
162
+ const color = getColorForType(obj);
163
+ return color + getTypeString(obj) + colors.reset;
164
+ }
165
+
166
+ // Handle arrays with special bracket formatting
167
+ if (Array.isArray(obj)) {
168
+ let result = colors.blue + "[" + colors.reset;
169
+ if (obj.length) result += "\n";
170
+ obj.forEach((item, idx) => {
171
+ result += pad + " " + printJSONStructure(item, indent + 1);
172
+ if (idx < obj.length - 1) result += ",";
173
+ result += "\n";
174
+ });
175
+ result += pad + colors.blue + "]" + colors.reset;
176
+ return result;
177
+ }
178
+
179
+ // Handle objects with special brace and property formatting
180
+ let result = colors.green + "{" + colors.reset;
181
+ const keys = Object.keys(obj);
182
+ if (keys.length) result += "\n";
183
+ keys.forEach((key, index) => {
184
+ const value = obj[key];
185
+ const color = getColorForType(value);
186
+ result += pad + " ";
187
+
188
+ // Handle nested objects recursively
189
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
190
+ result +=
191
+ color +
192
+ key +
193
+ colors.reset +
194
+ ": " +
195
+ printJSONStructure(value, indent + 1);
196
+ }
197
+ // Handle nested arrays recursively
198
+ else if (Array.isArray(value)) {
199
+ result +=
200
+ color +
201
+ key +
202
+ colors.reset +
203
+ ": " +
204
+ printJSONStructure(value, indent + 1);
205
+ }
206
+ // Handle primitive values
207
+ else {
208
+ result += color + key + ": " + getTypeString(value) + colors.reset;
209
+ }
210
+ if (index < keys.length - 1) result += ",";
211
+ result += "\n";
212
+ });
213
+ result += pad + colors.green + "}" + colors.reset;
214
+
215
+ // Only log at top level of recursion
216
+ if (indent === 0) {
217
+ // console.log(result);
218
+ }
219
+ return result;
220
+ }
221
+
222
+ /**
223
+ * Shows message in a modal overlay with scrollable message stack
224
+ * and is easier to dismiss unlike alert() which blocks window.
225
+ * Creates a semi-transparent overlay with a white box containing the message.
226
+ * @param {string} msg - The message to display
227
+ */
228
+ export function showAlert(msg) {
229
+ if (typeof document === "undefined") return;
230
+ let o = document.getElementById("alert-overlay"),
231
+ list;
232
+
233
+ // Create overlay and alert box if they don't exist
234
+ if (!o) {
235
+ o = document.body.appendChild(document.createElement("div"));
236
+ o.id = "alert-overlay";
237
+ o.setAttribute(
238
+ "style",
239
+ "position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center"
240
+ );
241
+ o.innerHTML = `<div id="alert-box" style="background:#fff;padding:1.5em 2em;border-radius:8px;box-shadow:0 2px 16px #0003;min-width:220px;max-height:80vh;position:relative;display:flex;flex-direction:column;">
242
+ <button id="close-alert" style="position:absolute;top:12px;right:20px;font-size:1.5em;background:none;border:none;cursor:pointer;color:black;">&times;</button>
243
+ <div id="alert-list" style="overflow:auto;flex:1;"></div>
244
+ </div>`;
245
+
246
+ // Add click handlers to close overlay
247
+ o.addEventListener("click", (e) => e.target == o && o.remove());
248
+ document.getElementById("close-alert").onclick = () => o.remove();
249
+ }
250
+
251
+ list = o.querySelector("#alert-list");
252
+
253
+ // Add new message to list
254
+ list.innerHTML += `<div style="border-bottom:1px solid #333; font-size:1.2em;margin:0.5em 0;">${msg}</div>`;
255
+ }
256
+
257
+ /**
258
+ * Sets up development tools for debugging API requests
259
+ * Adds a keyboard shortcut (Ctrl+I) that shows a modal with request history
260
+ * Each request entry shows:
261
+ * - Request path
262
+ * - Request details
263
+ * - Response data
264
+ * - Timestamp
265
+ */
266
+ export function setupDevTools() {
267
+ // Keyboard shortcut (Ctrl+I) to toggle debug view
268
+ document.addEventListener("keydown", (e) => {
269
+ if (e.key === "i" && e.ctrlKey) {
270
+ // Create HTML of the grab.log requests
271
+ let html = " ";
272
+ for (let request of grab.log) {
273
+ html += `<div style="margin-bottom:1em; border-bottom:1px solid #ccc; padding-bottom:1em;">
274
+ <b>Path:</b> ${request.path}<br>
275
+ <b>Request:</b> ${request.request}<br>
276
+ <b>Response:</b> ${JSON.stringify(request.response, null, 2)}<br>
277
+ <b>Time:</b> ${new Date(request.lastFetchTime).toLocaleString()}
278
+ </div>`;
279
+ }
280
+ showAlert(html);
281
+ }
282
+ });
283
+ }
284
+
285
+ /**
286
+ * Displays an animated spinner in the terminal with the provided text.
287
+ * The spinner animates in-place until the returned function is called,
288
+ * which stops the spinner and prints a success message.
289
+ * @param {string} text - The text to display next to the spinner animation.
290
+ * @returns {(success?: string) => void} Stop function with optional message.
291
+ * @example
292
+ * const stopSpinner = showSpinnerInTerminal('Downloading...');
293
+ * setTimeout(() => {
294
+ * stopSpinner('Success!');
295
+ * }, 2000);
296
+ */
297
+ export function showSpinnerInTerminal(text) {
298
+ let i = 0,
299
+ interval = setInterval(() => {
300
+ process.stdout.write(
301
+ "\r" + "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏".split("")[(i = ++i % 10)] + " " + text
302
+ );
303
+ }, 50);
304
+
305
+ return function (success = "✔ Done!") {
306
+ clearInterval(interval);
307
+ process.stdout.write("\r" + success + " ".repeat(text.length) + "\n");
308
+ };
309
+ }