@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,355 @@
1
+ import {LitElement, html, nothing, unsafeCSS, type TemplateResult} from 'lit';
2
+ import {property, state} from 'lit/decorators.js';
3
+ import {classMap} from 'lit/directives/class-map.js';
4
+ import componentStyle from './readout-block.css?inline';
5
+ import {customElement} from '../../decorator.js';
6
+ import '../../components/textbox/textbox.js';
7
+ import {
8
+ ObcTextboxSize,
9
+ ObcTextboxFontWeight,
10
+ ObcTextboxAlignment,
11
+ } from '../../components/textbox/textbox.js';
12
+ import '../../icons/icon-input-right.js';
13
+ import '../../icons/icon-notification-advice.js';
14
+ import {
15
+ formatNumericValue,
16
+ readoutFormattedInteger,
17
+ type ReadoutNumericFormatOptions,
18
+ } from '../../navigation-instruments/readout/readout-formatters.js';
19
+ import {
20
+ type AlertFrameConfig,
21
+ wrapWithAlertFrame,
22
+ } from '../../components/alert-frame/alert-frame.js';
23
+
24
+ // Re-exported so consumers can configure typography without a second import path.
25
+ export {
26
+ ObcTextboxSize,
27
+ ObcTextboxFontWeight,
28
+ ObcTextboxAlignment,
29
+ } from '../../components/textbox/textbox.js';
30
+
31
+ /**
32
+ * Semantic variant of the block. Drives the default marker icon and the
33
+ * colour token; the layout is identical across variants.
34
+ * - `value`: the primary reading (no default marker icon).
35
+ * - `setpoint`: a setpoint reference (default `input-right` marker).
36
+ * - `advice`: an advisory reference (default `notification-advice` marker).
37
+ */
38
+ export enum ReadoutBlockVariant {
39
+ value = 'value',
40
+ setpoint = 'setpoint',
41
+ advice = 'advice',
42
+ }
43
+
44
+ /**
45
+ * Density tier of the block — drives icon size, the icon↔number gap, and the
46
+ * degree-column width tier. The number typography size is controlled
47
+ * independently via {@link ObcReadoutBlock.valueSize} so a parent can de-emphasise
48
+ * one block relative to another within the same tier.
49
+ */
50
+ export enum ReadoutBlockSize {
51
+ small = 'small',
52
+ medium = 'medium',
53
+ large = 'large',
54
+ }
55
+
56
+ /**
57
+ * Per-block measurement quality, rendered as a non-shifting outline chip.
58
+ * - `low-integrity`: the value is suspect.
59
+ * - `invalid`: the value is invalid.
60
+ */
61
+ export enum ReadoutBlockDataQuality {
62
+ lowIntegrity = 'low-integrity',
63
+ invalid = 'invalid',
64
+ }
65
+
66
+ /**
67
+ * Pop-up fade phase for a setpoint block. Driven by the parent's setpoint
68
+ * interaction state machine; only meaningful for `role="setpoint"`.
69
+ */
70
+ export enum ReadoutBlockHidePhase {
71
+ none = 'none',
72
+ hiding = 'hiding',
73
+ hidden = 'hidden',
74
+ }
75
+
76
+ /**
77
+ * `<obc-readout-block>` – the most atomic readout primitive: a single
78
+ * cap-height-aligned, width-reservable numeric segment (value / setpoint /
79
+ * advice).
80
+ *
81
+ * It renders one `obc-textbox` number with optional hinted leading zeros, a
82
+ * reservable width, an optional leading marker icon (via the `icon` slot or the
83
+ * role default), an optional trailing degree glyph, an `off`/unavailable text
84
+ * state, per-block data-quality and an optional per-block alert frame.
85
+ *
86
+ * This is a building block used inside `obc-readout-list-item` (and, in a future
87
+ * refactor, inside `obc-readout`); it is not normally used on its own. Colour is
88
+ * inherited from the host context (the parent sets the role colour), so the
89
+ * block stays neutral until placed.
90
+ *
91
+ * @experimental Pilot for the new primitives + per-block options Readout API; the
92
+ * API may change in a future release.
93
+ *
94
+ * @slot icon - Replaces the role's default marker icon.
95
+ *
96
+ * @csspart block - The block container (carries role / tone / data-quality).
97
+ * @csspart block-content - The number + degree group.
98
+ * @csspart block-text - The `obc-textbox` rendering the number.
99
+ * @csspart block-icon - The leading marker-icon container.
100
+ * @csspart degree - The trailing degree-glyph column.
101
+ */
102
+ @customElement('obc-readout-block')
103
+ export class ObcReadoutBlock extends LitElement {
104
+ /** Semantic variant (value / setpoint / advice). */
105
+ @property({type: String}) variant: ReadoutBlockVariant =
106
+ ReadoutBlockVariant.value;
107
+
108
+ /** The numeric value; `null`/`undefined` renders a dash. */
109
+ @property({type: Number}) value: number | null = null;
110
+
111
+ /** Density tier — icon size, gap, degree tier. */
112
+ @property({type: String}) size: ReadoutBlockSize = ReadoutBlockSize.small;
113
+
114
+ /**
115
+ * Resolved number-typography size. When unset it is derived from `size`
116
+ * (small→s, medium→m, large→l), so a parent that de-emphasises a block (e.g.
117
+ * a secondary setpoint) can pass a smaller size without changing the tier.
118
+ */
119
+ @property({type: String}) valueSize?: ObcTextboxSize;
120
+
121
+ /** Accent (in-command) colour tone. */
122
+ @property({type: Boolean}) enhanced = false;
123
+
124
+ /** Number font weight (regular / semibold / bold); colour is independent. */
125
+ @property({type: String}) weight: ObcTextboxFontWeight =
126
+ ObcTextboxFontWeight.regular;
127
+
128
+ /** Render the trailing cap-height degree glyph (`°`). */
129
+ @property({type: Boolean}) hasDegree = false;
130
+
131
+ /** Show the leading marker-icon container (always on for setpoint/advice). */
132
+ @property({type: Boolean}) hasIcon = false;
133
+
134
+ /** Number of fraction digits. */
135
+ @property({type: Number}) fractionDigits = 0;
136
+
137
+ /** Integer digits to reserve / hint (independent of `fractionDigits`). */
138
+ @property({type: Number}) maxDigits = 0;
139
+
140
+ /** Render muted leading zeros filling the integer part to `maxDigits`. */
141
+ @property({type: Boolean}) hintedZeros = false;
142
+
143
+ /** Explicit longest string to reserve width for (e.g. `"0000.0"`). */
144
+ @property({type: String}) spaceReserver?: string;
145
+
146
+ /** Render `offText` instead of a number (e.g. equipment powered down). */
147
+ @property({type: Boolean}) off = false;
148
+
149
+ /** Text shown when `off` is true. */
150
+ @property({type: String}) offText = 'OFF';
151
+
152
+ /** Text alignment of the number within its reserved width. */
153
+ @property({type: String}) alignment: ObcTextboxAlignment =
154
+ ObcTextboxAlignment.Right;
155
+
156
+ /** Per-block measurement quality (outline chip). */
157
+ @property({type: String}) dataQuality?: ReadoutBlockDataQuality;
158
+
159
+ // `boolean | …` (not `false | …`): the generated Angular wrapper widens a
160
+ // literal-`false` union to `boolean`. `wrapWithAlertFrame` treats any
161
+ // non-object as "no frame", so accepting `boolean` is harmless.
162
+ /** Per-block alert frame; nests inside any parent alert frame. */
163
+ @property({type: Object}) alert: boolean | AlertFrameConfig = false;
164
+
165
+ /** Setpoint focus (touch) state — only meaningful for `role="setpoint"`. */
166
+ @property({type: Boolean}) touching = false;
167
+
168
+ /** Setpoint pop-up fade phase — only meaningful for `role="setpoint"`. */
169
+ @property({type: String}) hidePhase: ReadoutBlockHidePhase =
170
+ ReadoutBlockHidePhase.none;
171
+
172
+ @state() private hasAssignedIcon = false;
173
+
174
+ private get resolvedValueSize(): ObcTextboxSize {
175
+ if (this.valueSize) {
176
+ return this.valueSize;
177
+ }
178
+ switch (this.size) {
179
+ case ReadoutBlockSize.large:
180
+ return ObcTextboxSize.l;
181
+ case ReadoutBlockSize.medium:
182
+ return ObcTextboxSize.m;
183
+ default:
184
+ return ObcTextboxSize.s;
185
+ }
186
+ }
187
+
188
+ private get numericFormatOptions(): ReadoutNumericFormatOptions {
189
+ return {
190
+ showZeroPadding: false,
191
+ minValueLength: this.maxDigits,
192
+ fractionDigits: this.fractionDigits,
193
+ };
194
+ }
195
+
196
+ /** Widest possible value string for width reservation (e.g. `"000.0"`). */
197
+ private get reserverText(): string {
198
+ const maxDigits = this.maxDigits;
199
+ if (maxDigits <= 0) {
200
+ return '';
201
+ }
202
+ const integer = '0'.repeat(Math.max(maxDigits, 1));
203
+ return this.fractionDigits > 0
204
+ ? `${integer}.${'0'.repeat(this.fractionDigits)}`
205
+ : integer;
206
+ }
207
+
208
+ /**
209
+ * Effective width reserver: the wider of the explicit `spaceReserver` and the
210
+ * `maxDigits`/`fractionDigits`-derived reserve, so an explicit reserver can
211
+ * never reserve *less* than the formatted value needs. Under tabular-nums the
212
+ * rendered width is proportional to character count, so "wider" compares length.
213
+ */
214
+ private widerReserver(explicit: string | undefined, derived: string): string {
215
+ if (!explicit) {
216
+ return derived;
217
+ }
218
+ if (!derived) {
219
+ return explicit;
220
+ }
221
+ return explicit.length >= derived.length ? explicit : derived;
222
+ }
223
+
224
+ private dataQualityClasses(): Record<string, boolean> {
225
+ return {
226
+ 'data-low-integrity':
227
+ this.dataQuality === ReadoutBlockDataQuality.lowIntegrity,
228
+ 'data-invalid': this.dataQuality === ReadoutBlockDataQuality.invalid,
229
+ };
230
+ }
231
+
232
+ private onIconSlotChange = (event: Event): void => {
233
+ // `slotchange` fires after the first render once content is (re)assigned,
234
+ // so it covers the initial state without a firstUpdated re-render.
235
+ this.hasAssignedIcon =
236
+ (event.target as HTMLSlotElement).assignedElements({flatten: true})
237
+ .length > 0;
238
+ };
239
+
240
+ private renderIcon(): TemplateResult | typeof nothing {
241
+ // Value blocks only show an icon when asked; setpoint/advice always reserve
242
+ // their marker. The role default is rendered as a direct child (so it sizes
243
+ // via `.block-icon obi-*`) and hidden once an icon is assigned to the slot —
244
+ // `{flatten: true}` sees through a forwarded-but-empty parent slot, so the
245
+ // default still shows when the consumer overrides nothing.
246
+ const showIcon = this.variant !== ReadoutBlockVariant.value || this.hasIcon;
247
+ if (!showIcon) {
248
+ return nothing;
249
+ }
250
+ let fallback: TemplateResult | typeof nothing = nothing;
251
+ if (this.variant === ReadoutBlockVariant.setpoint) {
252
+ fallback = html`<obi-input-right></obi-input-right>`;
253
+ } else if (this.variant === ReadoutBlockVariant.advice) {
254
+ fallback = html`<obi-notification-advice></obi-notification-advice>`;
255
+ }
256
+ return html`<span class="block-icon" part="block-icon" aria-hidden="true">
257
+ <slot name="icon" @slotchange=${this.onIconSlotChange}></slot>
258
+ ${this.hasAssignedIcon ? nothing : fallback}
259
+ </span>`;
260
+ }
261
+
262
+ /**
263
+ * A cap-height `°` column whose width scales with the number size. Inherits the
264
+ * block's colour.
265
+ */
266
+ private renderDegreeGlyph(size: ObcTextboxSize): TemplateResult {
267
+ return html`
268
+ <span
269
+ class=${classMap({
270
+ 'degree-column': true,
271
+ [`degree-${size}`]: true,
272
+ 'degree-inherit': true,
273
+ })}
274
+ part="degree"
275
+ >
276
+ <obc-textbox class="degree-glyph" .size=${size} alignment="center"
277
+ >°</obc-textbox
278
+ >
279
+ </span>
280
+ `;
281
+ }
282
+
283
+ override render() {
284
+ const valueSize = this.resolvedValueSize;
285
+ const formatOptions = this.numericFormatOptions;
286
+ const valueForFormat = this.value ?? undefined;
287
+ const text = this.off
288
+ ? this.offText
289
+ : formatNumericValue(valueForFormat, formatOptions);
290
+ // Hinted zeros pad the INTEGER part up to `maxDigits`, independent of
291
+ // `fractionDigits` (the decimal point and fraction digits never count toward
292
+ // `maxDigits`). Negative / dashed values are not padded. Example: value 1.2,
293
+ // maxDigits 3, fractionDigits 1 → "001.2".
294
+ const hintCount =
295
+ this.off ||
296
+ !this.hintedZeros ||
297
+ valueForFormat === undefined ||
298
+ valueForFormat < 0
299
+ ? 0
300
+ : Math.max(this.maxDigits - readoutFormattedInteger(text), 0);
301
+ const hinted = hintCount > 0 ? '0'.repeat(hintCount) : '';
302
+ // Hinted zeros own the width — they already fill to `maxDigits` — so when
303
+ // `hintedZeros` is enabled an explicit `spaceReserver` is ignored (it has
304
+ // higher priority). Otherwise the wider of the explicit reserver and the
305
+ // `maxDigits`-derived reserve wins.
306
+ const reserver = this.hintedZeros
307
+ ? this.reserverText
308
+ : this.widerReserver(this.spaceReserver, this.reserverText);
309
+
310
+ const block = html`
311
+ <div
312
+ class=${classMap({
313
+ block: true,
314
+ [`block-${this.variant}`]: true,
315
+ [`size-${this.size}`]: true,
316
+ 'tone-enhanced': this.enhanced,
317
+ touching: this.touching,
318
+ 'is-hiding': this.hidePhase === ReadoutBlockHidePhase.hiding,
319
+ 'is-hidden': this.hidePhase === ReadoutBlockHidePhase.hidden,
320
+ ...this.dataQualityClasses(),
321
+ })}
322
+ part="block block-${this.variant}"
323
+ >
324
+ ${this.renderIcon()}
325
+ <span class="block-content" part="block-content">
326
+ <obc-textbox
327
+ class="block-text"
328
+ part="block-text"
329
+ .size=${valueSize}
330
+ .fontWeight=${this.weight}
331
+ .alignment=${this.alignment}
332
+ .tabularNums=${true}
333
+ >
334
+ ${hinted
335
+ ? html`<span class="hinted-zero" aria-hidden="true"
336
+ >${hinted}</span
337
+ >`
338
+ : nothing}${text}
339
+ ${reserver ? html`<span slot="length">${reserver}</span>` : nothing}
340
+ </obc-textbox>
341
+ ${this.hasDegree ? this.renderDegreeGlyph(valueSize) : nothing}
342
+ </span>
343
+ </div>
344
+ `;
345
+ return wrapWithAlertFrame(this.alert ?? false, block);
346
+ }
347
+
348
+ static override styles = unsafeCSS(componentStyle);
349
+ }
350
+
351
+ declare global {
352
+ interface HTMLElementTagNameMap {
353
+ 'obc-readout-block': ObcReadoutBlock;
354
+ }
355
+ }
@@ -4,45 +4,68 @@
4
4
  height: 100%;
5
5
  }
6
6
 
7
- /* Host takes each sector's crop aspect (see `sectorClips`): 270/90 square, 180
8
- wide so the dial is the same size at any width.
7
+ .gauge-radial-root {
8
+ /* Readout vertical placement per layout (% of the cropped box), tuned to the
9
+ watch geometry — re-check if the ring radii or the 448 box change. Which
10
+ layout uses which is in renderReadouts(). */
11
+ --readout-meta-top: 72%;
12
+ --readout-meta-top-needle: 63%;
13
+ --readout-meta-top-180: 60%;
14
+
15
+ position: relative;
9
16
 
10
- `width`/`height: auto` + `max-width`/`max-height: 100%` makes the host the
11
- largest box of the sector's aspect ratio that fits inside *both* the
12
- available width and height. Using `height: auto` alone only honours the
13
- width, so in a short, wide (e.g. flex) container the square would grow as
14
- tall as it is wide and overflow see issue #992. */
15
- :host([sector="270"]),
16
- :host([sector="90-left"]),
17
- :host([sector="90-right"]) {
17
+ /* The dial is the largest box of its sector's aspect ratio that fits inside
18
+ *both* the available width and height of the (full-size) host. `width: auto`
19
+ stretches the block to the host width, then `aspect-ratio` + `max-height`
20
+ transfer the constraint back, shrinking it to fit a short host too — see
21
+ issue #992. The host always fills its container, so the gauge can be
22
+ positioned within the leftover space via the alignment classes below. */
18
23
  width: auto;
19
24
  height: auto;
20
25
  max-width: 100%;
21
26
  max-height: 100%;
22
27
  aspect-ratio: 1;
23
- }
24
28
 
25
- /* 448 / 251 = the 180° crop's aspect (448 wide × 448·(1 44%) tall); keep in
26
- sync with `sectorClips` bottom=44 in gauge-radial.ts. */
27
- :host([sector="180"]) {
28
- width: auto;
29
- height: auto;
30
- max-width: 100%;
31
- max-height: 100%;
32
- aspect-ratio: 448 / 251;
33
- }
29
+ /* Defaults (centered both ways); overridden by the halign-* / valign-*
30
+ classes. Horizontal alignment uses auto margins (which reference the host
31
+ width); vertical alignment uses a relative `top` offset (which references
32
+ the definite host height) plus a self-sized `translateY`. */
33
+ margin-left: auto;
34
+ margin-right: auto;
35
+ top: 50%;
36
+ transform: translateY(-50%);
34
37
 
35
- .gauge-radial-root {
36
- /* Readout vertical placement per layout (% of the cropped host), tuned to the
37
- watch geometry — re-check if the ring radii or the 448 box change. Which
38
- layout uses which is in renderReadouts(). */
39
- --readout-meta-top: 72%;
40
- --readout-meta-top-needle: 63%;
41
- --readout-meta-top-180: 60%;
38
+ /* 448 / 251 = the 180° crop's aspect (448 wide × 448·(1 − 44%) tall); keep in
39
+ sync with `sectorClips` bottom=44 in gauge-radial.ts. */
40
+ &.sector-180 {
41
+ aspect-ratio: 448 / 251;
42
+ }
42
43
 
43
- position: relative;
44
- width: 100%;
45
- height: 100%;
44
+ &.halign-left {
45
+ margin-left: 0;
46
+ margin-right: auto;
47
+ }
48
+ &.halign-center {
49
+ margin-left: auto;
50
+ margin-right: auto;
51
+ }
52
+ &.halign-right {
53
+ margin-left: auto;
54
+ margin-right: 0;
55
+ }
56
+
57
+ &.valign-top {
58
+ top: 0;
59
+ transform: translateY(0);
60
+ }
61
+ &.valign-center {
62
+ top: 50%;
63
+ transform: translateY(-50%);
64
+ }
65
+ &.valign-bottom {
66
+ top: 100%;
67
+ transform: translateY(-100%);
68
+ }
46
69
 
47
70
  .gauge-readout-value {
48
71
  position: absolute;
@@ -1,6 +1,8 @@
1
1
  import type {Meta, StoryObj} from '@storybook/web-components-vite';
2
2
  import {
3
+ GaugeRadialHorizontalAlignment,
3
4
  GaugeRadialSector,
5
+ GaugeRadialVerticalAlignment,
4
6
  ObcGaugeRadial,
5
7
  ObcGaugeRadialType,
6
8
  } from './gauge-radial.js';
@@ -28,6 +30,14 @@ const meta = {
28
30
  height: {control: {type: 'range', min: 100, max: 1000, step: 1}},
29
31
  type: {control: 'select', options: Object.values(ObcGaugeRadialType)},
30
32
  sector: {control: 'select', options: Object.values(GaugeRadialSector)},
33
+ horizontalAlignment: {
34
+ control: 'select',
35
+ options: Object.values(GaugeRadialHorizontalAlignment),
36
+ },
37
+ verticalAlignment: {
38
+ control: 'select',
39
+ options: Object.values(GaugeRadialVerticalAlignment),
40
+ },
31
41
  priority: {control: 'select', options: Object.values(Priority)},
32
42
  state: {control: 'select', options: Object.values(InstrumentState)},
33
43
  tickmarkStyle: {
@@ -259,6 +269,46 @@ export const ShortWideContainer180: Story = {
259
269
  },
260
270
  };
261
271
 
272
+ export const ShortWideAlignedLeft: Story = {
273
+ args: {
274
+ ...sectorStoryArgs,
275
+ sector: GaugeRadialSector.deg270,
276
+ width: 600,
277
+ height: 160,
278
+ horizontalAlignment: GaugeRadialHorizontalAlignment.left,
279
+ },
280
+ };
281
+
282
+ export const ShortWideAlignedRight: Story = {
283
+ args: {
284
+ ...sectorStoryArgs,
285
+ sector: GaugeRadialSector.deg270,
286
+ width: 600,
287
+ height: 160,
288
+ horizontalAlignment: GaugeRadialHorizontalAlignment.right,
289
+ },
290
+ };
291
+
292
+ export const TallNarrowAlignedTop: Story = {
293
+ args: {
294
+ ...sectorStoryArgs,
295
+ sector: GaugeRadialSector.deg270,
296
+ width: 200,
297
+ height: 500,
298
+ verticalAlignment: GaugeRadialVerticalAlignment.top,
299
+ },
300
+ };
301
+
302
+ export const TallNarrowAlignedBottom: Story = {
303
+ args: {
304
+ ...sectorStoryArgs,
305
+ sector: GaugeRadialSector.deg270,
306
+ width: 200,
307
+ height: 500,
308
+ verticalAlignment: GaugeRadialVerticalAlignment.bottom,
309
+ },
310
+ };
311
+
262
312
  const readoutStoryArgs = {
263
313
  value: 123,
264
314
  minValue: 0,
@@ -27,6 +27,18 @@ export enum GaugeRadialSector {
27
27
  deg90Right = '90-right',
28
28
  }
29
29
 
30
+ export enum GaugeRadialHorizontalAlignment {
31
+ left = 'left',
32
+ center = 'center',
33
+ right = 'right',
34
+ }
35
+
36
+ export enum GaugeRadialVerticalAlignment {
37
+ top = 'top',
38
+ center = 'center',
39
+ bottom = 'bottom',
40
+ }
41
+
30
42
  export interface GaugeRadialAdvice {
31
43
  minValue: number;
32
44
  maxValue: number;
@@ -59,6 +71,12 @@ export interface GaugeRadialAdvice {
59
71
  * - **Advice zones**: Pass an array of {@link GaugeRadialAdvice} objects to
60
72
  * render caution/alert arcs on the gauge. Not shown on the `90-left` /
61
73
  * `90-right` sectors.
74
+ * - **Alignment**: The host always fills its container and the dial shrinks to
75
+ * fit the smaller of the available width/height, so a short, wide (or tall,
76
+ * narrow) container leaves slack on one axis. `horizontalAlignment`
77
+ * (`left` | `center` | `right`) and `verticalAlignment`
78
+ * (`top` | `center` | `bottom`) position the dial within that slack; both
79
+ * default to `center`.
62
80
  *
63
81
  * ## Usage Guidelines
64
82
  *
@@ -125,6 +143,21 @@ export class ObcGaugeRadial extends SetpointMixin(LitElement) {
125
143
  @property({type: Array, attribute: false}) advices: GaugeRadialAdvice[] = [];
126
144
  @property({type: String, reflect: true}) sector: GaugeRadialSector =
127
145
  GaugeRadialSector.deg270;
146
+ /**
147
+ * Horizontal placement of the dial when the host is wider than the dial
148
+ * (e.g. a short, wide container shrinks the dial to fit the height, leaving
149
+ * horizontal slack). Default `center`.
150
+ */
151
+ @property({type: String})
152
+ horizontalAlignment: GaugeRadialHorizontalAlignment =
153
+ GaugeRadialHorizontalAlignment.center;
154
+ /**
155
+ * Vertical placement of the dial when the host is taller than the dial
156
+ * (e.g. a tall, narrow container shrinks the dial to fit the width, leaving
157
+ * vertical slack). Default `center`.
158
+ */
159
+ @property({type: String}) verticalAlignment: GaugeRadialVerticalAlignment =
160
+ GaugeRadialVerticalAlignment.center;
128
161
  /**
129
162
  * When `true`, shows the centre `<obc-readout>`(s) with the current value
130
163
  * (and optional `label`/`unit`). Layout depends on `sector` and `type`.
@@ -281,6 +314,8 @@ export class ObcGaugeRadial extends SetpointMixin(LitElement) {
281
314
  'sector-180': this.sector === GaugeRadialSector.deg180,
282
315
  'sector-90-left': this.sector === GaugeRadialSector.deg90Left,
283
316
  'sector-90-right': this.sector === GaugeRadialSector.deg90Right,
317
+ [`halign-${this.horizontalAlignment}`]: true,
318
+ [`valign-${this.verticalAlignment}`]: true,
284
319
  })}
285
320
  >
286
321
  <obc-instrument-radial
@@ -0,0 +1,13 @@
1
+ :host {
2
+ display: block;
3
+ width: 100%;
4
+ }
5
+
6
+ /* Vertical stack of rows. The 4px row gap matches the rich detail rows in
7
+ `obc-automation-tank`; override via `--obc-readout-list-row-gap`. */
8
+ .list {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: var(--obc-readout-list-row-gap, 4px);
12
+ width: 100%;
13
+ }