@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.
Files changed (56) hide show
  1. package/bundle/openbridge-webcomponents.bundle.js +255 -67
  2. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  3. package/custom-elements.json +67 -18
  4. package/dist/automation/automation-tank/automation-tank.css.js +94 -14
  5. package/dist/automation/automation-tank/automation-tank.css.js.map +1 -1
  6. package/dist/automation/automation-tank/automation-tank.d.ts +53 -5
  7. package/dist/automation/automation-tank/automation-tank.d.ts.map +1 -1
  8. package/dist/automation/automation-tank/automation-tank.js +39 -20
  9. package/dist/automation/automation-tank/automation-tank.js.map +1 -1
  10. package/dist/building-blocks/readout-block/readout-block.d.ts +27 -1
  11. package/dist/building-blocks/readout-block/readout-block.d.ts.map +1 -1
  12. package/dist/building-blocks/readout-block/readout-block.js +31 -10
  13. package/dist/building-blocks/readout-block/readout-block.js.map +1 -1
  14. package/dist/integration-systems/integration-button/integration-button.css.js +20 -13
  15. package/dist/integration-systems/integration-button/integration-button.css.js.map +1 -1
  16. package/dist/integration-systems/integration-button/integration-button.d.ts +1 -1
  17. package/dist/integration-systems/integration-button/integration-button.js +1 -1
  18. package/dist/integration-systems/integration-button/integration-button.js.map +1 -1
  19. package/dist/integration-systems/integration-dropdown-button/integration-dropdown-button.css.js +1 -0
  20. package/dist/integration-systems/integration-dropdown-button/integration-dropdown-button.css.js.map +1 -1
  21. package/dist/navigation-instruments/readout/readout-formatters.d.ts +61 -0
  22. package/dist/navigation-instruments/readout/readout-formatters.d.ts.map +1 -1
  23. package/dist/navigation-instruments/readout/readout-formatters.js +35 -2
  24. package/dist/navigation-instruments/readout/readout-formatters.js.map +1 -1
  25. package/dist/navigation-instruments/readout/readout.d.ts +19 -2
  26. package/dist/navigation-instruments/readout/readout.d.ts.map +1 -1
  27. package/dist/navigation-instruments/readout/readout.js +18 -4
  28. package/dist/navigation-instruments/readout/readout.js.map +1 -1
  29. package/dist/navigation-instruments/readout-list/readout-list.d.ts.map +1 -1
  30. package/dist/navigation-instruments/readout-list/readout-list.js +6 -3
  31. package/dist/navigation-instruments/readout-list/readout-list.js.map +1 -1
  32. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts +19 -2
  33. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts.map +1 -1
  34. package/dist/navigation-instruments/readout-list-item/readout-list-item.js +18 -4
  35. package/dist/navigation-instruments/readout-list-item/readout-list-item.js.map +1 -1
  36. package/package.json +4 -2
  37. package/script/agent-docs/emitters.ts +189 -0
  38. package/script/agent-docs/frontmatter.ts +118 -0
  39. package/script/check-slot-event-docs.ts +2 -2
  40. package/script/sync-agent-docs.test.ts +298 -0
  41. package/script/sync-agent-docs.ts +156 -0
  42. package/src/automation/automation-tank/automation-tank.css +70 -3
  43. package/src/automation/automation-tank/automation-tank.stories.ts +140 -1
  44. package/src/automation/automation-tank/automation-tank.ts +106 -27
  45. package/src/building-blocks/readout-block/readout-block.stories.ts +20 -0
  46. package/src/building-blocks/readout-block/readout-block.ts +56 -10
  47. package/src/icons/AGENTS.md +6 -0
  48. package/src/integration-systems/integration-button/integration-button.css +20 -13
  49. package/src/integration-systems/integration-button/integration-button.stories.ts +70 -44
  50. package/src/integration-systems/integration-button/integration-button.ts +2 -2
  51. package/src/integration-systems/integration-dropdown-button/integration-dropdown-button.css +1 -0
  52. package/src/navigation-instruments/readout/readout-formatters.spec.ts +205 -0
  53. package/src/navigation-instruments/readout/readout-formatters.ts +128 -2
  54. package/src/navigation-instruments/readout/readout.ts +38 -5
  55. package/src/navigation-instruments/readout-list/readout-list.ts +10 -2
  56. 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: not focusable (rendered as <div>) and not
160
- * interactive disable :hover/:active visuals from the flat-mixin rules. */
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
  }