ink-gradient-shim 0.1.0-alpha.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.d.ts +9 -0
  2. package/index.js +153 -0
  3. package/package.json +11 -0
package/index.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type React from "react";
2
+
3
+ export interface GradientProps {
4
+ colors?: string[];
5
+ children?: React.ReactNode;
6
+ }
7
+
8
+ declare const Gradient: React.FC<GradientProps>;
9
+ export default Gradient;
package/index.js ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * ink-gradient shim.
3
+ * Applies a per-line multiline gradient and emits ANSI truecolor text.
4
+ */
5
+ import React from "react";
6
+
7
+ const NAMED_COLORS = {
8
+ black: [0, 0, 0],
9
+ red: [255, 0, 0],
10
+ green: [0, 255, 0],
11
+ yellow: [255, 255, 0],
12
+ blue: [0, 0, 255],
13
+ magenta: [255, 0, 255],
14
+ cyan: [0, 255, 255],
15
+ white: [255, 255, 255],
16
+ gray: [127, 127, 127],
17
+ grey: [127, 127, 127],
18
+ };
19
+
20
+ const GRADIENT_TRACE_ENABLED = process.env.INK_GRADIENT_TRACE === "1";
21
+ const SHIM_PATH =
22
+ typeof __filename === "string"
23
+ ? __filename
24
+ : typeof import.meta !== "undefined"
25
+ ? import.meta.url
26
+ : "unknown";
27
+
28
+ const traceGradient = (message) => {
29
+ if (!GRADIENT_TRACE_ENABLED) return;
30
+ try {
31
+ process.stderr.write(`[ink-gradient-shim trace] ${message}\n`);
32
+ } catch {
33
+ // Best-effort tracing only.
34
+ }
35
+ };
36
+
37
+ const clampByte = (value) => Math.max(0, Math.min(255, Math.round(value)));
38
+ const ANSI_ESCAPE_REGEX = /\u001b\[[0-9:;]*[ -/]*[@-~]|\u009b[0-9:;]*[ -/]*[@-~]/g;
39
+
40
+ const parseColor = (color) => {
41
+ if (!color || typeof color !== "string") return undefined;
42
+ const trimmed = color.trim();
43
+ if (!trimmed) return undefined;
44
+
45
+ const lower = trimmed.toLowerCase();
46
+ const named = NAMED_COLORS[lower];
47
+ if (named) return { r: named[0], g: named[1], b: named[2] };
48
+
49
+ if (trimmed.startsWith("#")) {
50
+ const hex = trimmed.slice(1);
51
+ if (/^[\da-fA-F]{6}$/.test(hex)) {
52
+ return {
53
+ r: Number.parseInt(hex.slice(0, 2), 16),
54
+ g: Number.parseInt(hex.slice(2, 4), 16),
55
+ b: Number.parseInt(hex.slice(4, 6), 16),
56
+ };
57
+ }
58
+ if (/^[\da-fA-F]{3}$/.test(hex)) {
59
+ return {
60
+ r: Number.parseInt(hex[0] + hex[0], 16),
61
+ g: Number.parseInt(hex[1] + hex[1], 16),
62
+ b: Number.parseInt(hex[2] + hex[2], 16),
63
+ };
64
+ }
65
+ return undefined;
66
+ }
67
+
68
+ const rgbMatch = trimmed.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
69
+ if (!rgbMatch) return undefined;
70
+
71
+ return {
72
+ r: clampByte(Number(rgbMatch[1])),
73
+ g: clampByte(Number(rgbMatch[2])),
74
+ b: clampByte(Number(rgbMatch[3])),
75
+ };
76
+ };
77
+
78
+ const mixChannel = (start, end, t) => clampByte(start + (end - start) * t);
79
+
80
+ const interpolateStops = (stops, t) => {
81
+ if (stops.length === 0) return { r: 255, g: 255, b: 255 };
82
+ if (stops.length === 1) return stops[0];
83
+
84
+ const clamped = Math.max(0, Math.min(1, t));
85
+ const scaled = clamped * (stops.length - 1);
86
+ const leftIndex = Math.floor(scaled);
87
+ const rightIndex = Math.min(stops.length - 1, leftIndex + 1);
88
+ const localT = scaled - leftIndex;
89
+
90
+ const left = stops[leftIndex];
91
+ const right = stops[rightIndex];
92
+ return {
93
+ r: mixChannel(left.r, right.r, localT),
94
+ g: mixChannel(left.g, right.g, localT),
95
+ b: mixChannel(left.b, right.b, localT),
96
+ };
97
+ };
98
+
99
+ const stripAnsi = (value) => value.replace(ANSI_ESCAPE_REGEX, "");
100
+
101
+ const extractPlainText = (value) => {
102
+ if (value == null || typeof value === "boolean") return "";
103
+ if (typeof value === "string" || typeof value === "number") return String(value);
104
+ if (Array.isArray(value)) return value.map(extractPlainText).join("");
105
+ if (React.isValidElement(value)) return extractPlainText(value.props?.children);
106
+ return "";
107
+ };
108
+
109
+ const applyGradient = (text, stops) => {
110
+ if (stops.length < 2) return stripAnsi(text);
111
+
112
+ const lines = text.split("\n");
113
+
114
+ const renderedLines = lines.map((line) => {
115
+ const chars = Array.from(stripAnsi(line));
116
+ if (chars.length === 0) return "";
117
+ const denominator = Math.max(1, chars.length - 1);
118
+ const sampled = Array.from({ length: chars.length }, (_, index) =>
119
+ interpolateStops(stops, index / denominator),
120
+ );
121
+ let out = "";
122
+ for (let index = 0; index < chars.length; index += 1) {
123
+ const color = sampled[index];
124
+ out += `\u001b[38;2;${color.r};${color.g};${color.b}m${chars[index]}`;
125
+ }
126
+ return `${out}\u001b[0m`;
127
+ });
128
+
129
+ return renderedLines.join("\n");
130
+ };
131
+
132
+ const Gradient = ({ colors, children }) => {
133
+ const traceCountRef = React.useRef(0);
134
+ const parsedStops = (Array.isArray(colors) ? colors : [])
135
+ .map((entry) => parseColor(entry))
136
+ .filter(Boolean);
137
+ const colorsLength = Array.isArray(colors) ? colors.length : 0;
138
+ const plainText = extractPlainText(children);
139
+ const gradientText = applyGradient(plainText, parsedStops);
140
+ React.useEffect(() => {
141
+ if (!GRADIENT_TRACE_ENABLED || traceCountRef.current >= 20) return;
142
+ if (traceCountRef.current === 0) {
143
+ traceGradient(`module=${SHIM_PATH}`);
144
+ }
145
+ traceCountRef.current += 1;
146
+ traceGradient(
147
+ `render#${traceCountRef.current} colors=${colorsLength} parsedStops=${parsedStops.length} textChars=${Array.from(plainText).length} emittedAnsi=${gradientText.includes("\u001b[38;2;")}`,
148
+ );
149
+ }, [colorsLength, parsedStops.length, plainText, gradientText]);
150
+ return React.createElement("ink-text", null, gradientText);
151
+ };
152
+
153
+ export default Gradient;
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "ink-gradient-shim",
3
+ "version": "0.1.0-alpha.44",
4
+ "type": "module",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "peerDependencies": {
8
+ "ink": "*",
9
+ "react": "^18.0.0 || ^19.0.0"
10
+ }
11
+ }