@table-bro/mcp 0.0.0-stage → 1.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/LICENSE +21 -0
- package/README.md +97 -2
- package/dist/docs/docs.d.ts +21 -0
- package/dist/docs/docs.js +103 -0
- package/dist/docs/pages.d.ts +28 -0
- package/dist/docs/pages.js +2734 -0
- package/dist/docs/topics.d.ts +11 -0
- package/dist/docs/topics.js +410 -0
- package/dist/extension.d.ts +23 -0
- package/dist/extension.js +1 -0
- package/dist/generate/angular.d.ts +26 -0
- package/dist/generate/angular.js +241 -0
- package/dist/generate/cells.d.ts +22 -0
- package/dist/generate/cells.js +157 -0
- package/dist/generate/code.d.ts +48 -0
- package/dist/generate/code.js +153 -0
- package/dist/generate/config.d.ts +16 -0
- package/dist/generate/config.js +95 -0
- package/dist/generate/fillings.d.ts +49 -0
- package/dist/generate/fillings.js +130 -0
- package/dist/generate/filters.d.ts +36 -0
- package/dist/generate/filters.js +254 -0
- package/dist/generate/generate-code.d.ts +34 -0
- package/dist/generate/generate-code.js +59 -0
- package/dist/generate/names.d.ts +52 -0
- package/dist/generate/names.js +93 -0
- package/dist/generate/rows.d.ts +5 -0
- package/dist/generate/rows.js +22 -0
- package/dist/generate/scaffold.d.ts +35 -0
- package/dist/generate/scaffold.js +62 -0
- package/dist/generate/service.d.ts +16 -0
- package/dist/generate/service.js +320 -0
- package/dist/generate/setup.d.ts +34 -0
- package/dist/generate/setup.js +158 -0
- package/dist/generate/vanilla.d.ts +10 -0
- package/dist/generate/vanilla.js +160 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +10 -0
- package/dist/project/inspect-project.d.ts +53 -0
- package/dist/project/inspect-project.js +223 -0
- package/dist/server/main.d.ts +2 -0
- package/dist/server/main.js +8 -0
- package/dist/server/server.d.ts +17 -0
- package/dist/server/server.js +212 -0
- package/dist/spec/builtin-specs.d.ts +13 -0
- package/dist/spec/builtin-specs.js +167 -0
- package/dist/spec/css-variables.d.ts +22 -0
- package/dist/spec/css-variables.js +54 -0
- package/dist/spec/schema.d.ts +19 -0
- package/dist/spec/schema.js +278 -0
- package/dist/spec/skin-config.d.ts +30 -0
- package/dist/spec/skin-config.js +67 -0
- package/dist/spec/spec.d.ts +157 -0
- package/dist/spec/spec.js +2 -0
- package/dist/spec/validate-spec.d.ts +54 -0
- package/dist/spec/validate-spec.js +979 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +71 -4
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import * as z from 'zod/v4';
|
|
4
|
+
import { allTopics, getDocs, getStarted, serverInstructions } from '../docs/docs.js';
|
|
5
|
+
import { generateCode } from '../generate/generate-code.js';
|
|
6
|
+
import { scaffoldComponent } from '../generate/scaffold.js';
|
|
7
|
+
import { setupText } from '../generate/setup.js';
|
|
8
|
+
import { findExtensions, inspectProject } from '../project/inspect-project.js';
|
|
9
|
+
import { builtinSpecs } from '../spec/builtin-specs.js';
|
|
10
|
+
import { cellFillingSchema, fillingsSchema, filterSchema, specSchema } from '../spec/schema.js';
|
|
11
|
+
import { rowDetailsByDefault, validateSpec } from '../spec/validate-spec.js';
|
|
12
|
+
import { tableBroVersion } from '../version.js';
|
|
13
|
+
const skin = z.enum(['mat', 'bs', 'nv']).describe('The skin, the UI library the table works on top of: mat (Angular Material), bs (Bootstrap 5) or nv (native HTML, no library)');
|
|
14
|
+
const target = z.enum(['angular', 'vanilla']).describe('The target: angular for an Angular project, through the wrapper <table-bro-ng>; vanilla for a page without a framework, the Custom Element <table-bro> itself');
|
|
15
|
+
/**
|
|
16
|
+
* A value that the tool checks itself, shown to the agent with its JSON Schema: the check of the tool names every
|
|
17
|
+
* error with its path and its fix, as did you mean, which a check of the arguments by the schema would not.
|
|
18
|
+
*/
|
|
19
|
+
function shown(schema) {
|
|
20
|
+
// The dialect of the schema stays out: the list of the tools gives its own
|
|
21
|
+
return z.any().meta(Object.fromEntries(Object.entries(schema).filter(([key]) => key !== '$schema')));
|
|
22
|
+
}
|
|
23
|
+
const issue = z.object({ path: z.string(), message: z.string(), fix: z.string().optional() });
|
|
24
|
+
const report = { valid: z.boolean(), errors: z.array(issue), warnings: z.array(issue), omitted: z.object({ errors: z.number().optional(), warnings: z.number().optional() }).optional() };
|
|
25
|
+
/** The sizes of a spec that the server takes: the answers of a larger one would fill the context of the agent. */
|
|
26
|
+
const limits = { cols: 200, rows: 1000, cells: 100, filters: 100, valueLength: 2000 };
|
|
27
|
+
const profile = {
|
|
28
|
+
folder: z.string(),
|
|
29
|
+
tableBro: z.object({ version: z.string().nullable(), packages: z.record(z.string(), z.string()), extensions: z.array(z.string()) }),
|
|
30
|
+
framework: z.object({ name: z.enum(['angular', 'none']), version: z.string().nullable() }),
|
|
31
|
+
ui: z.object({ material: z.string().nullable(), bootstrap: z.string().nullable() }),
|
|
32
|
+
skins: z.object({ registered: z.array(z.enum(['mat', 'bs', 'nv'])), where: z.array(z.string()) }),
|
|
33
|
+
styles: z.array(z.string()),
|
|
34
|
+
customElements: z.array(z.object({ tag: z.string(), file: z.string() })),
|
|
35
|
+
tables: z.array(z.string()),
|
|
36
|
+
problems: z.array(z.object({ code: z.string(), message: z.string(), fix: z.string() })),
|
|
37
|
+
omitted: z.record(z.string(), z.number()).optional()
|
|
38
|
+
};
|
|
39
|
+
/** Every tool only reads, and the same call gives the same answer: the agent writes the files. */
|
|
40
|
+
const readOnly = { readOnlyHint: true, idempotentHint: true, openWorldHint: false };
|
|
41
|
+
/**
|
|
42
|
+
* The first folder of the workspace of the client, when the client gives its roots: a client may start the server
|
|
43
|
+
* outside the project, as a desktop application does.
|
|
44
|
+
*/
|
|
45
|
+
async function clientRoot(server) {
|
|
46
|
+
if (!server.server.getClientCapabilities()?.roots) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const { roots } = await server.server.listRoots();
|
|
51
|
+
const folder = roots.find(item => item.uri.startsWith('file:'));
|
|
52
|
+
return folder ? fileURLToPath(folder.uri) : undefined;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** The paid modules that the arguments of the server name: --extension <package>, once for each module. */
|
|
59
|
+
export function enabledExtensions(args) {
|
|
60
|
+
return args.flatMap((arg, index) => {
|
|
61
|
+
if (arg === '--extension') {
|
|
62
|
+
return args[index + 1] ? [args[index + 1]] : [];
|
|
63
|
+
}
|
|
64
|
+
return arg.startsWith('--extension=') ? [arg.slice('--extension='.length)] : [];
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Loads the parts of the server of the installed paid modules that the server is started with, from the scope
|
|
69
|
+
* @table-bro-extended only: the server runs no code of a package that nobody named. A module that fails to load, or has
|
|
70
|
+
* no part of the server, is left out with a message on stderr, and the server works without it.
|
|
71
|
+
* @param root - the folder of the project
|
|
72
|
+
* @param enabled - the packages of the modules that the arguments of the server name
|
|
73
|
+
*/
|
|
74
|
+
export async function loadExtensions(root, enabled) {
|
|
75
|
+
const extensions = [];
|
|
76
|
+
const installed = findExtensions(root);
|
|
77
|
+
for (const name of enabled) {
|
|
78
|
+
const found = installed.find(extension => extension.name === name);
|
|
79
|
+
if (!found) {
|
|
80
|
+
console.error(`[@table-bro/mcp] ${name} is not loaded: no package of the scope @table-bro-extended with a part of the MCP server has this name in ${root}`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const extension = (await import(pathToFileURL(found.entry).href)).default;
|
|
85
|
+
if (typeof extension?.package !== 'string') {
|
|
86
|
+
throw new Error('its file has no part of the MCP server as its default export');
|
|
87
|
+
}
|
|
88
|
+
extensions.push(extension);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error(`[@table-bro/mcp] ${name} is not loaded: ${error.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return extensions;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The MCP server of Table, Bro: six tools over the pure functions of the package. It reads the project and writes
|
|
98
|
+
* nothing: the agent writes the files and shows the changes to the developer.
|
|
99
|
+
*/
|
|
100
|
+
export function createServer(root, extensions = []) {
|
|
101
|
+
const server = new McpServer({ name: 'table-bro', version: tableBroVersion }, { instructions: serverInstructions });
|
|
102
|
+
server.registerTool('get_started', {
|
|
103
|
+
title: 'Get started with Table, Bro',
|
|
104
|
+
description: 'Call this first, once per session. It explains how Table, Bro works, a Web Components table engine on top of the UI library of your project, and which tool to use when; '
|
|
105
|
+
+ 'shows a minimal table for your target and skin; lists the topics of get_docs. Do not call it again in the same session: use get_docs. Example: {"target": "angular", "skin": "mat"}',
|
|
106
|
+
inputSchema: { target: target.optional(), skin: skin.optional() },
|
|
107
|
+
annotations: readOnly
|
|
108
|
+
}, ({ target, skin }) => text(getStarted({ target, skin }, extensions)));
|
|
109
|
+
server.registerTool('get_docs', {
|
|
110
|
+
title: 'The reference of Table, Bro',
|
|
111
|
+
description: 'Returns one topic of the reference or one page of Docs with its code by its name, the two topics that match a query best, or all topics with their summaries. '
|
|
112
|
+
+ 'Use it for questions about the API and for changes of an existing table; get_started already gives the topics basics and workflow. '
|
|
113
|
+
+ 'Example: {"topic": "server-mode"} or {"query": "row height"}',
|
|
114
|
+
inputSchema: {
|
|
115
|
+
topic: z.enum(allTopics(extensions).map(topic => topic.name)).optional(),
|
|
116
|
+
query: z.string().optional().describe('The words to search the topics for, as row height; without topic')
|
|
117
|
+
},
|
|
118
|
+
annotations: readOnly
|
|
119
|
+
}, ({ topic, query }) => text(getDocs({ topic, query }, extensions)));
|
|
120
|
+
server.registerTool('inspect_project', {
|
|
121
|
+
title: 'Inspect the project',
|
|
122
|
+
description: 'Profiles the project: the packages of Table, Bro and their version, the framework, the registered skins, the CSS, the custom elements, the tables, '
|
|
123
|
+
+ 'and the problems of the setup with their fixes. Call it once before you add code to a project, and when a table does not render. '
|
|
124
|
+
+ 'Pass skins.registered as the skin and framework.name as the target of the other tools. Not for questions about the API. '
|
|
125
|
+
+ 'Each list keeps its first 20 items; omitted counts the rest. It reads the project as text and writes nothing. Example: {}',
|
|
126
|
+
inputSchema: { path: z.string().optional().describe('The root of the project. Without it, the first root of the workspace that the client gives, else the folder where the server was started') },
|
|
127
|
+
outputSchema: profile,
|
|
128
|
+
annotations: readOnly
|
|
129
|
+
}, async ({ path }) => structured(inspectProject(path ?? await clientRoot(server) ?? root)));
|
|
130
|
+
server.registerTool('validate_spec', {
|
|
131
|
+
title: 'Check a table spec',
|
|
132
|
+
description: 'Checks a table spec, a BroTableSpec (get_docs, topic spec), and reports every error and warning with its path, what is wrong and the fix. '
|
|
133
|
+
+ 'Use it while you draft a spec. generate_code runs the same check first and returns the same report for a spec with errors. '
|
|
134
|
+
+ 'Example: {"spec": {"specVersion": 1, "rowType": "Product", "fields": [{"name": "title", "type": "string"}], "cols": [{"title": "Product", "name": "title"}]}, "skin": "mat"}',
|
|
135
|
+
inputSchema: { spec: shown(specSchema), skin: skin.optional().describe('The skin of the project: mat, bs or nv. Without it the spec is checked against all three') },
|
|
136
|
+
outputSchema: report,
|
|
137
|
+
annotations: readOnly
|
|
138
|
+
}, ({ spec, skin }) => {
|
|
139
|
+
const result = validateSpec(spec, { skin, extensions, limits });
|
|
140
|
+
// Every table has row details by default: a warning, as the request may ask for a table without them
|
|
141
|
+
result.warnings.push(...rowDetailsByDefault(spec));
|
|
142
|
+
return structured(result);
|
|
143
|
+
});
|
|
144
|
+
server.registerTool('generate_code', {
|
|
145
|
+
title: 'Generate the code of a table',
|
|
146
|
+
description: 'Generates the code of a new table from a spec or from a built-in spec: the files, the setup of the project, once per project, and what is left to finish by hand. '
|
|
147
|
+
+ 'The fillings are the templates of the cells, the rules of the custom filters and the bodies of the functions of the skin config (get_docs, topic fillings); '
|
|
148
|
+
+ 'without them the files keep TODOs. A spec with errors returns the report of validate_spec and no files. '
|
|
149
|
+
+ 'For one cell, the row details or the filters of an existing table use scaffold_component. '
|
|
150
|
+
+ 'Example: {"builtinSpec": "products", "target": "angular", "skin": "mat"}',
|
|
151
|
+
inputSchema: {
|
|
152
|
+
// The list of the tools goes into the context of the agent in every session: the schema of a spec stays in validate_spec
|
|
153
|
+
spec: z.any().optional().describe('A BroTableSpec, as the JSON Schema of the spec of validate_spec describes it'),
|
|
154
|
+
builtinSpec: z.enum(builtinSpecs.map(builtin => builtin.name)).optional().describe(`The name of a built-in spec instead of a spec: ${builtinSpecs.map(builtin => `${builtin.name}, ${builtin.description}`).join('; ')}`),
|
|
155
|
+
fillings: shown(fillingsSchema).optional(),
|
|
156
|
+
target,
|
|
157
|
+
skin
|
|
158
|
+
},
|
|
159
|
+
annotations: readOnly
|
|
160
|
+
}, ({ spec, builtinSpec, fillings, target, skin }) => {
|
|
161
|
+
const builtin = builtinSpecs.find(item => item.name === builtinSpec);
|
|
162
|
+
if (!spec && !builtin) {
|
|
163
|
+
return { ...text('Give a spec or the name of a built-in spec.'), isError: true };
|
|
164
|
+
}
|
|
165
|
+
const result = generateCode((spec ?? builtin.spec), { target, skin, fillings: fillings ?? builtin?.fillings, extensions, limits });
|
|
166
|
+
if (!result.report.valid) {
|
|
167
|
+
return { ...text(`The spec has errors:\n\n${JSON.stringify(result.report, null, 2)}`), isError: true };
|
|
168
|
+
}
|
|
169
|
+
result.report.warnings.push(...rowDetailsByDefault(spec ?? builtin.spec));
|
|
170
|
+
return text([
|
|
171
|
+
...result.files.map(file => `## ${file.path}\n\n${file.description ? `${file.description[0].toUpperCase()}${file.description.slice(1)}\n\n` : ''}\`\`\`${file.path.endsWith('.css') ? 'css' : 'ts'}\n${file.content}\`\`\``),
|
|
172
|
+
`## Setup, once per project\n\n${setupText(result.setup)}`,
|
|
173
|
+
...(result.todo.length ? [`## To finish\n\n${result.todo.map(item => `- ${item}`).join('\n')}`] : []),
|
|
174
|
+
...(result.report.warnings.length ? [`## Warnings\n\n${result.report.warnings.map(item => `- ${item.path}: ${item.message}${item.fix ? `; ${item.fix}` : ''}`).join('\n')}`] : [])
|
|
175
|
+
].join('\n\n'));
|
|
176
|
+
});
|
|
177
|
+
server.registerTool('scaffold_component', {
|
|
178
|
+
title: 'Scaffold a component of a table',
|
|
179
|
+
description: 'Scaffolds one component for an existing table: a custom cell, the element of the row details, or the filters of the Filter Drawer, and tells how to connect it to the page: '
|
|
180
|
+
+ 'the registration of the element and its column, or the container of the drawer. '
|
|
181
|
+
+ 'For a new table use generate_code, which writes its cells too. '
|
|
182
|
+
+ 'Example: {"kind": "cell", "target": "angular", "skin": "mat", "rowType": "Order", "fields": [{"name": "status", "type": "string"}], "name": "status", "shows": "the status as a chip", "reads": ["status"]}',
|
|
183
|
+
inputSchema: {
|
|
184
|
+
kind: z.enum(['cell', 'details', 'filters']).describe('What to scaffold: cell, a custom cell of a column; details, the element of the row details; filters, the filters of the Filter Drawer'),
|
|
185
|
+
target,
|
|
186
|
+
skin,
|
|
187
|
+
rowType: z.string().describe('The name of the type of a row in PascalCase, as Order'),
|
|
188
|
+
fields: z.array(z.object({ name: z.string(), type: z.enum(['string', 'number', 'boolean', 'string[]']), values: z.array(z.string()).optional() })).describe('The fields of the row: name, type (string, number, boolean or string[]) and, for a string field, its values'),
|
|
189
|
+
name: z.string().optional().describe('For a cell or the details: its name in kebab-case, as status; the tag of the element is bro-cell-<row-type>-<name>'),
|
|
190
|
+
shows: z.string().optional().describe('For a cell or the details: what it shows, in words, as the status as a chip'),
|
|
191
|
+
reads: z.array(z.string()).optional().describe('For a cell or the details: the names of the fields of the row that it reads'),
|
|
192
|
+
actions: z.array(z.string()).optional().describe('For a cell: the actions that it sends in its output action, as edit; the table gives them to the page as bro-action with the column and the row'),
|
|
193
|
+
filters: shown({ type: 'array', description: 'For filters: the filters of the Filter Drawer, in the form of filters of a spec; get_docs, topic filters', items: filterSchema }).optional(),
|
|
194
|
+
filling: shown({ ...cellFillingSchema, description: 'For a cell or the details, the code that you write: {imports, template, styles, members} for an Angular project, {render, members} for Vanilla JS; get_docs, topic fillings' }).optional()
|
|
195
|
+
},
|
|
196
|
+
annotations: readOnly
|
|
197
|
+
}, request => {
|
|
198
|
+
const scaffold = scaffoldComponent(request);
|
|
199
|
+
if (!scaffold.report.valid) {
|
|
200
|
+
return { ...text(`The request has errors:\n\n${JSON.stringify(scaffold.report, null, 2)}`), isError: true };
|
|
201
|
+
}
|
|
202
|
+
return text(`## ${scaffold.path}\n\n\`\`\`ts\n${scaffold.content}\`\`\`\n\n${scaffold.connect}`);
|
|
203
|
+
});
|
|
204
|
+
return server;
|
|
205
|
+
}
|
|
206
|
+
function text(value) {
|
|
207
|
+
return { content: [{ type: 'text', text: value }] };
|
|
208
|
+
}
|
|
209
|
+
/** A result of the form of the output schema of its tool, also as JSON text for a client without structured content. */
|
|
210
|
+
function structured(value) {
|
|
211
|
+
return { ...text(JSON.stringify(value, null, 2)), structuredContent: value };
|
|
212
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BroSpecFillings } from '../generate/fillings.js';
|
|
2
|
+
import type { BroTableSpec } from './spec.js';
|
|
3
|
+
export interface BroBuiltinSpec {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
spec: BroTableSpec;
|
|
7
|
+
fillings: BroSpecFillings;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The built-in specs of the reference: generate_code gives the code of a built-in spec by its name, and the check
|
|
11
|
+
* of the generator runs every built-in spec through every pair of a target and a skin.
|
|
12
|
+
*/
|
|
13
|
+
export declare const builtinSpecs: BroBuiltinSpec[];
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in specs of the reference: generate_code gives the code of a built-in spec by its name, and the check
|
|
3
|
+
* of the generator runs every built-in spec through every pair of a target and a skin.
|
|
4
|
+
*/
|
|
5
|
+
export const builtinSpecs = [
|
|
6
|
+
{
|
|
7
|
+
name: 'orders',
|
|
8
|
+
description: 'Orders with custom cells and their actions, row details, filters in the Filter Drawer, buttons of the toolbar, a message of a state and a taller row',
|
|
9
|
+
spec: {
|
|
10
|
+
specVersion: 1,
|
|
11
|
+
rowType: 'Order',
|
|
12
|
+
fields: [
|
|
13
|
+
{ name: 'number', type: 'string' },
|
|
14
|
+
{ name: 'customer', type: 'string' },
|
|
15
|
+
{ name: 'total', type: 'number' },
|
|
16
|
+
{ name: 'status', type: 'string', values: ['paid', 'pending', 'refunded'] },
|
|
17
|
+
{ name: 'tags', type: 'string[]' },
|
|
18
|
+
{ name: 'urgent', type: 'boolean' }
|
|
19
|
+
],
|
|
20
|
+
cols: [
|
|
21
|
+
{ title: 'Order', name: 'number' },
|
|
22
|
+
{ title: 'Customer', name: 'customer', component: { cell: 'customer' } },
|
|
23
|
+
{ title: 'Total amount', shortTitle: 'Total', name: 'total' },
|
|
24
|
+
{ title: 'Status', name: 'status', component: { cell: 'status' } },
|
|
25
|
+
{ title: 'Tags', name: 'tags', component: { element: 'tags' } },
|
|
26
|
+
{ title: 'Urgent', name: 'urgent', component: { element: 'switch' } },
|
|
27
|
+
{ title: '', component: { cell: 'actions' } },
|
|
28
|
+
{ title: '', component: { element: 'details-button' } }
|
|
29
|
+
],
|
|
30
|
+
cells: [
|
|
31
|
+
{ name: 'customer', shows: 'the customer and the order number below it in a smaller font', fields: ['customer', 'number'] },
|
|
32
|
+
{ name: 'status', shows: 'the status as a badge of the library of the skin, paid stands out', fields: ['status'] },
|
|
33
|
+
{ name: 'actions', shows: 'Edit and Delete icon buttons', fields: ['number'], actions: ['edit', 'delete'] },
|
|
34
|
+
{ name: 'details', kind: 'details', shows: 'the customer, the total and the tags of the order', fields: ['customer', 'total', 'tags'] }
|
|
35
|
+
],
|
|
36
|
+
filters: [
|
|
37
|
+
{ name: 'customer', label: 'Customer', control: 'text', field: 'customer' },
|
|
38
|
+
{ name: 'status', label: 'Status', control: 'select', field: 'status' },
|
|
39
|
+
{ name: 'total', label: 'Total', control: 'range', field: 'total' },
|
|
40
|
+
{ name: 'urgent', label: 'Urgent only', control: 'checkbox', field: 'urgent' },
|
|
41
|
+
{ name: 'big', label: 'Big orders', control: 'checkbox', match: 'custom', rule: 'the total is over 1000' }
|
|
42
|
+
],
|
|
43
|
+
rows: [
|
|
44
|
+
{ number: 'A-1042', customer: 'Ada Lovelace', total: 1200, status: 'paid', tags: ['new', 'priority'], urgent: true },
|
|
45
|
+
{ number: 'A-1043', customer: 'Alan Turing', total: 310, status: 'pending', tags: ['new'], urgent: false },
|
|
46
|
+
{ number: 'A-1044', customer: 'Grace Hopper', total: 75, status: 'paid', tags: [], urgent: false },
|
|
47
|
+
{ number: 'A-1045', customer: 'Edsger Dijkstra', total: 990, status: 'refunded', tags: ['return'], urgent: true },
|
|
48
|
+
{ number: 'A-1046', customer: 'Barbara Liskov', total: 1450, status: 'pending', tags: ['priority'], urgent: false }
|
|
49
|
+
],
|
|
50
|
+
config: { toolbarTitle: 'Orders', pageSize: 10, rowDetailsComponent: { cell: 'details' } },
|
|
51
|
+
toolbar: {
|
|
52
|
+
buttons: [{ title: 'Export', action: 'export', icon: 'download' }],
|
|
53
|
+
menu: { title: 'More', items: [{ title: 'Archive', action: 'archive' }] }
|
|
54
|
+
},
|
|
55
|
+
stateMessages: { 'no-data': 'No orders yet', 'not-found': 'No orders match the filters' },
|
|
56
|
+
cssVars: { '--bro-row-height': '56px' }
|
|
57
|
+
},
|
|
58
|
+
fillings: {
|
|
59
|
+
cells: {
|
|
60
|
+
customer: {
|
|
61
|
+
mat: {
|
|
62
|
+
template: '<span class="customer">{{ row.customer }}</span>\n<small class="number">{{ row.number }}</small>',
|
|
63
|
+
styles: ':host {\n display: flex;\n flex-direction: column;\n line-height: 1.2;\n}\n\n.number {\n font-size: 12px;\n opacity: .7;\n}'
|
|
64
|
+
},
|
|
65
|
+
bs: {
|
|
66
|
+
template: '<span>{{ row.customer }}</span>\n<small class="text-body-secondary">{{ row.number }}</small>',
|
|
67
|
+
styles: ':host {\n display: flex;\n flex-direction: column;\n line-height: 1.2;\n}',
|
|
68
|
+
render: "const customer = document.createElement('div');\nconst number = document.createElement('small');\n\ncustomer.textContent = this.row?.customer ?? '';\nnumber.className = 'text-body-secondary';\nnumber.textContent = this.row?.number ?? '';\n\nthis.style.lineHeight = '1.2';\nthis.append(customer, number);"
|
|
69
|
+
},
|
|
70
|
+
nv: {
|
|
71
|
+
template: '<span>{{ row.customer }}</span>\n<small class="number">{{ row.number }}</small>',
|
|
72
|
+
styles: ':host {\n display: flex;\n flex-direction: column;\n line-height: 1.2;\n}\n\n.number {\n opacity: .7;\n}',
|
|
73
|
+
render: "const customer = document.createElement('div');\nconst number = document.createElement('small');\n\ncustomer.textContent = this.row?.customer ?? '';\nnumber.style.opacity = '.7';\nnumber.textContent = this.row?.number ?? '';\n\nthis.style.lineHeight = '1.2';\nthis.append(customer, number);"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
status: {
|
|
77
|
+
mat: {
|
|
78
|
+
template: '<mat-chip-set>\n <mat-chip [highlighted]="row.status === \'paid\'">{{ row.status }}</mat-chip>\n</mat-chip-set>'
|
|
79
|
+
},
|
|
80
|
+
bs: {
|
|
81
|
+
template: '<span class="badge" [class]="colors[row.status] ?? \'text-bg-light\'">{{ row.status }}</span>',
|
|
82
|
+
members: "colors: Record<string, string> = {paid: 'text-bg-success', pending: 'text-bg-warning', refunded: 'text-bg-secondary'};",
|
|
83
|
+
render: "const badge = document.createElement('span');\nconst colors: Record<string, string> = {paid: 'text-bg-success', pending: 'text-bg-warning', refunded: 'text-bg-secondary'};\n\nbadge.className = `badge ${colors[this.row?.status ?? ''] ?? 'text-bg-light'}`;\nbadge.textContent = this.row?.status ?? '';\n\nthis.append(badge);"
|
|
84
|
+
},
|
|
85
|
+
nv: {
|
|
86
|
+
template: '<span class="status" [class.status_paid]="row.status === \'paid\'">{{ row.status }}</span>',
|
|
87
|
+
styles: '.status {\n padding: 0 6px;\n border: 1px solid GrayText;\n border-radius: 4px;\n}\n\n.status_paid {\n border-color: CanvasText;\n}',
|
|
88
|
+
render: "const badge = document.createElement('span');\n\nbadge.style.padding = '0 6px';\nbadge.style.border = `1px solid ${(this.row?.status === 'paid') ? 'CanvasText' : 'GrayText'}`;\nbadge.style.borderRadius = '4px';\nbadge.textContent = this.row?.status ?? '';\n\nthis.append(badge);"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
actions: {
|
|
92
|
+
mat: {
|
|
93
|
+
template: '<button matIconButton aria-label="Edit" matTooltip="Edit" matTooltipPosition="above" (click)="action.emit(\'edit\')">\n <mat-icon>edit</mat-icon>\n</button>\n<button matIconButton aria-label="Delete" matTooltip="Delete" matTooltipPosition="above" (click)="action.emit(\'delete\')">\n <mat-icon>delete</mat-icon>\n</button>',
|
|
94
|
+
styles: ':host {\n --mat-icon-button-icon-color: var(--mat-sys-primary);\n}'
|
|
95
|
+
},
|
|
96
|
+
bs: {
|
|
97
|
+
template: '<button type="button" class="btn btn-link" (click)="action.emit(\'edit\')">Edit</button>\n<button type="button" class="btn btn-link" (click)="action.emit(\'delete\')">Delete</button>',
|
|
98
|
+
render: "for (const [action, title] of [['edit', 'Edit'], ['delete', 'Delete']]) {\n const button = document.createElement('button');\n\n button.type = 'button';\n button.className = 'btn btn-link';\n button.textContent = title;\n button.addEventListener('click', () => this.sendAction(action));\n\n this.append(button);\n}"
|
|
99
|
+
},
|
|
100
|
+
nv: {
|
|
101
|
+
template: '<a href="" role="button" (click)="$event.preventDefault(); action.emit(\'edit\')">Edit</a>\n<a href="" role="button" (click)="$event.preventDefault(); action.emit(\'delete\')">Delete</a>',
|
|
102
|
+
styles: ':host {\n display: flex;\n gap: 16px;\n}\n\na {\n color: LinkText;\n}',
|
|
103
|
+
render: "for (const [action, title] of [['edit', 'Edit'], ['delete', 'Delete']]) {\n const link = document.createElement('a');\n\n link.href = '';\n link.setAttribute('role', 'button');\n link.style.color = 'LinkText';\n link.textContent = title;\n link.addEventListener('click', event => {\n event.preventDefault();\n this.sendAction(action);\n });\n\n this.append(link);\n}"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
details: {
|
|
107
|
+
mat: {
|
|
108
|
+
template: '<dl>\n <dt>Customer</dt>\n <dd>{{ row.customer }}</dd>\n <dt>Total</dt>\n <dd>{{ row.total }}</dd>\n <dt>Tags</dt>\n <dd>\n <mat-chip-set>\n @for (tag of row.tags; track tag) {\n <mat-chip>{{ tag }}</mat-chip>\n }\n </mat-chip-set>\n </dd>\n</dl>',
|
|
109
|
+
styles: ':host {\n display: block;\n}\n\ndl {\n padding: 16px 24px;\n display: grid;\n grid-template-columns: max-content 1fr;\n align-items: center;\n gap: 8px 24px;\n margin: 0;\n}\n\ndt {\n color: var(--mat-sys-on-surface-variant);\n}\n\ndd {\n margin: 0;\n}'
|
|
110
|
+
},
|
|
111
|
+
bs: {
|
|
112
|
+
template: '<dl>\n <dt>Customer</dt>\n <dd>{{ row.customer }}</dd>\n <dt>Total</dt>\n <dd>{{ row.total }}</dd>\n <dt>Tags</dt>\n <dd>\n @for (tag of row.tags; track tag) {\n <span class="badge text-bg-secondary me-1">{{ tag }}</span>\n }\n </dd>\n</dl>',
|
|
113
|
+
styles: ':host {\n display: block;\n}\n\ndl {\n padding: 12px 16px;\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: 8px 24px;\n margin: 0;\n}\n\ndt {\n font-weight: normal;\n color: var(--bs-secondary-color);\n}\n\ndd {\n margin: 0;\n}',
|
|
114
|
+
render: "this.style.display = 'block';\n\nconst list = document.createElement('dl');\n\nlist.style.cssText = 'display: grid; grid-template-columns: max-content 1fr; gap: 8px 24px; margin: 0; padding: 12px 16px;';\n\nfor (const [label, value] of [['Customer', this.row?.customer], ['Total', this.row?.total], ['Tags', this.row?.tags.join(', ')]] as [string, unknown][]) {\n const term = document.createElement('dt');\n const detail = document.createElement('dd');\n\n term.style.cssText = 'font-weight: normal; color: var(--bs-secondary-color);';\n term.textContent = label;\n detail.style.margin = '0';\n detail.textContent = String(value ?? '');\n list.append(term, detail);\n}\n\nthis.append(list);"
|
|
115
|
+
},
|
|
116
|
+
nv: {
|
|
117
|
+
template: '<dl>\n <dt>Customer</dt>\n <dd>{{ row.customer }}</dd>\n <dt>Total</dt>\n <dd>{{ row.total }}</dd>\n <dt>Tags</dt>\n <dd>{{ row.tags.join(\', \') }}</dd>\n</dl>',
|
|
118
|
+
styles: ':host {\n display: block;\n}\n\ndl {\n padding: 12px 16px;\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: 8px 24px;\n margin: 0;\n}\n\ndt {\n color: GrayText;\n}\n\ndd {\n margin: 0;\n}',
|
|
119
|
+
render: "this.style.display = 'block';\n\nconst list = document.createElement('dl');\n\nlist.style.cssText = 'display: grid; grid-template-columns: max-content 1fr; gap: 8px 24px; margin: 0; padding: 12px 16px;';\n\nfor (const [label, value] of [['Customer', this.row?.customer], ['Total', this.row?.total], ['Tags', this.row?.tags.join(', ')]] as [string, unknown][]) {\n const term = document.createElement('dt');\n const detail = document.createElement('dd');\n\n term.style.color = 'GrayText';\n term.textContent = label;\n detail.style.margin = '0';\n detail.textContent = String(value ?? '');\n list.append(term, detail);\n}\n\nthis.append(list);"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
filterRules: {
|
|
124
|
+
big: 'return row.total > 1000;'
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'users-server',
|
|
130
|
+
description: 'Users loaded from the server page by page, with a search of the toolbar and the sort of the columns',
|
|
131
|
+
spec: {
|
|
132
|
+
specVersion: 1,
|
|
133
|
+
rowType: 'User',
|
|
134
|
+
fields: [
|
|
135
|
+
{ name: 'name', type: 'string' },
|
|
136
|
+
{ name: 'email', type: 'string' },
|
|
137
|
+
{ name: 'role', type: 'string', values: ['admin', 'editor', 'viewer'] },
|
|
138
|
+
{ name: 'active', type: 'boolean' }
|
|
139
|
+
],
|
|
140
|
+
cols: [
|
|
141
|
+
{ title: 'Name', name: 'name' },
|
|
142
|
+
{ title: 'Email', name: 'email' },
|
|
143
|
+
{ title: 'Role', name: 'role' },
|
|
144
|
+
{ title: 'Active', name: 'active', component: { element: 'switch' } }
|
|
145
|
+
],
|
|
146
|
+
config: { toolbarTitle: 'Users', pageSize: 20, server: true }
|
|
147
|
+
},
|
|
148
|
+
fillings: {}
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'products',
|
|
152
|
+
description: 'The smallest table: columns of the fields of a row, the rows loaded by the page',
|
|
153
|
+
spec: {
|
|
154
|
+
specVersion: 1,
|
|
155
|
+
rowType: 'Product',
|
|
156
|
+
fields: [
|
|
157
|
+
{ name: 'title', type: 'string' },
|
|
158
|
+
{ name: 'price', type: 'number' }
|
|
159
|
+
],
|
|
160
|
+
cols: [
|
|
161
|
+
{ title: 'Product', name: 'title' },
|
|
162
|
+
{ title: 'Price', name: 'price' }
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
fillings: {}
|
|
166
|
+
}
|
|
167
|
+
];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CSS custom properties a spec may set: the sizes and the timings that a skin declares on the host and the core
|
|
3
|
+
* registers with @property (packages/core/styles/_custom-properties.scss). A value set on the element changes
|
|
4
|
+
* the CSS and the script of the table together, without a new build of the skin.
|
|
5
|
+
*/
|
|
6
|
+
export interface BroCssVariable {
|
|
7
|
+
name: string;
|
|
8
|
+
kind: 'length' | 'time' | 'number' | 'integer' | 'easing';
|
|
9
|
+
/**
|
|
10
|
+
* The element that reads it: the table, the container of the Filter Drawer, or both. A value from an ancestor does
|
|
11
|
+
* not reach them, so the generator sets it on the element itself.
|
|
12
|
+
*/
|
|
13
|
+
target: 'table' | 'drawer' | 'both';
|
|
14
|
+
min?: number;
|
|
15
|
+
max?: number;
|
|
16
|
+
description: string;
|
|
17
|
+
}
|
|
18
|
+
export declare const cssVariables: BroCssVariable[];
|
|
19
|
+
/** The registered properties a spec may not set, with the reason. */
|
|
20
|
+
export declare const excludedCssVariables: Record<string, string>;
|
|
21
|
+
/** The problem of a value for its variable, or undefined for a good value. */
|
|
22
|
+
export declare function checkCssValue(variable: BroCssVariable, value: string): string | undefined;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const cssVariables = [
|
|
2
|
+
{ name: '--bro-row-height', kind: 'length', target: 'table', description: 'the height of a body row' },
|
|
3
|
+
{ name: '--bro-header-height', kind: 'length', target: 'table', description: 'the height of the header' },
|
|
4
|
+
{ name: '--bro-shadow-size', kind: 'length', target: 'both', description: 'the size of the shadow of the table and the room the Filter Drawer leaves for it' },
|
|
5
|
+
{ name: '--bro-corner-radius', kind: 'length', target: 'both', description: 'the radius of the corners of the table; 0 keeps them square' },
|
|
6
|
+
{ name: '--bro-island-space-size', kind: 'length', target: 'both', description: 'the space between the table and the Filter Drawer' },
|
|
7
|
+
{ name: '--bro-table-min-width', kind: 'length', target: 'table', description: 'the narrowest table, as wide as its paginator in one line; the Filter Drawer lies over a table that would be narrower next to it' },
|
|
8
|
+
{ name: '--bro-filter-drawer-width', kind: 'length', target: 'drawer', description: 'the width of the Filter Drawer' },
|
|
9
|
+
{ name: '--bro-filter-drawer-corner-radius', kind: 'length', target: 'drawer', description: 'the radius of the corners of the Filter Drawer; 0 keeps them square' },
|
|
10
|
+
{ name: '--bro-filter-drawer-duration', kind: 'time', target: 'both', description: 'the duration of the motion of the Filter Drawer and of the columns with it' },
|
|
11
|
+
{ name: '--bro-column-width-duration', kind: 'time', target: 'table', description: 'the duration of the motion of the columns to their new widths' },
|
|
12
|
+
{ name: '--bro-details-duration', kind: 'time', target: 'table', description: 'the duration of opening the row details' },
|
|
13
|
+
{ name: '--bro-details-easing', kind: 'easing', target: 'table', description: 'the curve of the eased row details, with config.rowDetailsMotion: eased, and of the turn of their button' },
|
|
14
|
+
{ name: '--bro-kinetic-time-constant', kind: 'time', target: 'table', description: 'the time constant of the kinetic scroll' },
|
|
15
|
+
{ name: '--bro-state-icon-duration', kind: 'time', target: 'table', description: 'the duration of the animation of the state icon' },
|
|
16
|
+
{ name: '--bro-state-message-duration', kind: 'time', target: 'table', description: 'the duration of the animation of the state message' },
|
|
17
|
+
{ name: '--bro-refresh-overlay-opacity', kind: 'number', target: 'table', min: 0, max: 1, description: 'how much the cells of the body fade during a refresh, in the light theme' },
|
|
18
|
+
{ name: '--bro-refresh-overlay-opacity-dark', kind: 'number', target: 'table', min: 0, max: 1, description: 'how much the cells of the body fade during a refresh, in the dark theme' },
|
|
19
|
+
{ name: '--bro-table-body-container-z-index', kind: 'integer', target: 'table', description: 'the layer of the body of the table' }
|
|
20
|
+
];
|
|
21
|
+
/** The registered properties a spec may not set, with the reason. */
|
|
22
|
+
export const excludedCssVariables = {
|
|
23
|
+
'--bro-duration-scale': 'the skin sets it to 0 under prefers-reduced-motion; a value on the table would cancel this setting of the user',
|
|
24
|
+
'--bro-filter-drawer-opened': 'it is the state of the Filter Drawer, not a setting: its CSS moves it from 0 to 1 on opening, and a value would stop the drawer'
|
|
25
|
+
};
|
|
26
|
+
const units = {
|
|
27
|
+
length: /^(\d+|\d*\.\d+)(px|rem|em)$|^0$/,
|
|
28
|
+
time: /^(\d+|\d*\.\d+)(ms|s)$/,
|
|
29
|
+
number: /^(\d+|\d*\.\d+)$/,
|
|
30
|
+
integer: /^-?\d+$/,
|
|
31
|
+
// The x of the two points of a cubic-bezier lie from 0 to 1, as CSS takes them
|
|
32
|
+
easing: /^cubic-bezier\(\s*(0(\.\d+)?|1(\.0+)?|\.\d+)\s*,\s*-?(\d+(\.\d+)?|\.\d+)\s*,\s*(0(\.\d+)?|1(\.0+)?|\.\d+)\s*,\s*-?(\d+(\.\d+)?|\.\d+)\s*\)$|^(linear|ease|ease-in|ease-out|ease-in-out)$/
|
|
33
|
+
};
|
|
34
|
+
const expectedValues = {
|
|
35
|
+
length: 'a length in px, rem or em, as 64px',
|
|
36
|
+
time: 'a time in ms or s, as 300ms',
|
|
37
|
+
number: 'a number, as 0.5',
|
|
38
|
+
integer: 'an integer, as 2',
|
|
39
|
+
easing: 'a curve of CSS, as cubic-bezier(.2, 0, 0, 1) or ease-out'
|
|
40
|
+
};
|
|
41
|
+
/** The problem of a value for its variable, or undefined for a good value. */
|
|
42
|
+
export function checkCssValue(variable, value) {
|
|
43
|
+
if (!units[variable.kind].test(value)) {
|
|
44
|
+
return `expected ${expectedValues[variable.kind]}`;
|
|
45
|
+
}
|
|
46
|
+
const number = parseFloat(value);
|
|
47
|
+
if (variable.min !== undefined && number < variable.min) {
|
|
48
|
+
return `expected a value from ${variable.min}`;
|
|
49
|
+
}
|
|
50
|
+
if (variable.max !== undefined && number > variable.max) {
|
|
51
|
+
return `expected a value up to ${variable.max}`;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BroSpecField } from './spec.js';
|
|
2
|
+
/** A JSON Schema as a plain object. */
|
|
3
|
+
export type BroJsonSchema = Record<string, unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* The JSON Schema of BroTableSpec, for the tools of the MCP server and the reference. It gives the form; validateSpec
|
|
6
|
+
* checks the meaning: the names of the fields, the cells and the filters, the values of the CSS variables.
|
|
7
|
+
*/
|
|
8
|
+
export declare const specSchema: BroJsonSchema;
|
|
9
|
+
/** The JSON Schema of a filter of the Filter Drawer, as in the spec. */
|
|
10
|
+
export declare const filterSchema: BroJsonSchema;
|
|
11
|
+
/** The JSON Schema of the filling of one cell or of the row details, BroCellFilling, for the tools of the MCP server. */
|
|
12
|
+
export declare const cellFillingSchema: BroJsonSchema;
|
|
13
|
+
/** The JSON Schema of BroSpecFillings, the parts of the code that a model writes, for the tools of the MCP server. */
|
|
14
|
+
export declare const fillingsSchema: BroJsonSchema;
|
|
15
|
+
/**
|
|
16
|
+
* The JSON Schema of the rows for the fields of a spec. A strict schema of a model has no object with free keys, so
|
|
17
|
+
* the site asks the model for the rows by this schema, after the spec: every value gets the type of its field.
|
|
18
|
+
*/
|
|
19
|
+
export declare function rowsSchema(fields: BroSpecField[]): BroJsonSchema;
|