@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.87 → 2.0.0-next.89

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 (40) hide show
  1. package/bundle/openbridge-webcomponents.bundle.js +17409 -16824
  2. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  3. package/custom-elements.json +755 -44
  4. package/dist/building-blocks/readout-block/readout-block.css.js +229 -0
  5. package/dist/building-blocks/readout-block/readout-block.css.js.map +1 -0
  6. package/dist/building-blocks/readout-block/readout-block.d.ts +146 -0
  7. package/dist/building-blocks/readout-block/readout-block.d.ts.map +1 -0
  8. package/dist/building-blocks/readout-block/readout-block.js +273 -0
  9. package/dist/building-blocks/readout-block/readout-block.js.map +1 -0
  10. package/dist/navigation-instruments/gauge-radial/gauge-radial.css.js +58 -32
  11. package/dist/navigation-instruments/gauge-radial/gauge-radial.css.js.map +1 -1
  12. package/dist/navigation-instruments/gauge-radial/gauge-radial.d.ts +28 -0
  13. package/dist/navigation-instruments/gauge-radial/gauge-radial.d.ts.map +1 -1
  14. package/dist/navigation-instruments/gauge-radial/gauge-radial.js +25 -2
  15. package/dist/navigation-instruments/gauge-radial/gauge-radial.js.map +1 -1
  16. package/dist/navigation-instruments/readout-list/readout-list.css.js +24 -0
  17. package/dist/navigation-instruments/readout-list/readout-list.css.js.map +1 -0
  18. package/dist/navigation-instruments/readout-list/readout-list.d.ts +70 -0
  19. package/dist/navigation-instruments/readout-list/readout-list.d.ts.map +1 -0
  20. package/dist/navigation-instruments/readout-list/readout-list.js +154 -0
  21. package/dist/navigation-instruments/readout-list/readout-list.js.map +1 -0
  22. package/dist/navigation-instruments/readout-list-item/readout-list-item.css.js +80 -145
  23. package/dist/navigation-instruments/readout-list-item/readout-list-item.css.js.map +1 -1
  24. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts +25 -26
  25. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts.map +1 -1
  26. package/dist/navigation-instruments/readout-list-item/readout-list-item.js +66 -118
  27. package/dist/navigation-instruments/readout-list-item/readout-list-item.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/building-blocks/readout-block/readout-block.css +192 -0
  30. package/src/building-blocks/readout-block/readout-block.stories.ts +346 -0
  31. package/src/building-blocks/readout-block/readout-block.ts +355 -0
  32. package/src/navigation-instruments/gauge-radial/gauge-radial.css +53 -30
  33. package/src/navigation-instruments/gauge-radial/gauge-radial.stories.ts +50 -0
  34. package/src/navigation-instruments/gauge-radial/gauge-radial.ts +35 -0
  35. package/src/navigation-instruments/readout-list/readout-list.css +13 -0
  36. package/src/navigation-instruments/readout-list/readout-list.stories.ts +275 -0
  37. package/src/navigation-instruments/readout-list/readout-list.ts +216 -0
  38. package/src/navigation-instruments/readout-list-item/readout-list-item.css +75 -128
  39. package/src/navigation-instruments/readout-list-item/readout-list-item.stories.ts +18 -17
  40. package/src/navigation-instruments/readout-list-item/readout-list-item.ts +84 -140
@@ -0,0 +1,275 @@
1
+ import type {Meta, StoryObj} from '@storybook/web-components-vite';
2
+ import {html} from 'lit';
3
+ import {expect} from 'storybook/test';
4
+ import './readout-list.js';
5
+ import {
6
+ ReadoutListItemSize,
7
+ ReadoutListItemPriority,
8
+ type ReadoutValueOptions,
9
+ } from '../readout-list-item/readout-list-item.js';
10
+ import '../readout-list-item/readout-list-item.js';
11
+
12
+ type ListArgs = {
13
+ showDebugOverlay: boolean;
14
+ };
15
+
16
+ type Row = {
17
+ label: string;
18
+ value: number | null;
19
+ unit: string;
20
+ size?: ReadoutListItemSize;
21
+ hasDegree?: boolean;
22
+ fractionDigits?: number;
23
+ priority?: ReadoutListItemPriority;
24
+ hasSetpoint?: boolean;
25
+ setpoint?: number;
26
+ };
27
+
28
+ function renderRow(row: Row) {
29
+ return html`
30
+ <obc-readout-list-item
31
+ .label=${row.label}
32
+ .unit=${row.unit}
33
+ .value=${row.value}
34
+ .size=${row.size ?? ReadoutListItemSize.small}
35
+ .hasDegree=${row.hasDegree ?? false}
36
+ .fractionDigits=${row.fractionDigits ?? 0}
37
+ .priority=${row.priority}
38
+ .hasSetpoint=${row.hasSetpoint ?? false}
39
+ .setpoint=${row.setpoint}
40
+ ></obc-readout-list-item>
41
+ `;
42
+ }
43
+
44
+ function renderList(rows: Row[], showDebugOverlay: boolean) {
45
+ return html`
46
+ <div
47
+ data-obc-theme="day"
48
+ style="background: var(--container-background-color); padding: 16px; width: 360px; box-sizing: border-box;"
49
+ >
50
+ <obc-readout-list .showDebugOverlay=${showDebugOverlay}>
51
+ ${rows.map(renderRow)}
52
+ </obc-readout-list>
53
+ </div>
54
+ `;
55
+ }
56
+
57
+ const meta = {
58
+ title: 'Instruments/Readout List',
59
+ tags: ['autodocs', '6.0', 'wip'],
60
+ component: 'obc-readout-list',
61
+ args: {
62
+ showDebugOverlay: true,
63
+ },
64
+ argTypes: {
65
+ showDebugOverlay: {control: {type: 'boolean'}},
66
+ },
67
+ } satisfies Meta<ListArgs>;
68
+
69
+ export default meta;
70
+ type Story = StoryObj<ListArgs>;
71
+
72
+ const MIXED_ROWS: Row[] = [
73
+ {label: 'Temperature', value: 45, unit: 'C', hasDegree: true},
74
+ {label: 'Heading', value: 287, unit: 'T', hasDegree: true},
75
+ {label: 'Pressure', value: 1013, unit: 'Pa', fractionDigits: 1},
76
+ {label: 'Distance', value: 4.2, unit: 'miles', fractionDigits: 1},
77
+ {label: 'Speed', value: 18, unit: 'kn', fractionDigits: 1},
78
+ ];
79
+
80
+ export const Default: Story = {
81
+ render: (args) => renderList(MIXED_ROWS, args.showDebugOverlay),
82
+ };
83
+
84
+ const DEGREE_ROWS: Row[] = [
85
+ {label: 'Heading', value: 287, unit: 'T', hasDegree: true},
86
+ {label: 'COG', value: 92, unit: 'T', hasDegree: true},
87
+ {label: 'Speed', value: 18, unit: 'kn'},
88
+ {label: 'Depth', value: 124, unit: 'm'},
89
+ ];
90
+
91
+ export const Degrees: Story = {
92
+ render: (args) => renderList(DEGREE_ROWS, args.showDebugOverlay),
93
+ };
94
+
95
+ export const WithSetpoints: Story = {
96
+ render: (args) =>
97
+ renderList(
98
+ [
99
+ {
100
+ label: 'Heading',
101
+ value: 287,
102
+ unit: 'T',
103
+ hasDegree: true,
104
+ hasSetpoint: true,
105
+ setpoint: 290,
106
+ },
107
+ {
108
+ label: 'Speed',
109
+ value: 8,
110
+ unit: 'kn',
111
+ fractionDigits: 1,
112
+ hasSetpoint: true,
113
+ setpoint: 12,
114
+ },
115
+ {label: 'Depth', value: 1013, unit: 'm'},
116
+ ],
117
+ args.showDebugOverlay
118
+ ),
119
+ };
120
+
121
+ export const TestDynamicRow: Story = {
122
+ render: (args) => renderList(MIXED_ROWS, args.showDebugOverlay),
123
+ play: async ({canvasElement}) => {
124
+ const list = canvasElement.querySelector('obc-readout-list');
125
+ const row = document.createElement(
126
+ 'obc-readout-list-item'
127
+ ) as HTMLElement & {
128
+ label: string;
129
+ value: number;
130
+ unit: string;
131
+ valueOptions?: ReadoutValueOptions;
132
+ };
133
+ row.label = 'Added';
134
+ row.value = 9;
135
+ row.unit = 'kn';
136
+ list?.appendChild(row);
137
+ await new Promise((resolve) => requestAnimationFrame(resolve));
138
+ await new Promise((resolve) => requestAnimationFrame(resolve));
139
+ await expect(row.valueOptions?.spaceReserver).toBe('0000.0');
140
+ },
141
+ };
142
+
143
+ /**
144
+ * **Manual (Interactive)** — the rows are slotted children, so they cannot be
145
+ * driven by Storybook controls. Use the buttons to add / remove rows (a
146
+ * structural change the list re-aligns automatically) and to change the first
147
+ * row's value / unit (a property-only change, so it calls the public
148
+ * {@link ObcReadoutList.align} method). The debug overlay shows the reserved
149
+ * column widths reacting to each change.
150
+ */
151
+ export const Manual: Story = {
152
+ name: 'Manual (Interactive)',
153
+ tags: ['skip-test'],
154
+ render: (args) => html`
155
+ <div
156
+ data-obc-theme="day"
157
+ style="background: var(--container-background-color); padding: 16px; width: 380px; box-sizing: border-box; display: flex; flex-direction: column; gap: 16px;"
158
+ >
159
+ <div style="font-size: 14px; color: var(--element-neutral-color, #888);">
160
+ Rows are slotted children, so Storybook controls can't drive them. Use
161
+ the buttons to add / remove rows (the list re-aligns automatically) and
162
+ to change the first row's value / unit (a property-only change, so it
163
+ calls
164
+ <code>align()</code>). The debug overlay shows the reserved column
165
+ widths.
166
+ </div>
167
+ <obc-readout-list
168
+ id="manual-list"
169
+ .showDebugOverlay=${args.showDebugOverlay}
170
+ >
171
+ <obc-readout-list-item
172
+ .label=${'Heading'}
173
+ .unit=${'T'}
174
+ .value=${287}
175
+ .hasDegree=${true}
176
+ ></obc-readout-list-item>
177
+ <obc-readout-list-item
178
+ .label=${'Speed'}
179
+ .unit=${'kn'}
180
+ .value=${18}
181
+ ></obc-readout-list-item>
182
+ <obc-readout-list-item
183
+ .label=${'Depth'}
184
+ .unit=${'m'}
185
+ .value=${124}
186
+ ></obc-readout-list-item>
187
+ </obc-readout-list>
188
+ <div style="display: flex; gap: 8px; flex-wrap: wrap;">
189
+ <button id="m-add" style="padding: 6px 12px; cursor: pointer;">
190
+ Add row
191
+ </button>
192
+ <button id="m-remove" style="padding: 6px 12px; cursor: pointer;">
193
+ Remove row
194
+ </button>
195
+ <button id="m-bump" style="padding: 6px 12px; cursor: pointer;">
196
+ Bump first value ×10
197
+ </button>
198
+ <button id="m-unit" style="padding: 6px 12px; cursor: pointer;">
199
+ Toggle first unit
200
+ </button>
201
+ </div>
202
+ <div
203
+ id="m-status"
204
+ style="font: 12px/1.4 monospace; color: var(--element-neutral-color, #666);"
205
+ >
206
+ 3 rows
207
+ </div>
208
+ </div>
209
+ `,
210
+ play: async ({canvasElement}) => {
211
+ const list = canvasElement.querySelector('#manual-list') as
212
+ | (HTMLElement & {align: () => void})
213
+ | null;
214
+ const status = canvasElement.querySelector(
215
+ '#m-status'
216
+ ) as HTMLElement | null;
217
+ const addBtn = canvasElement.querySelector(
218
+ '#m-add'
219
+ ) as HTMLButtonElement | null;
220
+ const removeBtn = canvasElement.querySelector(
221
+ '#m-remove'
222
+ ) as HTMLButtonElement | null;
223
+ const bumpBtn = canvasElement.querySelector(
224
+ '#m-bump'
225
+ ) as HTMLButtonElement | null;
226
+ const unitBtn = canvasElement.querySelector(
227
+ '#m-unit'
228
+ ) as HTMLButtonElement | null;
229
+ if (!list || !status || !addBtn || !removeBtn || !bumpBtn || !unitBtn) {
230
+ return;
231
+ }
232
+
233
+ const UNITS = ['kn', 'm', 'C', 'miles'];
234
+ let added = 0;
235
+ const rows = () =>
236
+ Array.from(
237
+ list.querySelectorAll('obc-readout-list-item')
238
+ ) as (HTMLElement & {
239
+ value: number | null;
240
+ unit: string;
241
+ })[];
242
+ const refresh = () => {
243
+ status.textContent = `${rows().length} rows`;
244
+ };
245
+
246
+ addBtn.onclick = () => {
247
+ added += 1;
248
+ const row = document.createElement(
249
+ 'obc-readout-list-item'
250
+ ) as HTMLElement & {label: string; value: number; unit: string};
251
+ row.label = `Row ${added}`;
252
+ row.value = added * 7;
253
+ row.unit = UNITS[added % UNITS.length];
254
+ list.appendChild(row);
255
+ refresh();
256
+ };
257
+ removeBtn.onclick = () => {
258
+ const all = rows();
259
+ all[all.length - 1]?.remove();
260
+ refresh();
261
+ };
262
+ bumpBtn.onclick = () => {
263
+ const first = rows()[0];
264
+ if (!first) return;
265
+ first.value = Math.round((first.value ?? 1) * 10);
266
+ list.align();
267
+ };
268
+ unitBtn.onclick = () => {
269
+ const first = rows()[0];
270
+ if (!first) return;
271
+ first.unit = first.unit === 'miles' ? 'T' : 'miles';
272
+ list.align();
273
+ };
274
+ },
275
+ };
@@ -0,0 +1,216 @@
1
+ import {LitElement, html, unsafeCSS, type PropertyValues} from 'lit';
2
+ import {property} from 'lit/decorators.js';
3
+ import componentStyle from './readout-list.css?inline';
4
+ import {customElement} from '../../decorator.js';
5
+ import '../readout-list-item/readout-list-item.js';
6
+ import {ObcReadoutListItem} from '../readout-list-item/readout-list-item.js';
7
+
8
+ const ITEM_TAG = 'obc-readout-list-item';
9
+
10
+ /** Child attributes whose change should re-trigger alignment (HTML-attribute usage). */
11
+ const OBSERVED_ATTRIBUTES = [
12
+ 'unit',
13
+ 'src',
14
+ 'value',
15
+ 'setpoint',
16
+ 'advice',
17
+ 'max-digits',
18
+ 'fraction-digits',
19
+ 'has-degree',
20
+ 'has-setpoint',
21
+ 'has-advice',
22
+ ];
23
+
24
+ /** Integer-digit count of a numeric value (sign and fraction excluded). */
25
+ function integerDigitCount(value: number | null | undefined): number {
26
+ if (value === null || value === undefined || Number.isNaN(value)) {
27
+ return 0;
28
+ }
29
+ return String(Math.trunc(Math.abs(value))).length;
30
+ }
31
+
32
+ /**
33
+ * `<obc-readout-list>` – A container that groups `<obc-readout-list-item>` rows
34
+ * and **auto-aligns their columns**.
35
+ *
36
+ * Because each row is its own custom element, cross-row column alignment is not
37
+ * automatic. This container inspects its rows and pushes shared width reservers
38
+ * down so the unit column, the value / setpoint / advice columns and the source
39
+ * column all line up — the same effect the `Readout List Item → ColumnAlignment`
40
+ * story achieves by hand, done for you. Alignment is always on.
41
+ *
42
+ * What it equalizes — derived from each row's data and broadcast to every row, so
43
+ * the widest value / unit / source is never clipped:
44
+ * - **Unit:** the longest `unit` becomes every row's unit space-reserver.
45
+ * - **Value / setpoint / advice:** the widest numeric width (max integer digits +
46
+ * max fraction digits across rows, derived from each row's `maxDigits` /
47
+ * `fractionDigits` / current values) is reserved on every row's numeric blocks.
48
+ * Reserving off digit counts keeps it stable as live values update.
49
+ * - **Source:** the longest `src` becomes every row's source space-reserver.
50
+ * - **Degree:** if any row has a degree, non-degree rows reserve the degree column
51
+ * (`hasDegreeSpacer`) so their digits line up with the degree rows; the spacer is
52
+ * cleared once no degree rows remain.
53
+ *
54
+ * The list **owns** these reservers: it recomputes them from the rows' data on
55
+ * every pass (and clears stale reservers / spacers when rows change), so a
56
+ * `spaceReserver` set directly on a row inside the list is overwritten. Drive the
57
+ * data (`maxDigits` / `fractionDigits` / `unit` / `src`) rather than setting a
58
+ * manual reserver when a row lives in a list.
59
+ *
60
+ * Alignment runs on `slotchange` and on child mutations (added/removed rows and
61
+ * HTML-attribute changes). When rows are updated via JS **properties** only (no
62
+ * attribute/DOM mutation), call {@link align} to recompute.
63
+ *
64
+ * @experimental Pilot for the new primitives + per-block options Readout API; the
65
+ * API may change in a future release.
66
+ *
67
+ * @slot - The `<obc-readout-list-item>` rows.
68
+ *
69
+ * @csspart list - The vertical stack container.
70
+ */
71
+ @customElement('obc-readout-list')
72
+ export class ObcReadoutList extends LitElement {
73
+ /**
74
+ * Development aid: outline each row's readout building blocks (red), degree
75
+ * columns (blue) and degree spacer (green) so the reserved column widths are
76
+ * visible. Propagated to every row. Off by default.
77
+ */
78
+ @property({type: Boolean, reflect: true}) showDebugOverlay = false;
79
+
80
+ private mutationObserver?: MutationObserver;
81
+
82
+ protected override updated(changed: PropertyValues): void {
83
+ super.updated(changed);
84
+ // A host-property change is not seen by the child MutationObserver, so
85
+ // re-propagate `showDebugOverlay` (and re-align) when it toggles.
86
+ if (changed.has('showDebugOverlay')) {
87
+ this.align();
88
+ }
89
+ }
90
+
91
+ override disconnectedCallback(): void {
92
+ this.mutationObserver?.disconnect();
93
+ this.mutationObserver = undefined;
94
+ super.disconnectedCallback();
95
+ }
96
+
97
+ /** The `obc-readout-list-item` rows, including those nested in wrapper elements. */
98
+ private get items(): ObcReadoutListItem[] {
99
+ const slot = this.shadowRoot?.querySelector('slot');
100
+ const assigned = slot?.assignedElements({flatten: true}) ?? [];
101
+ return assigned.flatMap((el) =>
102
+ el.tagName.toLowerCase() === ITEM_TAG
103
+ ? [el as ObcReadoutListItem]
104
+ : Array.from(el.querySelectorAll<ObcReadoutListItem>(ITEM_TAG))
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Recompute and apply the shared column reservers across all rows. Call this
110
+ * after updating rows via JS properties only (attribute/DOM changes are picked
111
+ * up automatically).
112
+ */
113
+ align(): void {
114
+ const items = this.items;
115
+ if (items.length === 0) {
116
+ return;
117
+ }
118
+
119
+ let maxIntegerDigits = 0;
120
+ let maxFractionDigits = 0;
121
+ let longestUnit = '';
122
+ let longestSrc = '';
123
+ let anyDegree = false;
124
+
125
+ for (const item of items) {
126
+ maxFractionDigits = Math.max(maxFractionDigits, item.fractionDigits ?? 0);
127
+ maxIntegerDigits = Math.max(
128
+ maxIntegerDigits,
129
+ item.maxDigits ?? 0,
130
+ integerDigitCount(item.value),
131
+ item.hasSetpoint ? integerDigitCount(item.setpoint) : 0,
132
+ item.hasAdvice ? integerDigitCount(item.advice) : 0
133
+ );
134
+ if (item.unit && item.unit.length > longestUnit.length) {
135
+ longestUnit = item.unit;
136
+ }
137
+ if (item.src && item.src.length > longestSrc.length) {
138
+ longestSrc = item.src;
139
+ }
140
+ if (item.hasDegree) {
141
+ anyDegree = true;
142
+ }
143
+ }
144
+
145
+ const numericReserver =
146
+ maxIntegerDigits > 0
147
+ ? '0'.repeat(maxIntegerDigits) +
148
+ (maxFractionDigits > 0 ? `.${'0'.repeat(maxFractionDigits)}` : '')
149
+ : undefined;
150
+
151
+ // Our writes are properties (no reflected attributes), so they do not trigger
152
+ // the MutationObserver; disconnecting around them is belt-and-suspenders.
153
+ this.mutationObserver?.disconnect();
154
+ for (const item of items) {
155
+ // Recompute every reserver / spacer on every pass (do not gate on a value
156
+ // being present), so stale state clears when rows change — e.g. when the
157
+ // last degree row, the last unit, or the last source is removed.
158
+ item.valueOptions = {
159
+ ...item.valueOptions,
160
+ spaceReserver: numericReserver,
161
+ };
162
+ item.setpointOptions = {
163
+ ...item.setpointOptions,
164
+ spaceReserver: numericReserver,
165
+ };
166
+ item.adviceOptions = {
167
+ ...item.adviceOptions,
168
+ spaceReserver: numericReserver,
169
+ };
170
+ item.unitOptions = {
171
+ ...item.unitOptions,
172
+ spaceReserver: longestUnit || undefined,
173
+ };
174
+ item.srcOptions = {
175
+ ...item.srcOptions,
176
+ spaceReserver: longestSrc || undefined,
177
+ };
178
+ item.hasDegreeSpacer = anyDegree && !item.hasDegree;
179
+ item.showDebugOverlay = this.showDebugOverlay;
180
+ }
181
+ this.observeChildren();
182
+ }
183
+
184
+ private observeChildren(): void {
185
+ if (!this.mutationObserver) {
186
+ this.mutationObserver = new MutationObserver(() => this.align());
187
+ }
188
+ this.mutationObserver.observe(this, {
189
+ childList: true,
190
+ subtree: true,
191
+ attributes: true,
192
+ attributeFilter: OBSERVED_ATTRIBUTES,
193
+ });
194
+ }
195
+
196
+ private handleSlotChange = (): void => {
197
+ this.align();
198
+ this.observeChildren();
199
+ };
200
+
201
+ override render() {
202
+ return html`
203
+ <div class="list" part="list">
204
+ <slot @slotchange=${this.handleSlotChange}></slot>
205
+ </div>
206
+ `;
207
+ }
208
+
209
+ static override styles = unsafeCSS(componentStyle);
210
+ }
211
+
212
+ declare global {
213
+ interface HTMLElementTagNameMap {
214
+ 'obc-readout-list': ObcReadoutList;
215
+ }
216
+ }