@grampro/kit 2.6.3 → 2.7.1
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 +5 -4
- package/.gbs/skills/gbs-components/references/install.md +17 -14
- package/CHANGELOG.md +188 -0
- package/README.md +26 -7
- package/index.cjs +59 -59
- package/package.json +3 -3
- package/schema/passport-overlay-v1.schema.json +67 -0
- package/source/components/accordion/passport.json +45 -8
- package/source/components/accordion/passport.manual.json +46 -0
- package/source/components/alert/passport.json +41 -8
- package/source/components/alert/passport.manual.json +42 -0
- package/source/components/avatar/passport.json +40 -7
- package/source/components/avatar/passport.manual.json +43 -0
- package/source/components/badge/passport.json +43 -7
- package/source/components/badge/passport.manual.json +46 -0
- package/source/components/breadcrumb/passport.json +36 -7
- package/source/components/breadcrumb/passport.manual.json +39 -0
- package/source/components/button/passport.json +1 -1
- package/source/components/button/passport.manual.json +1 -1
- package/source/components/card/passport.json +45 -8
- package/source/components/card/passport.manual.json +47 -0
- package/source/components/checkbox/core/agent.ts +33 -0
- package/source/components/checkbox/core/index.ts +2 -0
- package/source/components/checkbox/passport.json +83 -10
- package/source/components/checkbox/passport.manual.json +82 -0
- package/source/components/combobox/core/agent.ts +38 -0
- package/source/components/combobox/core/index.ts +2 -0
- package/source/components/combobox/passport.json +103 -11
- package/source/components/combobox/passport.manual.json +101 -0
- package/source/components/combobox/react/props.ts +14 -0
- package/source/components/combobox/react/useCombobox.ts +7 -1
- package/source/components/data-grid/export/xlsx.ts +9 -2
- package/source/components/data-grid/passport.json +2 -2
- package/source/components/data-grid/passport.manual.json +1 -1
- package/source/components/date-picker/core/agent.ts +42 -0
- package/source/components/date-picker/core/index.ts +2 -0
- package/source/components/date-picker/passport.json +102 -11
- package/source/components/date-picker/passport.manual.json +100 -0
- package/source/components/date-picker/react/DatePicker.tsx +6 -0
- package/source/components/date-picker/react/DateRangePicker.tsx +4 -0
- package/source/components/date-picker/react/props.ts +17 -0
- package/source/components/dialog/passport.json +54 -9
- package/source/components/dialog/passport.manual.json +54 -0
- package/source/components/file-uploader/passport.json +65 -10
- package/source/components/file-uploader/passport.manual.json +64 -0
- package/source/components/input/core/agent.ts +36 -0
- package/source/components/input/core/index.ts +2 -0
- package/source/components/input/passport.json +89 -12
- package/source/components/input/passport.manual.json +86 -0
- package/source/components/menu/passport.json +64 -9
- package/source/components/menu/passport.manual.json +64 -0
- package/source/components/modal/passport.json +52 -9
- package/source/components/modal/passport.manual.json +52 -0
- package/source/components/number-input/core/agent.ts +38 -0
- package/source/components/number-input/core/index.ts +2 -0
- package/source/components/number-input/passport.json +92 -11
- package/source/components/number-input/passport.manual.json +90 -0
- package/source/components/popover/passport.json +47 -8
- package/source/components/popover/passport.manual.json +48 -0
- package/source/components/progress/passport.json +37 -8
- package/source/components/progress/passport.manual.json +39 -0
- package/source/components/radio-group/core/agent.ts +34 -0
- package/source/components/radio-group/core/index.ts +2 -0
- package/source/components/radio-group/passport.json +77 -10
- package/source/components/radio-group/passport.manual.json +76 -0
- package/source/components/shared/core/agent/field-agent.ts +101 -0
- package/source/components/shared/core/agent/field-executors.ts +137 -0
- package/source/components/shared/core/agent/field.ts +649 -0
- package/source/components/shared/core/agent/index.ts +30 -0
- package/source/components/shared/core/agent/write.ts +58 -0
- package/source/components/shared/react/useFieldAgent.ts +88 -0
- package/source/components/skeleton/passport.json +37 -6
- package/source/components/skeleton/passport.manual.json +39 -0
- package/source/components/spinner/passport.json +34 -7
- package/source/components/spinner/passport.manual.json +37 -0
- package/source/components/switch/core/agent.ts +31 -0
- package/source/components/switch/core/index.ts +2 -0
- package/source/components/switch/passport.json +68 -10
- package/source/components/switch/passport.manual.json +67 -0
- package/source/components/tabs/passport.json +50 -9
- package/source/components/tabs/passport.manual.json +50 -0
- package/source/components/textarea/core/agent.ts +34 -0
- package/source/components/textarea/core/index.ts +2 -0
- package/source/components/textarea/passport.json +83 -10
- package/source/components/textarea/passport.manual.json +82 -0
- package/source/components/toaster/passport.json +41 -8
- package/source/components/toaster/passport.manual.json +42 -0
- package/source/components/tooltip/passport.json +39 -6
- package/source/components/tooltip/passport.manual.json +41 -0
- package/tools/mcp/README.md +23 -0
- package/tools/mcp/design.cjs +6 -1
- package/tools/mcp/figma.cjs +49 -9
- package/tools/mcp/install.cjs +6 -1
- package/tools/mcp/server.cjs +30 -2
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schema/passport-overlay-v1.schema.json",
|
|
3
|
+
"component": "RadioGroup",
|
|
4
|
+
"purpose": "One choice out of a few, on native radios: a single tab stop, arrow keys between the choices, and a card variant for richer options.",
|
|
5
|
+
"intentDomains": [
|
|
6
|
+
"ActionIntent"
|
|
7
|
+
],
|
|
8
|
+
"$operationsNote": "Mirrors shared/core/agent/field.ts, which is what the runtime dispatches on. source/components/shared/core/agent/__tests__/field-passport.test.ts fails if the two drift. No `apiMethod`: a field is written through its ref — the element for most of these, a handle for Combobox and DatePicker — and that is a normal case, not a gap.",
|
|
9
|
+
"operations": {
|
|
10
|
+
"setValue": {
|
|
11
|
+
"summary": "Write a value into the field, as though a person had entered it.",
|
|
12
|
+
"effects": [
|
|
13
|
+
"replaces the field's current value",
|
|
14
|
+
"fires the host's change handler"
|
|
15
|
+
],
|
|
16
|
+
"reversible": true
|
|
17
|
+
},
|
|
18
|
+
"clear": {
|
|
19
|
+
"summary": "Empty the field.",
|
|
20
|
+
"effects": [
|
|
21
|
+
"discards the field's current value",
|
|
22
|
+
"fires the host's change handler"
|
|
23
|
+
],
|
|
24
|
+
"reversible": true
|
|
25
|
+
},
|
|
26
|
+
"focus": {
|
|
27
|
+
"summary": "Move keyboard focus to the field.",
|
|
28
|
+
"effects": [
|
|
29
|
+
"moves focus"
|
|
30
|
+
],
|
|
31
|
+
"reversible": false
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"accessibility": {
|
|
35
|
+
"supplied": [
|
|
36
|
+
"native radio semantics: one tab stop for the group, arrow keys to move and select",
|
|
37
|
+
"a fieldset with the group label as its legend",
|
|
38
|
+
"description and error linked by aria-describedby"
|
|
39
|
+
],
|
|
40
|
+
"required": [
|
|
41
|
+
"a `label` for the group — a set of radios with no question is meaningless",
|
|
42
|
+
"`name`, so the browser treats them as one group"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"composition": {
|
|
46
|
+
"allowedChildren": [
|
|
47
|
+
"Radio",
|
|
48
|
+
"RadioCard"
|
|
49
|
+
],
|
|
50
|
+
"notes": "Children or `options`, not both. A radio outside a RadioGroup loses the arrow-key behaviour."
|
|
51
|
+
},
|
|
52
|
+
"safeMutations": {
|
|
53
|
+
"allowed": [
|
|
54
|
+
"size",
|
|
55
|
+
"orientation",
|
|
56
|
+
"variant",
|
|
57
|
+
"description"
|
|
58
|
+
],
|
|
59
|
+
"forbidden": [
|
|
60
|
+
"value",
|
|
61
|
+
"defaultValue",
|
|
62
|
+
"name",
|
|
63
|
+
"options",
|
|
64
|
+
"required",
|
|
65
|
+
"disabled",
|
|
66
|
+
"readOnly",
|
|
67
|
+
"onChange",
|
|
68
|
+
"onValueChange",
|
|
69
|
+
"error"
|
|
70
|
+
],
|
|
71
|
+
"rationale": "The selected value is the user's answer and `options` is the question."
|
|
72
|
+
},
|
|
73
|
+
"propNotes": {
|
|
74
|
+
"variant": "`card` renders each option as a selectable surface; the semantics are identical."
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The loop: an intent arrives, the validator rules on it, and only then does
|
|
3
|
+
* an executor run.
|
|
4
|
+
*
|
|
5
|
+
* There is nothing here but that order, which is the whole point. Every route
|
|
6
|
+
* into a field — a model's proposal, a WebMCP tool call, a test — goes through
|
|
7
|
+
* this function, so there is one place where "is this allowed" is decided and
|
|
8
|
+
* no second path that skips it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { fieldOperations, validateFieldIntent } from "./field";
|
|
12
|
+
import type { FieldCommand, FieldContract, FieldIntent } from "./field";
|
|
13
|
+
import type { ExecutorRegistry, ValidationIssue } from "./types";
|
|
14
|
+
|
|
15
|
+
export interface FieldExecution {
|
|
16
|
+
readonly ok: boolean;
|
|
17
|
+
/** What was applied, once the validator agreed to it. */
|
|
18
|
+
readonly command?: FieldCommand;
|
|
19
|
+
/** Coercions applied and plausibility notes. Never a reason for refusal. */
|
|
20
|
+
readonly warnings: readonly ValidationIssue[];
|
|
21
|
+
/** Present when refused: the reason, in the words the person should see. */
|
|
22
|
+
readonly reason?: string;
|
|
23
|
+
readonly code?: string;
|
|
24
|
+
readonly layer?: ValidationIssue["layer"];
|
|
25
|
+
readonly suggestion?: string;
|
|
26
|
+
/** Whatever the executor returned, for a caller that wants it. */
|
|
27
|
+
readonly result?: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FieldAgentOptions {
|
|
31
|
+
readonly contract: FieldContract;
|
|
32
|
+
readonly executors: ExecutorRegistry<FieldContract>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface FieldAgent {
|
|
36
|
+
/** What this instance can do, with `setValue` narrowed to its type. */
|
|
37
|
+
readonly operations: ReturnType<typeof fieldOperations>;
|
|
38
|
+
readonly contract: FieldContract;
|
|
39
|
+
/** Validate and, if it passes, perform. */
|
|
40
|
+
run(intent: FieldIntent, options?: { dryRun?: boolean; signal?: AbortSignal }): Promise<FieldExecution>;
|
|
41
|
+
/** Validate only. Useful for a confirmation prompt before anything happens. */
|
|
42
|
+
check(intent: FieldIntent): FieldExecution;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createFieldAgent({ contract, executors }: FieldAgentOptions): FieldAgent {
|
|
46
|
+
const check = (intent: FieldIntent): FieldExecution => {
|
|
47
|
+
const verdict = validateFieldIntent(intent, contract);
|
|
48
|
+
if (!verdict.ok) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
warnings: [],
|
|
52
|
+
reason: verdict.reason,
|
|
53
|
+
code: verdict.code,
|
|
54
|
+
layer: verdict.layer,
|
|
55
|
+
...(verdict.suggestion ? { suggestion: verdict.suggestion } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return { ok: true, command: verdict.command, warnings: verdict.warnings };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
contract,
|
|
63
|
+
operations: fieldOperations(contract),
|
|
64
|
+
check,
|
|
65
|
+
async run(intent, options = {}) {
|
|
66
|
+
const verdict = check(intent);
|
|
67
|
+
if (!verdict.ok) return verdict;
|
|
68
|
+
|
|
69
|
+
const executor = executors.get(verdict.command!.action);
|
|
70
|
+
if (!executor) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
warnings: verdict.warnings,
|
|
74
|
+
reason: `${contract.component} declares "${verdict.command!.action}" but nothing performs it.`,
|
|
75
|
+
code: "no-executor",
|
|
76
|
+
layer: "reference",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const result = await executor.execute(verdict.command as never, {
|
|
82
|
+
contract,
|
|
83
|
+
dryRun: options.dryRun ?? false,
|
|
84
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
85
|
+
});
|
|
86
|
+
return { ...verdict, result };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// An executor throwing is a failure to *reach* the field, which is a
|
|
89
|
+
// different thing from the field refusing — and is reported as such,
|
|
90
|
+
// so "it is disabled" never reads as "your wording was wrong".
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
warnings: verdict.warnings,
|
|
94
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
95
|
+
code: "execution-failed",
|
|
96
|
+
layer: "policy",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* How a field actually performs what the validator allowed.
|
|
3
|
+
*
|
|
4
|
+
* Two kinds of field exist in this library and the difference is not cosmetic:
|
|
5
|
+
*
|
|
6
|
+
* - Input, Textarea, NumberInput, Checkbox, Switch and Radio hand you the
|
|
7
|
+
* DOM element as their ref. They are written through the element.
|
|
8
|
+
* - Combobox and DatePicker hand you a handle object, because their value is
|
|
9
|
+
* not a string in an input — it is a selection, or a date. They are
|
|
10
|
+
* written through that handle.
|
|
11
|
+
*
|
|
12
|
+
* Both end up satisfying the same `OperationExecutor`, which is the point of
|
|
13
|
+
* the executor being a separate thing from the operation. Everything above
|
|
14
|
+
* this file — the contract, the schemas, the validator, the refusals — is
|
|
15
|
+
* identical for both, and neither knows which it is driving.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { writeChecked, writeValue, writable } from "./write";
|
|
19
|
+
import type { FieldCommand, FieldContract, FieldOperationName } from "./field";
|
|
20
|
+
import type { ExecutionContext, RegisteredExecutor } from "./types";
|
|
21
|
+
|
|
22
|
+
export type FieldExecutionContext = ExecutionContext<FieldContract>;
|
|
23
|
+
|
|
24
|
+
/** What a component with a real handle must provide to be driven. */
|
|
25
|
+
export interface FieldHandleTarget {
|
|
26
|
+
setValue(value: unknown): void;
|
|
27
|
+
clear?(): void;
|
|
28
|
+
focus?(): void;
|
|
29
|
+
open?(): void;
|
|
30
|
+
close?(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Registry = Map<string, RegisteredExecutor<FieldContract>>;
|
|
34
|
+
|
|
35
|
+
const define = (
|
|
36
|
+
registry: Registry,
|
|
37
|
+
operation: FieldOperationName,
|
|
38
|
+
execute: (input: FieldCommand, context: FieldExecutionContext) => unknown,
|
|
39
|
+
) => {
|
|
40
|
+
registry.set(operation, { operation, execute } as RegisteredExecutor<FieldContract>);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Executors for a field whose ref is the DOM element.
|
|
45
|
+
*
|
|
46
|
+
* `getElement` is a function rather than the element itself because the ref is
|
|
47
|
+
* not populated on the first render, and an executor registry built once must
|
|
48
|
+
* still work on the tenth.
|
|
49
|
+
*/
|
|
50
|
+
export function domFieldExecutors(
|
|
51
|
+
getElement: () => HTMLInputElement | HTMLTextAreaElement | null,
|
|
52
|
+
options: { readonly checkable?: boolean } = {},
|
|
53
|
+
): Registry {
|
|
54
|
+
const registry: Registry = new Map();
|
|
55
|
+
|
|
56
|
+
const element = () => {
|
|
57
|
+
const found = getElement();
|
|
58
|
+
if (!found) throw new Error("The field is not mounted.");
|
|
59
|
+
return found;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
define(registry, "setValue", (command, context) => {
|
|
63
|
+
const target = element();
|
|
64
|
+
if (context.dryRun) return { wouldWrite: command.value };
|
|
65
|
+
if (!writable(target)) throw new Error("The field is disabled or read-only.");
|
|
66
|
+
if (options.checkable) writeChecked(target as HTMLInputElement, Boolean(command.value));
|
|
67
|
+
else writeValue(target, String(command.value ?? ""));
|
|
68
|
+
return { wrote: command.value };
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
define(registry, "clear", (_command, context) => {
|
|
72
|
+
const target = element();
|
|
73
|
+
if (context.dryRun) return { wouldClear: true };
|
|
74
|
+
if (!writable(target)) throw new Error("The field is disabled or read-only.");
|
|
75
|
+
if (options.checkable) writeChecked(target as HTMLInputElement, false);
|
|
76
|
+
else writeValue(target, "");
|
|
77
|
+
return { cleared: true };
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
define(registry, "focus", (_command, context) => {
|
|
81
|
+
if (context.dryRun) return { wouldFocus: true };
|
|
82
|
+
element().focus();
|
|
83
|
+
return { focused: true };
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return registry;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Executors for a field that exposes a handle object rather than an element. */
|
|
90
|
+
export function handleFieldExecutors(getHandle: () => FieldHandleTarget | null): Registry {
|
|
91
|
+
const registry: Registry = new Map();
|
|
92
|
+
|
|
93
|
+
const handle = () => {
|
|
94
|
+
const found = getHandle();
|
|
95
|
+
if (!found) throw new Error("The field is not mounted.");
|
|
96
|
+
return found;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
define(registry, "setValue", (command, context) => {
|
|
100
|
+
const target = handle();
|
|
101
|
+
if (context.dryRun) return { wouldWrite: command.value };
|
|
102
|
+
target.setValue(command.value);
|
|
103
|
+
return { wrote: command.value };
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
define(registry, "clear", (_command, context) => {
|
|
107
|
+
const target = handle();
|
|
108
|
+
if (context.dryRun) return { wouldClear: true };
|
|
109
|
+
// A handle without `clear` still clears: writing null is the same thing.
|
|
110
|
+
if (target.clear) target.clear();
|
|
111
|
+
else target.setValue(null);
|
|
112
|
+
return { cleared: true };
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
define(registry, "focus", (_command, context) => {
|
|
116
|
+
const target = handle();
|
|
117
|
+
if (context.dryRun) return { wouldFocus: true };
|
|
118
|
+
target.focus?.();
|
|
119
|
+
return { focused: true };
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
define(registry, "open", (_command, context) => {
|
|
123
|
+
const target = handle();
|
|
124
|
+
if (context.dryRun) return { wouldOpen: true };
|
|
125
|
+
target.open?.();
|
|
126
|
+
return { opened: true };
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
define(registry, "close", (_command, context) => {
|
|
130
|
+
const target = handle();
|
|
131
|
+
if (context.dryRun) return { wouldClose: true };
|
|
132
|
+
target.close?.();
|
|
133
|
+
return { closed: true };
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return registry;
|
|
137
|
+
}
|