@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.120 → 2.0.0-next.122
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/bundle/openbridge-webcomponents.bundle.js +255 -67
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +67 -18
- package/dist/automation/automation-tank/automation-tank.css.js +94 -14
- package/dist/automation/automation-tank/automation-tank.css.js.map +1 -1
- package/dist/automation/automation-tank/automation-tank.d.ts +53 -5
- package/dist/automation/automation-tank/automation-tank.d.ts.map +1 -1
- package/dist/automation/automation-tank/automation-tank.js +39 -20
- package/dist/automation/automation-tank/automation-tank.js.map +1 -1
- package/dist/building-blocks/readout-block/readout-block.d.ts +27 -1
- package/dist/building-blocks/readout-block/readout-block.d.ts.map +1 -1
- package/dist/building-blocks/readout-block/readout-block.js +31 -10
- package/dist/building-blocks/readout-block/readout-block.js.map +1 -1
- package/dist/integration-systems/integration-button/integration-button.css.js +20 -13
- package/dist/integration-systems/integration-button/integration-button.css.js.map +1 -1
- package/dist/integration-systems/integration-button/integration-button.d.ts +1 -1
- package/dist/integration-systems/integration-button/integration-button.js +1 -1
- package/dist/integration-systems/integration-button/integration-button.js.map +1 -1
- package/dist/integration-systems/integration-dropdown-button/integration-dropdown-button.css.js +1 -0
- package/dist/integration-systems/integration-dropdown-button/integration-dropdown-button.css.js.map +1 -1
- package/dist/navigation-instruments/readout/readout-formatters.d.ts +61 -0
- package/dist/navigation-instruments/readout/readout-formatters.d.ts.map +1 -1
- package/dist/navigation-instruments/readout/readout-formatters.js +35 -2
- package/dist/navigation-instruments/readout/readout-formatters.js.map +1 -1
- package/dist/navigation-instruments/readout/readout.d.ts +19 -2
- package/dist/navigation-instruments/readout/readout.d.ts.map +1 -1
- package/dist/navigation-instruments/readout/readout.js +18 -4
- package/dist/navigation-instruments/readout/readout.js.map +1 -1
- package/dist/navigation-instruments/readout-list/readout-list.d.ts.map +1 -1
- package/dist/navigation-instruments/readout-list/readout-list.js +6 -3
- package/dist/navigation-instruments/readout-list/readout-list.js.map +1 -1
- package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts +19 -2
- package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts.map +1 -1
- package/dist/navigation-instruments/readout-list-item/readout-list-item.js +18 -4
- package/dist/navigation-instruments/readout-list-item/readout-list-item.js.map +1 -1
- package/package.json +4 -2
- package/script/agent-docs/emitters.ts +189 -0
- package/script/agent-docs/frontmatter.ts +118 -0
- package/script/check-slot-event-docs.ts +2 -2
- package/script/sync-agent-docs.test.ts +298 -0
- package/script/sync-agent-docs.ts +156 -0
- package/src/automation/automation-tank/automation-tank.css +70 -3
- package/src/automation/automation-tank/automation-tank.stories.ts +140 -1
- package/src/automation/automation-tank/automation-tank.ts +106 -27
- package/src/building-blocks/readout-block/readout-block.stories.ts +20 -0
- package/src/building-blocks/readout-block/readout-block.ts +56 -10
- package/src/icons/AGENTS.md +6 -0
- package/src/integration-systems/integration-button/integration-button.css +20 -13
- package/src/integration-systems/integration-button/integration-button.stories.ts +70 -44
- package/src/integration-systems/integration-button/integration-button.ts +2 -2
- package/src/integration-systems/integration-dropdown-button/integration-dropdown-button.css +1 -0
- package/src/navigation-instruments/readout/readout-formatters.spec.ts +205 -0
- package/src/navigation-instruments/readout/readout-formatters.ts +128 -2
- package/src/navigation-instruments/readout/readout.ts +38 -5
- package/src/navigation-instruments/readout-list/readout-list.ts +10 -2
- package/src/navigation-instruments/readout-list-item/readout-list-item.ts +38 -5
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import {readFileSync} from 'node:fs';
|
|
2
|
+
import {describe, expect, it} from 'vitest';
|
|
3
|
+
import {parseAgentDoc, type AgentDoc} from './agent-docs/frontmatter.js';
|
|
4
|
+
import {
|
|
5
|
+
ROUTING_MARKER,
|
|
6
|
+
renderClaudeMd,
|
|
7
|
+
renderCopilotInstructions,
|
|
8
|
+
renderCursorRule,
|
|
9
|
+
renderInstructionsFile,
|
|
10
|
+
renderRoutingTable,
|
|
11
|
+
replaceMarkedBlock,
|
|
12
|
+
rewriteSiblingLinks,
|
|
13
|
+
} from './agent-docs/emitters.js';
|
|
14
|
+
|
|
15
|
+
const VALID = `---
|
|
16
|
+
name: a11y
|
|
17
|
+
description: Accessibility (WCAG 2.1 AA) — keyboard nav, ARIA, focus
|
|
18
|
+
globs:
|
|
19
|
+
- packages/openbridge-webcomponents/src/components/**
|
|
20
|
+
- packages/openbridge-webcomponents/src/automation/**
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Accessibility Instructions
|
|
24
|
+
|
|
25
|
+
Body text.
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
describe('parseAgentDoc', () => {
|
|
29
|
+
it('parses name, description and globs', () => {
|
|
30
|
+
const doc = parseAgentDoc(VALID, 'docs/agents/a11y.md');
|
|
31
|
+
expect(doc.name).toBe('a11y');
|
|
32
|
+
expect(doc.description).toBe(
|
|
33
|
+
'Accessibility (WCAG 2.1 AA) — keyboard nav, ARIA, focus'
|
|
34
|
+
);
|
|
35
|
+
expect(doc.globs).toEqual([
|
|
36
|
+
'packages/openbridge-webcomponents/src/components/**',
|
|
37
|
+
'packages/openbridge-webcomponents/src/automation/**',
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns the body without the frontmatter', () => {
|
|
42
|
+
const doc = parseAgentDoc(VALID, 'docs/agents/a11y.md');
|
|
43
|
+
expect(doc.body).toBe('# Accessibility Instructions\n\nBody text.\n');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('strips single quotes used to escape leading "!"', () => {
|
|
47
|
+
const raw = `---
|
|
48
|
+
name: jsdoc
|
|
49
|
+
description: JSDoc rules
|
|
50
|
+
globs:
|
|
51
|
+
- packages/openbridge-webcomponents/src/**/*.ts
|
|
52
|
+
- '!packages/openbridge-webcomponents/src/icons/**'
|
|
53
|
+
---
|
|
54
|
+
body
|
|
55
|
+
`;
|
|
56
|
+
expect(parseAgentDoc(raw, 'docs/agents/jsdoc.md').globs[1]).toBe(
|
|
57
|
+
'!packages/openbridge-webcomponents/src/icons/**'
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('throws when name does not match the filename', () => {
|
|
62
|
+
expect(() => parseAgentDoc(VALID, 'docs/agents/wrong.md')).toThrow(
|
|
63
|
+
/name "a11y" does not match filename "wrong"/
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('throws on an unsupported key', () => {
|
|
68
|
+
const raw = `---
|
|
69
|
+
name: x
|
|
70
|
+
description: d
|
|
71
|
+
globs:
|
|
72
|
+
- a/**
|
|
73
|
+
alwaysApply: true
|
|
74
|
+
---
|
|
75
|
+
body
|
|
76
|
+
`;
|
|
77
|
+
expect(() => parseAgentDoc(raw, 'docs/agents/x.md')).toThrow(
|
|
78
|
+
/unsupported key "alwaysApply" on line 6/
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('throws when globs is empty', () => {
|
|
83
|
+
const raw = `---
|
|
84
|
+
name: x
|
|
85
|
+
description: d
|
|
86
|
+
globs:
|
|
87
|
+
---
|
|
88
|
+
body
|
|
89
|
+
`;
|
|
90
|
+
expect(() => parseAgentDoc(raw, 'docs/agents/x.md')).toThrow(
|
|
91
|
+
/"globs" must list at least one pattern/
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('throws when there is no frontmatter', () => {
|
|
96
|
+
expect(() =>
|
|
97
|
+
parseAgentDoc('# no frontmatter\n', 'docs/agents/x.md')
|
|
98
|
+
).toThrow(/must start with a "---" frontmatter block/);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const DOC: AgentDoc = {
|
|
103
|
+
name: 'a11y',
|
|
104
|
+
description: 'Accessibility (WCAG 2.1 AA)',
|
|
105
|
+
globs: ['packages/openbridge-webcomponents/src/components/**'],
|
|
106
|
+
body: '# Accessibility\n\nBody.\n',
|
|
107
|
+
sourcePath: 'docs/agents/a11y.md',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
describe('renderInstructionsFile', () => {
|
|
111
|
+
it('writes applyTo from globs and keeps the body verbatim', () => {
|
|
112
|
+
const out = renderInstructionsFile(DOC);
|
|
113
|
+
expect(out).toContain(
|
|
114
|
+
'applyTo: "packages/openbridge-webcomponents/src/components/**"'
|
|
115
|
+
);
|
|
116
|
+
expect(out).toContain('# Accessibility\n\nBody.\n');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('joins multiple globs with commas', () => {
|
|
120
|
+
const out = renderInstructionsFile({...DOC, globs: ['a/**', 'b/**']});
|
|
121
|
+
expect(out).toContain('applyTo: "a/**,b/**"');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('includes a do-not-edit banner naming the source and the command', () => {
|
|
125
|
+
const out = renderInstructionsFile(DOC);
|
|
126
|
+
expect(out).toContain('GENERATED FILE — DO NOT EDIT');
|
|
127
|
+
expect(out).toContain('docs/agents/a11y.md');
|
|
128
|
+
expect(out).toContain('npm run agents:sync');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('puts the frontmatter first so Copilot can read applyTo', () => {
|
|
132
|
+
expect(renderInstructionsFile(DOC).startsWith('---\napplyTo:')).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('renderRoutingTable', () => {
|
|
137
|
+
it('emits one row per doc with description and globs', () => {
|
|
138
|
+
const table = renderRoutingTable([DOC]);
|
|
139
|
+
expect(table).toContain('[a11y](docs/agents/a11y.md)');
|
|
140
|
+
expect(table).toContain('Accessibility (WCAG 2.1 AA)');
|
|
141
|
+
expect(table).toContain(
|
|
142
|
+
'`packages/openbridge-webcomponents/src/components/**`'
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('sorts rows by name regardless of input order', () => {
|
|
147
|
+
const z: AgentDoc = {
|
|
148
|
+
...DOC,
|
|
149
|
+
name: 'zebra',
|
|
150
|
+
sourcePath: 'docs/agents/zebra.md',
|
|
151
|
+
};
|
|
152
|
+
const table = renderRoutingTable([z, DOC]);
|
|
153
|
+
expect(table.indexOf('[a11y]')).toBeLessThan(table.indexOf('[zebra]'));
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('renderCopilotInstructions', () => {
|
|
158
|
+
it('points at AGENTS.md and carries the routing table but no bodies', () => {
|
|
159
|
+
const out = renderCopilotInstructions([DOC]);
|
|
160
|
+
expect(out).toContain('AGENTS.md');
|
|
161
|
+
expect(out).toContain('[a11y](docs/agents/a11y.md)');
|
|
162
|
+
expect(out).not.toContain('Body.');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('replaceMarkedBlock', () => {
|
|
167
|
+
it('replaces only the content between the markers', () => {
|
|
168
|
+
const content = [
|
|
169
|
+
'before',
|
|
170
|
+
`<!-- ${ROUTING_MARKER}:start -->`,
|
|
171
|
+
'OLD',
|
|
172
|
+
`<!-- ${ROUTING_MARKER}:end -->`,
|
|
173
|
+
'after',
|
|
174
|
+
].join('\n');
|
|
175
|
+
const out = replaceMarkedBlock(content, ROUTING_MARKER, 'NEW');
|
|
176
|
+
expect(out).toBe(
|
|
177
|
+
[
|
|
178
|
+
'before',
|
|
179
|
+
`<!-- ${ROUTING_MARKER}:start -->`,
|
|
180
|
+
'NEW',
|
|
181
|
+
`<!-- ${ROUTING_MARKER}:end -->`,
|
|
182
|
+
'after',
|
|
183
|
+
].join('\n')
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('throws when the markers are missing', () => {
|
|
188
|
+
expect(() => replaceMarkedBlock('no markers', ROUTING_MARKER, 'x')).toThrow(
|
|
189
|
+
/markers .* not found/
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('renderClaudeMd', () => {
|
|
195
|
+
it('points at AGENTS.md and docs/agents', () => {
|
|
196
|
+
const out = renderClaudeMd();
|
|
197
|
+
expect(out).toContain('AGENTS.md');
|
|
198
|
+
expect(out).toContain('docs/agents/');
|
|
199
|
+
expect(out).toContain('GENERATED FILE — DO NOT EDIT');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('carries no rules of its own — it is a pointer, not a source', () => {
|
|
203
|
+
const out = renderClaudeMd();
|
|
204
|
+
expect(out).toContain('intentionally adds nothing of its own');
|
|
205
|
+
// No numbered rule list: team rules belong in AGENTS.md, path-scoped rules
|
|
206
|
+
// in docs/agents/. Neither is tool-specific.
|
|
207
|
+
expect(out).not.toMatch(/^\d+\. \*\*/m);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('CLI failure semantics', () => {
|
|
212
|
+
const cli = 'script/sync-agent-docs.ts';
|
|
213
|
+
|
|
214
|
+
it('write mode never exits non-zero, so `prepare` cannot break npm ci', () => {
|
|
215
|
+
const src = readFileSync(cli, 'utf8');
|
|
216
|
+
expect(src).toContain('if (CHECK) process.exit(1);');
|
|
217
|
+
expect(src).not.toMatch(/^\s*process\.exit\(1\);\s*$/m);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('only skips negative globs in the resolution check', () => {
|
|
221
|
+
const src = readFileSync(cli, 'utf8');
|
|
222
|
+
expect(src).toContain("if (g.startsWith('!')) continue;");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('renderCursorRule', () => {
|
|
227
|
+
it('emits globs + alwaysApply:false — Cursor\'s "Apply to Specific Files" type', () => {
|
|
228
|
+
const out = renderCursorRule(DOC);
|
|
229
|
+
expect(out.startsWith('---\n')).toBe(true);
|
|
230
|
+
expect(out).toContain(
|
|
231
|
+
'globs: packages/openbridge-webcomponents/src/components/**'
|
|
232
|
+
);
|
|
233
|
+
expect(out).toContain('alwaysApply: false');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('omits description, which would select "Apply Intelligently" instead', () => {
|
|
237
|
+
expect(renderCursorRule(DOC)).not.toContain('description:');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('joins multiple globs with commas', () => {
|
|
241
|
+
expect(renderCursorRule({...DOC, globs: ['a/**', 'b/**']})).toContain(
|
|
242
|
+
'globs: a/**,b/**'
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('drops negative globs, which Cursor does not document', () => {
|
|
247
|
+
const out = renderCursorRule({...DOC, globs: ['a/**', '!b/**', 'c/**']});
|
|
248
|
+
expect(out).toContain('globs: a/**,c/**');
|
|
249
|
+
expect(out).not.toContain('!b/**');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('keeps the body verbatim and carries the do-not-edit banner', () => {
|
|
253
|
+
const out = renderCursorRule(DOC);
|
|
254
|
+
expect(out).toContain('# Accessibility\n\nBody.\n');
|
|
255
|
+
expect(out).toContain('GENERATED FILE — DO NOT EDIT');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('rewriteSiblingLinks', () => {
|
|
260
|
+
const siblings = new Set(['jsdoc', 'a11y', 'css-postcss']);
|
|
261
|
+
|
|
262
|
+
it('repoints bare sibling links at the canonical doc', () => {
|
|
263
|
+
expect(rewriteSiblingLinks('see [`jsdoc.md`](jsdoc.md).', siblings)).toBe(
|
|
264
|
+
'see [`jsdoc.md`](../../docs/agents/jsdoc.md).'
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('preserves anchors', () => {
|
|
269
|
+
expect(rewriteSiblingLinks('[x](a11y.md#section-2)', siblings)).toBe(
|
|
270
|
+
'[x](../../docs/agents/a11y.md#section-2)'
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('leaves ../../ links alone — they already resolve from every adapter dir', () => {
|
|
275
|
+
const link = '[g](../../IMPLEMENTATION_GUIDELINES.md#-postcss)';
|
|
276
|
+
expect(rewriteSiblingLinks(link, siblings)).toBe(link);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('leaves external and absolute links alone', () => {
|
|
280
|
+
const body = '[a](https://example.com/x.md) [b](/abs/y.md)';
|
|
281
|
+
expect(rewriteSiblingLinks(body, siblings)).toBe(body);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('leaves bare links that are not canonical docs alone', () => {
|
|
285
|
+
const link = '[r](README.md)';
|
|
286
|
+
expect(rewriteSiblingLinks(link, siblings)).toBe(link);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('is applied by both adapter renderers', () => {
|
|
290
|
+
const doc = {...DOC, body: 'see [`jsdoc.md`](jsdoc.md).\n'};
|
|
291
|
+
expect(renderInstructionsFile(doc, siblings)).toContain(
|
|
292
|
+
'](../../docs/agents/jsdoc.md)'
|
|
293
|
+
);
|
|
294
|
+
expect(renderCursorRule(doc, siblings)).toContain(
|
|
295
|
+
'](../../docs/agents/jsdoc.md)'
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {globSync} from 'glob';
|
|
4
|
+
import * as prettier from 'prettier';
|
|
5
|
+
import {parseAgentDoc, type AgentDoc} from './agent-docs/frontmatter.js';
|
|
6
|
+
import {
|
|
7
|
+
ROUTING_MARKER,
|
|
8
|
+
renderClaudeMd,
|
|
9
|
+
renderCopilotInstructions,
|
|
10
|
+
renderCursorRule,
|
|
11
|
+
renderInstructionsFile,
|
|
12
|
+
renderRoutingTable,
|
|
13
|
+
replaceMarkedBlock,
|
|
14
|
+
} from './agent-docs/emitters.js';
|
|
15
|
+
|
|
16
|
+
const ROOT = path.resolve(import.meta.dirname, '../../..');
|
|
17
|
+
const AGENTS_DIR = path.join(ROOT, 'docs/agents');
|
|
18
|
+
const INSTRUCTIONS_DIR = path.join(ROOT, '.github/instructions');
|
|
19
|
+
const CURSOR_RULES_DIR = path.join(ROOT, '.cursor/rules');
|
|
20
|
+
const CHECK = process.argv.includes('--check');
|
|
21
|
+
|
|
22
|
+
const problems: string[] = [];
|
|
23
|
+
const writes = new Map<string, string>();
|
|
24
|
+
|
|
25
|
+
function plan(absPath: string, content: string): void {
|
|
26
|
+
writes.set(absPath, content);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function loadDocs(): AgentDoc[] {
|
|
30
|
+
if (!fs.existsSync(AGENTS_DIR)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`docs/agents does not exist at ${AGENTS_DIR} — nothing to sync`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return fs
|
|
36
|
+
.readdirSync(AGENTS_DIR)
|
|
37
|
+
.filter((f) => f.endsWith('.md'))
|
|
38
|
+
.sort()
|
|
39
|
+
.map((f) => {
|
|
40
|
+
const rel = path.posix.join('docs/agents', f);
|
|
41
|
+
return parseAgentDoc(
|
|
42
|
+
fs.readFileSync(path.join(AGENTS_DIR, f), 'utf8'),
|
|
43
|
+
rel
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Check 1 — every positive glob must match at least one real path. */
|
|
49
|
+
function checkGlobs(docs: AgentDoc[]): void {
|
|
50
|
+
for (const doc of docs) {
|
|
51
|
+
for (const g of doc.globs) {
|
|
52
|
+
if (g.startsWith('!')) continue;
|
|
53
|
+
if (globSync(g, {cwd: ROOT, dot: true}).length === 0) {
|
|
54
|
+
problems.push(`${doc.sourcePath}: glob matches nothing → ${g}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Check 4 — no generated adapter without a canonical source. */
|
|
61
|
+
function checkOrphans(docs: AgentDoc[]): void {
|
|
62
|
+
const adapters: [string, string, string][] = [
|
|
63
|
+
[INSTRUCTIONS_DIR, '.instructions.md', '.github/instructions'],
|
|
64
|
+
[CURSOR_RULES_DIR, '.mdc', '.cursor/rules'],
|
|
65
|
+
];
|
|
66
|
+
for (const [dir, ext, rel] of adapters) {
|
|
67
|
+
if (!fs.existsSync(dir)) continue;
|
|
68
|
+
const expected = new Set(docs.map((d) => `${d.name}${ext}`));
|
|
69
|
+
for (const f of fs.readdirSync(dir)) {
|
|
70
|
+
if (f.endsWith(ext) && !expected.has(f)) {
|
|
71
|
+
problems.push(
|
|
72
|
+
`${rel}/${f}: no matching docs/agents/ source (delete it or add the source)`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const docs = loadDocs();
|
|
80
|
+
checkGlobs(docs);
|
|
81
|
+
checkOrphans(docs);
|
|
82
|
+
|
|
83
|
+
const routable = docs;
|
|
84
|
+
const docNames = new Set(docs.map((d) => d.name));
|
|
85
|
+
|
|
86
|
+
for (const doc of routable) {
|
|
87
|
+
plan(
|
|
88
|
+
path.join(INSTRUCTIONS_DIR, `${doc.name}.instructions.md`),
|
|
89
|
+
renderInstructionsFile(doc, docNames)
|
|
90
|
+
);
|
|
91
|
+
plan(
|
|
92
|
+
path.join(CURSOR_RULES_DIR, `${doc.name}.mdc`),
|
|
93
|
+
renderCursorRule(doc, docNames)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
plan(
|
|
97
|
+
path.join(ROOT, '.github/copilot-instructions.md'),
|
|
98
|
+
renderCopilotInstructions(routable)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const agentsPath = path.join(ROOT, 'AGENTS.md');
|
|
102
|
+
plan(
|
|
103
|
+
agentsPath,
|
|
104
|
+
replaceMarkedBlock(
|
|
105
|
+
fs.readFileSync(agentsPath, 'utf8'),
|
|
106
|
+
ROUTING_MARKER,
|
|
107
|
+
renderRoutingTable(routable)
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
plan(path.join(ROOT, 'CLAUDE.md'), renderClaudeMd());
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Runs generated Markdown through Prettier before writing or comparing.
|
|
115
|
+
*
|
|
116
|
+
* Without this the generator's compact tables would differ from what
|
|
117
|
+
* `npm run format:check` (and the lint-staged `.md` hook) demand, so every
|
|
118
|
+
* sync would leave the repo format-dirty and CI would fail. Formatting here
|
|
119
|
+
* makes generator output and Prettier output the same artefact by
|
|
120
|
+
* construction, and keeps `--check` comparing like with like.
|
|
121
|
+
*/
|
|
122
|
+
async function format(content: string, absPath: string): Promise<string> {
|
|
123
|
+
const config = await prettier.resolveConfig(absPath);
|
|
124
|
+
return prettier.format(content, {
|
|
125
|
+
...config,
|
|
126
|
+
filepath: absPath,
|
|
127
|
+
parser: 'markdown',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const [absPath, raw] of writes) {
|
|
132
|
+
const rel = path.relative(ROOT, absPath);
|
|
133
|
+
const content = await format(raw, absPath);
|
|
134
|
+
const current = fs.existsSync(absPath)
|
|
135
|
+
? fs.readFileSync(absPath, 'utf8')
|
|
136
|
+
: null;
|
|
137
|
+
if (current === content) continue;
|
|
138
|
+
if (CHECK) {
|
|
139
|
+
problems.push(`${rel}: out of date — run \`npm run agents:sync\``);
|
|
140
|
+
} else {
|
|
141
|
+
fs.mkdirSync(path.dirname(absPath), {recursive: true});
|
|
142
|
+
fs.writeFileSync(absPath, content);
|
|
143
|
+
console.log(`${current === null ? 'created' : 'updated'} ${rel}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (problems.length > 0) {
|
|
148
|
+
console.error(`\nagent-docs: ${problems.length} problem(s)`);
|
|
149
|
+
for (const p of problems) console.error(` ${CHECK ? '✗' : 'warning:'} ${p}`);
|
|
150
|
+
// Write mode reports but never fails. `agents:sync` runs from the root
|
|
151
|
+
// `prepare` script, so a stale glob or a missing source must not be able to
|
|
152
|
+
// break `npm ci` for everyone. `lint:agents` is the gate; it runs in CI after
|
|
153
|
+
// the build steps, when any generated paths actually exist.
|
|
154
|
+
if (CHECK) process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
console.log(CHECK ? 'agent-docs: up to date' : 'agent-docs: sync complete');
|
|
@@ -77,6 +77,39 @@
|
|
|
77
77
|
min-width: 0;
|
|
78
78
|
min-height: 0;
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
/* Indefinite-axis fallback.
|
|
82
|
+
*
|
|
83
|
+
* `width`/`height: 100%` only resolve when the parent's corresponding axis is
|
|
84
|
+
* definite. In a flex/grid dashboard slot one axis is very often left to
|
|
85
|
+
* content — `align-self: center` makes the cross axis shrink-to-fit, and a
|
|
86
|
+
* parent sized with `min-height`/`max-height` (rather than `height`) leaves the
|
|
87
|
+
* block axis indefinite. In those cases the percentage computes to `auto` and
|
|
88
|
+
* the tank would size to its own content, which includes the chart cell — and
|
|
89
|
+
* the chart sizes itself from the cell it was measured in. That is a circular
|
|
90
|
+
* constraint, and it resolves as a collapsed cell, unbounded growth or
|
|
91
|
+
* pixel-level ringing depending on the loop gain (issue #1121).
|
|
92
|
+
*
|
|
93
|
+
* `aspect-ratio` supplies the missing axis from the design footprint the
|
|
94
|
+
* `point` variant uses above, so an indefinite axis lands on a definite value
|
|
95
|
+
* that no longer depends on the tank's own content. It is inert when the parent
|
|
96
|
+
* makes both axes definite, so the normal "fill my slot" behavior is unchanged.
|
|
97
|
+
*/
|
|
98
|
+
:host([positioning="button"]) {
|
|
99
|
+
aspect-ratio: 256 / 376;
|
|
100
|
+
}
|
|
101
|
+
:host([positioning="button"][orientation="horizontal"]) {
|
|
102
|
+
aspect-ratio: 420 / 256;
|
|
103
|
+
}
|
|
104
|
+
:host([positioning="button"][compact]),
|
|
105
|
+
:host([positioning="button"][static]) {
|
|
106
|
+
aspect-ratio: 170 / 282;
|
|
107
|
+
}
|
|
108
|
+
:host([positioning="button"][orientation="horizontal"][compact]),
|
|
109
|
+
:host([positioning="button"][orientation="horizontal"][static]) {
|
|
110
|
+
aspect-ratio: 244 / 208;
|
|
111
|
+
}
|
|
112
|
+
|
|
80
113
|
:host([positioning="button"]) .root {
|
|
81
114
|
position: static;
|
|
82
115
|
top: auto;
|
|
@@ -134,10 +167,30 @@
|
|
|
134
167
|
display: flex;
|
|
135
168
|
flex-direction: column;
|
|
136
169
|
gap: 4px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Interactive (`clickable`, the default): the full six-state flat mixin —
|
|
173
|
+
* enabled, activated, hover, pressed, focus-visible, disabled — painted on
|
|
174
|
+
* `.halo`. */
|
|
175
|
+
.root.clickable {
|
|
137
176
|
cursor: pointer;
|
|
138
177
|
/* prettier-ignore */
|
|
139
178
|
@mixin style style=flat visibleWrapperClass=.halo;
|
|
140
179
|
}
|
|
180
|
+
|
|
181
|
+
/* Non-interactive (`clickable` is `false`, and `static`, which is display-only
|
|
182
|
+
* by definition): the mixin's `noClick` variant emits the enabled state ONLY, so
|
|
183
|
+
* the halo keeps exactly the same resting colors and the same 1px border box
|
|
184
|
+
* — no layout shift, nothing else in the tank changes — while hover, pressed
|
|
185
|
+
* and focus-visible disappear. The root is a `<div>` in this case, so there is
|
|
186
|
+
* no focus ring or tab stop to suppress either. Same split as
|
|
187
|
+
* `obc-elevated-card`'s `.not-clickable` / `:not(.not-clickable)` pair. */
|
|
188
|
+
.root:not(.clickable) {
|
|
189
|
+
cursor: default;
|
|
190
|
+
/* prettier-ignore */
|
|
191
|
+
@mixin style style=flat noClick visibleWrapperClass=.halo;
|
|
192
|
+
}
|
|
193
|
+
|
|
141
194
|
.root:focus {
|
|
142
195
|
outline: none;
|
|
143
196
|
}
|
|
@@ -156,10 +209,12 @@
|
|
|
156
209
|
top: auto;
|
|
157
210
|
}
|
|
158
211
|
|
|
159
|
-
/* Static is display-only
|
|
160
|
-
*
|
|
212
|
+
/* Static is display-only, so it never gets the `.clickable` class and the
|
|
213
|
+
* hover/pressed visuals are already gone via the `noClick` rule above. Static
|
|
214
|
+
* additionally refuses pointer events outright — unlike a non-clickable tank,
|
|
215
|
+
* which still lets clicks reach the host so a consumer can opt into handling
|
|
216
|
+
* them. */
|
|
161
217
|
:host([static]) .root {
|
|
162
|
-
cursor: default;
|
|
163
218
|
pointer-events: none;
|
|
164
219
|
}
|
|
165
220
|
|
|
@@ -510,9 +565,21 @@
|
|
|
510
565
|
overflow: hidden;
|
|
511
566
|
}
|
|
512
567
|
|
|
568
|
+
/* The renderers are taken out of flow so the cell sizes them, never the other
|
|
569
|
+
* way round. Both of them derive a pixel size from the box they are measured
|
|
570
|
+
* in — `obc-gauge-trend` publishes an inline `--chart-height` computed from its
|
|
571
|
+
* own measured width, and `obc-bar-vertical` writes `height: 100%; width: auto`
|
|
572
|
+
* onto its own host — so while they are in flow the cell's content height is a
|
|
573
|
+
* function of the cell's own height. Absolute positioning severs that return
|
|
574
|
+
* path: `.bar-container` is `position: relative`, so `inset: 0` pins the
|
|
575
|
+
* renderer to the cell the tank's grid/flex layout already decided on, and a
|
|
576
|
+
* rounding difference between the two can no longer relayout the tank
|
|
577
|
+
* (issue #1121). */
|
|
513
578
|
.bar-container > obc-gauge-trend,
|
|
514
579
|
.bar-container > obc-bar-vertical {
|
|
515
580
|
display: block;
|
|
581
|
+
position: absolute;
|
|
582
|
+
inset: 0;
|
|
516
583
|
width: 100%;
|
|
517
584
|
height: 100%;
|
|
518
585
|
}
|