@grampro/kit 2.7.2 → 2.7.3
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/.gbs/skills/gbs-components/SKILL.md +233 -209
- package/.gbs/skills/gbs-components/references/data-grid.md +9 -0
- package/.gbs/skills/gbs-components/references/forms.md +12 -0
- package/.gbs/skills/gbs-components/references/install.md +14 -3
- package/CHANGELOG.md +963 -861
- package/README.md +11 -3
- package/index.cjs +108 -123
- package/package.json +1 -1
- package/source/components/accordion/passport.json +1 -1
- package/source/components/alert/passport.json +1 -1
- package/source/components/avatar/passport.json +1 -1
- package/source/components/badge/passport.json +1 -1
- package/source/components/breadcrumb/passport.json +1 -1
- package/source/components/button/passport.json +1 -1
- package/source/components/card/passport.json +1 -1
- package/source/components/checkbox/passport.json +2 -2
- package/source/components/checkbox/passport.manual.json +1 -1
- package/source/components/checkbox/react/Checkbox.tsx +14 -0
- package/source/components/combobox/passport.json +3 -2
- package/source/components/combobox/passport.manual.json +1 -1
- package/source/components/combobox/react/MultiSelect.tsx +13 -0
- package/source/components/combobox/react/Select.tsx +13 -0
- package/source/components/combobox/react/surface.ts +70 -0
- package/source/components/data-grid/passport.json +6 -6
- package/source/components/data-grid/react/DataGrid.tsx +353 -351
- package/source/components/date-picker/passport.json +3 -2
- package/source/components/date-picker/passport.manual.json +1 -1
- package/source/components/date-picker/react/DatePicker.tsx +16 -2
- package/source/components/date-picker/react/DateRangePicker.tsx +16 -2
- package/source/components/date-picker/react/surface.ts +68 -0
- package/source/components/dialog/passport.json +1 -1
- package/source/components/file-uploader/passport.json +1 -1
- package/source/components/input/passport.json +2 -2
- package/source/components/input/passport.manual.json +1 -1
- package/source/components/input/react/Input.tsx +21 -0
- package/source/components/menu/passport.json +1 -1
- package/source/components/modal/passport.json +1 -1
- package/source/components/number-input/passport.json +2 -2
- package/source/components/number-input/passport.manual.json +1 -1
- package/source/components/number-input/react/NumberInput.tsx +30 -0
- package/source/components/popover/passport.json +1 -1
- package/source/components/progress/passport.json +1 -1
- package/source/components/radio-group/passport.json +2 -2
- package/source/components/radio-group/passport.manual.json +1 -1
- package/source/components/radio-group/react/RadioGroup.tsx +33 -0
- package/source/components/shared/core/agent/field-executors.ts +173 -137
- package/source/components/shared/core/agent/index.ts +21 -0
- package/source/components/shared/core/agent/surface.ts +437 -0
- package/source/components/shared/core/agent/webmcp.ts +332 -328
- package/source/components/shared/core/agent/write.ts +62 -58
- package/source/components/shared/index.ts +7 -0
- package/source/components/shared/react/AgentSurface.tsx +145 -0
- package/source/components/shared/react/agentSurfaceContext.ts +92 -0
- package/source/components/shared/react/useFieldAgent.ts +62 -88
- package/source/components/shared/version.json +4 -4
- package/source/components/shared/version.ts +6 -6
- package/source/components/skeleton/passport.json +1 -1
- package/source/components/spinner/passport.json +1 -1
- package/source/components/switch/passport.json +2 -2
- package/source/components/switch/passport.manual.json +1 -1
- package/source/components/switch/react/Switch.tsx +16 -0
- package/source/components/tabs/passport.json +1 -1
- package/source/components/textarea/passport.json +2 -2
- package/source/components/textarea/passport.manual.json +1 -1
- package/source/components/textarea/react/Textarea.tsx +16 -0
- package/source/components/toaster/passport.json +1 -1
- package/source/components/tooltip/passport.json +1 -1
- package/tools/doctor/baseline.cjs +5 -4
- package/tools/mcp/server.cjs +13 -1
- package/tools/passport/extract.cjs +20 -65
- package/tools/passport/hash.cjs +18 -1
- package/tools/passport/index.cjs +4 -2
- package/tools/passport/typescript.cjs +220 -0
- package/tools/prompt.cjs +205 -0
- package/tools/ui.cjs +67 -0
package/tools/prompt.cjs
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A multi-select prompt, with no dependency.
|
|
3
|
+
*
|
|
4
|
+
* The old picker cleared the whole screen on every key and printed every
|
|
5
|
+
* component, so it needed a full-screen terminal and visibly jumped. This one
|
|
6
|
+
* redraws only its own lines, in place, and shows a window of the list that
|
|
7
|
+
* fits the terminal, scrolling as the cursor moves. Every line is clipped to
|
|
8
|
+
* the terminal's width: a line that wrapped would throw off the count of lines
|
|
9
|
+
* to redraw, which is the jumping all over again.
|
|
10
|
+
*
|
|
11
|
+
* Keys: ↑/↓ (or PgUp/PgDn, Home/End) move, space toggles, typing filters,
|
|
12
|
+
* ctrl+a toggles everything shown, enter confirms, esc or ctrl+c cancels.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const readline = require("readline");
|
|
16
|
+
const { c, s, clip } = require("./ui.cjs");
|
|
17
|
+
|
|
18
|
+
const ESC = "\x1b[";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} config
|
|
22
|
+
* @param {string} config.message
|
|
23
|
+
* @param {{ value: string, label?: string, tag?: string, hint?: string, note?: string }[]} config.options
|
|
24
|
+
* @param {NodeJS.ReadStream} [config.input] defaults to stdin; a test passes a fake terminal
|
|
25
|
+
* @param {NodeJS.WriteStream} [config.output] defaults to stdout
|
|
26
|
+
* @returns {Promise<string[] | null>} the chosen values, or null when cancelled
|
|
27
|
+
*/
|
|
28
|
+
function multiselect({ message, options, input = process.stdin, output = process.stdout }) {
|
|
29
|
+
|
|
30
|
+
if (!input.isTTY || !output.isTTY) {
|
|
31
|
+
return Promise.reject(
|
|
32
|
+
new Error("The interactive picker needs a terminal. Name the components instead: npx @grampro/kit add Button Input"),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const chosen = new Set();
|
|
38
|
+
let query = "";
|
|
39
|
+
let cursor = 0;
|
|
40
|
+
let top = 0;
|
|
41
|
+
let drawn = 0;
|
|
42
|
+
let warning = "";
|
|
43
|
+
|
|
44
|
+
const tagWidth = Math.max(0, ...options.map((o) => (o.tag || "").length));
|
|
45
|
+
const labelWidth = Math.max(...options.map((o) => (o.label || o.value).length));
|
|
46
|
+
|
|
47
|
+
const visible = () => {
|
|
48
|
+
const q = query.trim().toLowerCase();
|
|
49
|
+
if (!q) return options;
|
|
50
|
+
return options.filter((o) =>
|
|
51
|
+
[o.value, o.label, o.tag, o.hint].some((field) => field && field.toLowerCase().includes(q)),
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/* Rows the list may use: what the terminal has, less the prompt's own frame. */
|
|
56
|
+
const windowSize = () => Math.max(3, Math.min(12, (output.rows || 24) - 9));
|
|
57
|
+
|
|
58
|
+
const lines = (state) => {
|
|
59
|
+
const cols = Math.max(20, (output.columns || 80) - 1);
|
|
60
|
+
const shown = visible();
|
|
61
|
+
const out = [];
|
|
62
|
+
|
|
63
|
+
if (state === "done" || state === "cancel") {
|
|
64
|
+
const symbol = state === "done" ? c.green(s.done) : c.red(s.cancel);
|
|
65
|
+
out.push(`${symbol} ${c.bold(clip(message, cols - 3))}`);
|
|
66
|
+
const picked = options.filter((o) => chosen.has(o.value)).map((o) => o.label || o.value);
|
|
67
|
+
const summary = state === "done" ? picked.join(", ") : "Cancelled";
|
|
68
|
+
out.push(`${c.gray(s.bar)} ${c.dim(clip(summary, cols - 3))}`);
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
out.push(`${c.cyan(s.active)} ${c.bold(clip(message, cols - 3))}`);
|
|
73
|
+
out.push(
|
|
74
|
+
query
|
|
75
|
+
? `${c.cyan(s.bar)} ${c.dim("Search:")} ${clip(query, cols - 12)}${c.cyan("_")}`
|
|
76
|
+
: `${c.cyan(s.bar)} ${c.dim(clip("Type to search", cols - 3))}`,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const size = windowSize();
|
|
80
|
+
if (cursor < top) top = cursor;
|
|
81
|
+
if (cursor >= top + size) top = cursor - size + 1;
|
|
82
|
+
top = Math.max(0, Math.min(top, Math.max(0, shown.length - size)));
|
|
83
|
+
|
|
84
|
+
if (shown.length === 0) {
|
|
85
|
+
out.push(`${c.cyan(s.bar)} ${c.yellow(clip(`Nothing matches "${query}"`, cols - 3))}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
out.push(
|
|
89
|
+
top > 0 ? `${c.cyan(s.bar)} ${c.dim(` ${s.up} ${top} more`)}` : `${c.cyan(s.bar)}`,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
for (let i = top; i < Math.min(shown.length, top + size); i += 1) {
|
|
93
|
+
const option = shown[i];
|
|
94
|
+
const active = i === cursor;
|
|
95
|
+
const on = chosen.has(option.value);
|
|
96
|
+
const box = on ? c.green(s.on) : c.dim(s.off);
|
|
97
|
+
const pointer = active ? c.cyan(s.pointer) : " ";
|
|
98
|
+
// Fixed columns, clipped as plain text before colour goes on.
|
|
99
|
+
const label = clip(option.label || option.value, labelWidth).padEnd(labelWidth);
|
|
100
|
+
const tag = clip(option.tag || "", tagWidth).padEnd(tagWidth);
|
|
101
|
+
const used = 3 + 2 + 2 + labelWidth + 2 + tagWidth + 2;
|
|
102
|
+
const note = option.note ? clip(option.note, Math.max(0, cols - used)) : "";
|
|
103
|
+
out.push(
|
|
104
|
+
`${c.cyan(s.bar)} ${pointer} ${box} ${active ? c.cyan(label) : label} ${c.gray(tag)} ${c.yellow(note)}`.trimEnd(),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const below = shown.length - (top + size);
|
|
109
|
+
out.push(below > 0 ? `${c.cyan(s.bar)} ${c.dim(` ${s.down} ${below} more`)}` : `${c.cyan(s.bar)}`);
|
|
110
|
+
|
|
111
|
+
const focused = shown[cursor];
|
|
112
|
+
out.push(`${c.cyan(s.bar)} ${c.dim(clip(focused && focused.hint ? focused.hint : "", cols - 3))}`);
|
|
113
|
+
out.push(
|
|
114
|
+
warning
|
|
115
|
+
? `${c.yellow(s.end)} ${c.yellow(clip(warning, cols - 3))}`
|
|
116
|
+
: `${c.cyan(s.end)} ${c.dim(
|
|
117
|
+
clip(
|
|
118
|
+
`${chosen.size} selected · space select · ctrl+a all · enter confirm · esc cancel`,
|
|
119
|
+
cols - 3,
|
|
120
|
+
),
|
|
121
|
+
)}`,
|
|
122
|
+
);
|
|
123
|
+
return out;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const draw = (state = "active") => {
|
|
127
|
+
const next = lines(state);
|
|
128
|
+
let frame = "";
|
|
129
|
+
if (drawn > 0) frame += `${ESC}${drawn}A\r${ESC}J`;
|
|
130
|
+
frame += `${next.join("\n")}\n`;
|
|
131
|
+
output.write(frame);
|
|
132
|
+
drawn = next.length;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const finish = (state, value) => {
|
|
136
|
+
input.removeListener("keypress", onKey);
|
|
137
|
+
output.removeListener("resize", onResize);
|
|
138
|
+
draw(state);
|
|
139
|
+
if (input.isTTY) input.setRawMode(false);
|
|
140
|
+
input.pause();
|
|
141
|
+
output.write(`${ESC}?25h`);
|
|
142
|
+
process.removeListener("exit", restoreCursor);
|
|
143
|
+
resolve(value);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const onKey = (text, key = {}) => {
|
|
147
|
+
warning = "";
|
|
148
|
+
const shown = visible();
|
|
149
|
+
const last = Math.max(0, shown.length - 1);
|
|
150
|
+
|
|
151
|
+
if ((key.ctrl && key.name === "c") || key.name === "escape") return finish("cancel", null);
|
|
152
|
+
|
|
153
|
+
if (key.name === "return" || key.name === "enter") {
|
|
154
|
+
if (chosen.size === 0) {
|
|
155
|
+
warning = "Select at least one component with space, or press esc to cancel.";
|
|
156
|
+
return draw();
|
|
157
|
+
}
|
|
158
|
+
return finish("done", options.filter((o) => chosen.has(o.value)).map((o) => o.value));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (key.name === "up") cursor = cursor <= 0 ? last : cursor - 1;
|
|
162
|
+
else if (key.name === "down") cursor = cursor >= last ? 0 : cursor + 1;
|
|
163
|
+
else if (key.name === "pageup") cursor = Math.max(0, cursor - windowSize());
|
|
164
|
+
else if (key.name === "pagedown") cursor = Math.min(last, cursor + windowSize());
|
|
165
|
+
else if (key.name === "home") cursor = 0;
|
|
166
|
+
else if (key.name === "end") cursor = last;
|
|
167
|
+
else if (key.name === "space") {
|
|
168
|
+
const option = shown[cursor];
|
|
169
|
+
if (option) {
|
|
170
|
+
if (chosen.has(option.value)) chosen.delete(option.value);
|
|
171
|
+
else chosen.add(option.value);
|
|
172
|
+
}
|
|
173
|
+
} else if (key.ctrl && key.name === "a") {
|
|
174
|
+
const all = shown.length > 0 && shown.every((o) => chosen.has(o.value));
|
|
175
|
+
for (const option of shown) {
|
|
176
|
+
if (all) chosen.delete(option.value);
|
|
177
|
+
else chosen.add(option.value);
|
|
178
|
+
}
|
|
179
|
+
} else if (key.name === "backspace") {
|
|
180
|
+
query = query.slice(0, -1);
|
|
181
|
+
cursor = 0;
|
|
182
|
+
} else if (text && !key.ctrl && !key.meta && text.length === 1 && text >= " ") {
|
|
183
|
+
query += text;
|
|
184
|
+
cursor = 0;
|
|
185
|
+
} else {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
return draw();
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const onResize = () => draw();
|
|
192
|
+
const restoreCursor = () => output.write(`${ESC}?25h`);
|
|
193
|
+
|
|
194
|
+
readline.emitKeypressEvents(input);
|
|
195
|
+
input.setRawMode(true);
|
|
196
|
+
input.resume();
|
|
197
|
+
input.on("keypress", onKey);
|
|
198
|
+
output.on("resize", onResize);
|
|
199
|
+
process.on("exit", restoreCursor);
|
|
200
|
+
output.write(`${ESC}?25l`);
|
|
201
|
+
draw();
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = { multiselect };
|
package/tools/ui.cjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Colour and symbols for the CLI, with no dependency.
|
|
3
|
+
*
|
|
4
|
+
* Colour is on only when it will be seen: stdout is a terminal, NO_COLOR is
|
|
5
|
+
* not set (https://no-color.org), and TERM is not "dumb". FORCE_COLOR turns it
|
|
6
|
+
* on regardless. Piped output, CI logs and the tests that read this CLI's
|
|
7
|
+
* stdout therefore get exactly the plain text they always did.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const env = process.env;
|
|
11
|
+
const enabled = (stream = process.stdout) => {
|
|
12
|
+
if (env.FORCE_COLOR && env.FORCE_COLOR !== "0") return true;
|
|
13
|
+
if ("NO_COLOR" in env || env.TERM === "dumb") return false;
|
|
14
|
+
return Boolean(stream && stream.isTTY);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const paint = (open, close) => (text, stream) =>
|
|
18
|
+
enabled(stream) ? `\x1b[${open}m${text}\x1b[${close}m` : String(text);
|
|
19
|
+
|
|
20
|
+
const c = {
|
|
21
|
+
bold: paint(1, 22),
|
|
22
|
+
dim: paint(2, 22),
|
|
23
|
+
inverse: paint(7, 27),
|
|
24
|
+
red: paint(31, 39),
|
|
25
|
+
green: paint(32, 39),
|
|
26
|
+
yellow: paint(33, 39),
|
|
27
|
+
blue: paint(34, 39),
|
|
28
|
+
magenta: paint(35, 39),
|
|
29
|
+
cyan: paint(36, 39),
|
|
30
|
+
gray: paint(90, 39),
|
|
31
|
+
bgCyan: paint(46, 49),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/* Windows' legacy console has no box-drawing glyphs worth trusting. */
|
|
35
|
+
const unicode = process.platform !== "win32" || Boolean(env.WT_SESSION || env.TERM_PROGRAM || env.ConEmuANSI);
|
|
36
|
+
|
|
37
|
+
const s = unicode
|
|
38
|
+
? { bar: "│", start: "┌", end: "└", active: "◆", done: "◇", cancel: "■", on: "◼", off: "◻", pointer: "❯", up: "↑", down: "↓" }
|
|
39
|
+
: { bar: "|", start: "T", end: "L", active: "*", done: "o", cancel: "x", on: "[x]", off: "[ ]", pointer: ">", up: "^", down: "v" };
|
|
40
|
+
|
|
41
|
+
/** ✓ and ✗ in colour, for the install log. Plain when colour is off. */
|
|
42
|
+
const check = (stream) => c.green("✓", stream);
|
|
43
|
+
const cross = (stream = process.stderr) => c.red("✗", stream);
|
|
44
|
+
|
|
45
|
+
/** Visible width of a string, ignoring ANSI sequences. */
|
|
46
|
+
const width = (text) => String(text).replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "").length;
|
|
47
|
+
|
|
48
|
+
/** Cut plain text to `max` visible characters, with an ellipsis. */
|
|
49
|
+
const clip = (text, max) => {
|
|
50
|
+
const plain = String(text);
|
|
51
|
+
if (max <= 0) return "";
|
|
52
|
+
return plain.length <= max ? plain : `${plain.slice(0, Math.max(0, max - 1))}…`;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** The opening line, as create-vite and friends draw it. */
|
|
56
|
+
const intro = (title) => {
|
|
57
|
+
console.log(`${c.gray(s.start)} ${c.bgCyan(c.bold(` ${title} `))}`);
|
|
58
|
+
console.log(c.gray(s.bar));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const outro = (message) => {
|
|
62
|
+
console.log(c.gray(s.bar));
|
|
63
|
+
console.log(`${c.gray(s.end)} ${message}`);
|
|
64
|
+
console.log("");
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
module.exports = { c, s, check, cross, enabled, width, clip, intro, outro };
|