@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.120 → 2.0.0-next.121

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.
@@ -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');
@@ -0,0 +1,6 @@
1
+ # src/icons — generated, do not edit
2
+
3
+ Every file here is generated from Figma by `npm run download:icons`. Edits are
4
+ discarded on the next run. See [`docs/agents/generated-code.md`](../../../../docs/agents/generated-code.md).
5
+
6
+ Note `src/manual-icon/` is the opposite: hand-written, edit it normally.
@@ -27,9 +27,13 @@
27
27
  .touch-target {
28
28
  @mixin style style=integration-normal visibleWrapperClass=.content-container;
29
29
  @mixin font-body;
30
+ --leading-icon-size: var(
31
+ --app-components-integration-system-navigation-item-horizontal-icon-size
32
+ );
30
33
  anchor-name: --touch-target;
31
34
  display: flex;
32
35
  align-items: center;
36
+ text-align: left;
33
37
  gap: 4px;
34
38
  flex: 1;
35
39
  min-height: var(
@@ -169,20 +173,23 @@
169
173
  color: var(--integration-on-normal-neutral-color);
170
174
  }
171
175
 
172
- .icon.leading {
173
- width: var(
174
- --app-components-integration-system-navigation-item-horizontal-icon-size
175
- );
176
- height: var(
177
- --app-components-integration-system-navigation-item-horizontal-icon-size
176
+ &.has-status {
177
+ --leading-icon-size: var(
178
+ --app-components-integration-system-navigation-item-horizontal-icon-size-large
178
179
  );
179
- min-width: var(
180
- --app-components-integration-system-navigation-item-horizontal-icon-size
181
- );
182
- flex: 0 0
183
- var(
184
- --app-components-integration-system-navigation-item-horizontal-icon-size
185
- );
180
+ }
181
+
182
+ .icon.leading {
183
+ width: var(--leading-icon-size);
184
+ height: var(--leading-icon-size);
185
+ min-width: var(--leading-icon-size);
186
+ flex: 0 0 var(--leading-icon-size);
187
+
188
+ & ::slotted(*) {
189
+ display: block;
190
+ width: 100%;
191
+ height: 100%;
192
+ }
186
193
  }
187
194
 
188
195
  .icon.trailing {
@@ -8,6 +8,35 @@ import './integration-button.js';
8
8
  import {html} from 'lit';
9
9
  import '../../icons/icon-placeholder.js';
10
10
 
11
+ const LONG_STATUS =
12
+ 'Very long status for a vessel at sea doing something important';
13
+
14
+ const renderButton = (args: ObcIntegrationButton, statusText: string) => html`
15
+ <obc-integration-button
16
+ style="width: 320px; display: block;"
17
+ .hasLeadingIcon=${args.hasLeadingIcon}
18
+ .hasTrailingIcon=${args.hasTrailingIcon}
19
+ .hasTrailingIcon2=${args.hasTrailingIcon2}
20
+ .hasStatus=${args.hasStatus}
21
+ .readouts=${args.readouts}
22
+ .selected=${args.selected}
23
+ .activated=${args.activated}
24
+ .disabled=${args.disabled}
25
+ .dividerBottom=${args.dividerBottom}
26
+ .dividerRight=${args.dividerRight}
27
+ .variant=${args.variant}
28
+ .type=${args.type}
29
+ >
30
+ <obi-placeholder slot="leading-icon"></obi-placeholder>
31
+ <obi-placeholder slot="trailing-icon"></obi-placeholder>
32
+ <obi-placeholder slot="trailing-icon2"></obi-placeholder>
33
+ <div slot="label">Label</div>
34
+ <div slot="status">${statusText}</div>
35
+ <div slot="info-label">Info Label</div>
36
+ <div slot="info-status">Info Status</div>
37
+ </obc-integration-button>
38
+ `;
39
+
11
40
  const meta: Meta<ObcIntegrationButton> = {
12
41
  title: 'Integration Systems/Integration Button',
13
42
  tags: ['experimental'],
@@ -30,28 +59,7 @@ const meta: Meta<ObcIntegrationButton> = {
30
59
  value: 'integration-container-global-color',
31
60
  },
32
61
  },
33
- render: (args) => html`
34
- <obc-integration-button
35
- style="width: 320px; display: block;"
36
- .hasLeadingIcon=${args.hasLeadingIcon}
37
- .hasTrailingIcon=${args.hasTrailingIcon}
38
- .hasTrailingIcon2=${args.hasTrailingIcon2}
39
- .hasStatus=${args.hasStatus}
40
- .readouts=${args.readouts}
41
- .selected=${args.selected}
42
- .disabled=${args.disabled}
43
- .variant=${args.variant}
44
- .type=${args.type}
45
- >
46
- <obi-placeholder slot="leading-icon"></obi-placeholder>
47
- <obi-placeholder slot="trailing-icon"></obi-placeholder>
48
- <obi-placeholder slot="trailing-icon2"></obi-placeholder>
49
- <div slot="label">Label</div>
50
- <div slot="status">Status</div>
51
- <div slot="info-label">Info Label</div>
52
- <div slot="info-status">Info Status</div>
53
- </obc-integration-button>
54
- `,
62
+ render: (args) => renderButton(args, 'Status'),
55
63
  } satisfies Meta<ObcIntegrationButton>;
56
64
  export default meta;
57
65
 
@@ -61,28 +69,6 @@ export const WithStatus: StoryObj<ObcIntegrationButton> = {
61
69
  args: {
62
70
  hasStatus: true,
63
71
  },
64
- render: (args) => html`
65
- <obc-integration-button
66
- style="width: 320px; display: block;"
67
- .hasLeadingIcon=${args.hasLeadingIcon}
68
- .hasTrailingIcon=${args.hasTrailingIcon}
69
- .hasTrailingIcon2=${args.hasTrailingIcon2}
70
- .readouts=${args.readouts}
71
- .selected=${args.selected}
72
- .disabled=${args.disabled}
73
- .variant=${args.variant}
74
- .type=${args.type}
75
- .hasStatus=${args.hasStatus}
76
- >
77
- <obi-placeholder slot="leading-icon"></obi-placeholder>
78
- <obi-placeholder slot="trailing-icon"></obi-placeholder>
79
- <obi-placeholder slot="trailing-icon2"></obi-placeholder>
80
- <div slot="label">Label</div>
81
- <div slot="status">Status</div>
82
- <div slot="info-label">Info Label</div>
83
- <div slot="info-status">Info Status</div>
84
- </obc-integration-button>
85
- `,
86
72
  };
87
73
 
88
74
  export const Selected: StoryObj<ObcIntegrationButton> = {
@@ -111,9 +97,49 @@ export const Rich: StoryObj<ObcIntegrationButton> = {
111
97
  },
112
98
  };
113
99
 
100
+ export const RichWithStatus: StoryObj<ObcIntegrationButton> = {
101
+ args: {
102
+ type: IntegrationButtonType.rich,
103
+ hasStatus: true,
104
+ },
105
+ };
106
+
114
107
  export const Disabled: StoryObj<ObcIntegrationButton> = {
115
108
  args: {
116
109
  type: IntegrationButtonType.rich,
117
110
  disabled: true,
118
111
  },
119
112
  };
113
+
114
+ export const WithLongStatus: StoryObj<ObcIntegrationButton> = {
115
+ args: {
116
+ hasStatus: true,
117
+ },
118
+ render: (args) => renderButton(args, LONG_STATUS),
119
+ };
120
+
121
+ export const HugWithLongStatus: StoryObj<ObcIntegrationButton> = {
122
+ args: {
123
+ type: IntegrationButtonType.hug,
124
+ hasStatus: true,
125
+ hasTrailingIcon: false,
126
+ },
127
+ render: (args) => renderButton(args, LONG_STATUS),
128
+ };
129
+
130
+ export const RichWithLongStatus: StoryObj<ObcIntegrationButton> = {
131
+ args: {
132
+ type: IntegrationButtonType.rich,
133
+ hasStatus: true,
134
+ },
135
+ render: (args) => renderButton(args, LONG_STATUS),
136
+ };
137
+
138
+ export const RichWithLongReadoutLabel: StoryObj<ObcIntegrationButton> = {
139
+ args: {
140
+ type: IntegrationButtonType.rich,
141
+ hasStatus: true,
142
+ readouts: [{label: 'Estimated Time of Arrival', value: '12', unit: 'h'}],
143
+ },
144
+ render: (args) => renderButton(args, 'Status'),
145
+ };
@@ -24,7 +24,7 @@ export enum IntegrationButtonType {
24
24
  /**
25
25
  * `<obc-integration-button>` – A button component for integration systems.
26
26
  *
27
- * @slot leading-icon - Icon before label (shown when `hasLeadingIcon` is true)
27
+ * @slot leading-icon - Icon before label (shown when `hasLeadingIcon` is true); rendered at the large icon size when `hasStatus` is true
28
28
  * @slot trailing-icon - Icon after label (shown when `hasTrailingIcon` is true)
29
29
  * @slot trailing-icon2 - Icon after label (shown when `hasTrailingIcon2` is true)
30
30
  * @slot label - Label text
@@ -76,7 +76,7 @@ export class ObcIntegrationButton extends LitElement {
76
76
  selected: this.selected,
77
77
  activated: this.activated,
78
78
  disabled: this.disabled,
79
- 'has-description': this.hasStatus,
79
+ 'has-status': this.hasStatus,
80
80
  ['variant-' + this.variant]: true,
81
81
  ['type-' + this.type]: true,
82
82
  };
@@ -134,6 +134,7 @@
134
134
  flex-direction: column;
135
135
  align-items: flex-start;
136
136
  flex-grow: 1;
137
+ text-align: left;
137
138
  }
138
139
 
139
140
  & .text-container .label {