@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.101 → 2.0.0-next.103

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 (110) hide show
  1. package/bundle/openbridge-webcomponents.bundle.js +580 -143
  2. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  3. package/custom-elements.json +629 -407
  4. package/dist/bars-graphs/donut-chart/donut-chart.d.ts +18 -16
  5. package/dist/bars-graphs/donut-chart/donut-chart.d.ts.map +1 -1
  6. package/dist/bars-graphs/donut-chart/donut-chart.js.map +1 -1
  7. package/dist/bars-graphs/pie-chart/pie-chart.d.ts +14 -12
  8. package/dist/bars-graphs/pie-chart/pie-chart.d.ts.map +1 -1
  9. package/dist/bars-graphs/pie-chart/pie-chart.js.map +1 -1
  10. package/dist/bars-graphs/polar-chart/polar-chart.d.ts +19 -16
  11. package/dist/bars-graphs/polar-chart/polar-chart.d.ts.map +1 -1
  12. package/dist/bars-graphs/polar-chart/polar-chart.js.map +1 -1
  13. package/dist/bars-graphs/radial-bar-chart/radial-bar-chart.d.ts +8 -8
  14. package/dist/bars-graphs/radial-bar-chart/radial-bar-chart.d.ts.map +1 -1
  15. package/dist/bars-graphs/radial-bar-chart/radial-bar-chart.js.map +1 -1
  16. package/dist/building-blocks/chart-line/chart-line-base.d.ts +8 -25
  17. package/dist/building-blocks/chart-line/chart-line-base.d.ts.map +1 -1
  18. package/dist/building-blocks/chart-line/chart-line-base.js.map +1 -1
  19. package/dist/building-blocks/instrument-radial/instrument-radial.d.ts +22 -2
  20. package/dist/building-blocks/instrument-radial/instrument-radial.d.ts.map +1 -1
  21. package/dist/building-blocks/instrument-radial/instrument-radial.js +61 -37
  22. package/dist/building-blocks/instrument-radial/instrument-radial.js.map +1 -1
  23. package/dist/integration-systems/integration-bar/integration-bar.d.ts +20 -24
  24. package/dist/integration-systems/integration-bar/integration-bar.d.ts.map +1 -1
  25. package/dist/integration-systems/integration-bar/integration-bar.js.map +1 -1
  26. package/dist/integration-systems/integration-button/integration-button.d.ts +15 -13
  27. package/dist/integration-systems/integration-button/integration-button.d.ts.map +1 -1
  28. package/dist/integration-systems/integration-button/integration-button.js.map +1 -1
  29. package/dist/navigation-instruments/azimuth-thruster/azimuth-thruster.d.ts +14 -0
  30. package/dist/navigation-instruments/azimuth-thruster/azimuth-thruster.d.ts.map +1 -1
  31. package/dist/navigation-instruments/azimuth-thruster/azimuth-thruster.js +30 -11
  32. package/dist/navigation-instruments/azimuth-thruster/azimuth-thruster.js.map +1 -1
  33. package/dist/navigation-instruments/compass/compass.d.ts +68 -31
  34. package/dist/navigation-instruments/compass/compass.d.ts.map +1 -1
  35. package/dist/navigation-instruments/compass/compass.js +36 -16
  36. package/dist/navigation-instruments/compass/compass.js.map +1 -1
  37. package/dist/navigation-instruments/compass-flat/compass-flat.d.ts +8 -9
  38. package/dist/navigation-instruments/compass-flat/compass-flat.d.ts.map +1 -1
  39. package/dist/navigation-instruments/compass-flat/compass-flat.js.map +1 -1
  40. package/dist/navigation-instruments/compass-sector/compass-sector.d.ts.map +1 -1
  41. package/dist/navigation-instruments/compass-sector/compass-sector.js.map +1 -1
  42. package/dist/navigation-instruments/gauge-radial/gauge-radial.css.js +34 -2
  43. package/dist/navigation-instruments/gauge-radial/gauge-radial.css.js.map +1 -1
  44. package/dist/navigation-instruments/gauge-radial/gauge-radial.d.ts +15 -0
  45. package/dist/navigation-instruments/gauge-radial/gauge-radial.d.ts.map +1 -1
  46. package/dist/navigation-instruments/gauge-radial/gauge-radial.js +35 -1
  47. package/dist/navigation-instruments/gauge-radial/gauge-radial.js.map +1 -1
  48. package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts +0 -5
  49. package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts.map +1 -1
  50. package/dist/navigation-instruments/gauge-trend/gauge-trend.js.map +1 -1
  51. package/dist/navigation-instruments/graph-mini/graph-mini.d.ts +1 -1
  52. package/dist/navigation-instruments/graph-mini/graph-mini.d.ts.map +1 -1
  53. package/dist/navigation-instruments/graph-mini/graph-mini.js.map +1 -1
  54. package/dist/navigation-instruments/heading/heading.d.ts +19 -1
  55. package/dist/navigation-instruments/heading/heading.d.ts.map +1 -1
  56. package/dist/navigation-instruments/heading/heading.js +34 -16
  57. package/dist/navigation-instruments/heading/heading.js.map +1 -1
  58. package/dist/navigation-instruments/indicator-graph/indicator-graph.d.ts +1 -1
  59. package/dist/navigation-instruments/indicator-graph/indicator-graph.d.ts.map +1 -1
  60. package/dist/navigation-instruments/indicator-graph/indicator-graph.js.map +1 -1
  61. package/dist/navigation-instruments/rudder/rudder.d.ts +15 -2
  62. package/dist/navigation-instruments/rudder/rudder.d.ts.map +1 -1
  63. package/dist/navigation-instruments/rudder/rudder.js +36 -26
  64. package/dist/navigation-instruments/rudder/rudder.js.map +1 -1
  65. package/dist/navigation-instruments/speed-gauge/speed-gauge.d.ts +15 -1
  66. package/dist/navigation-instruments/speed-gauge/speed-gauge.d.ts.map +1 -1
  67. package/dist/navigation-instruments/speed-gauge/speed-gauge.js +31 -3
  68. package/dist/navigation-instruments/speed-gauge/speed-gauge.js.map +1 -1
  69. package/dist/navigation-instruments/watch/watch.d.ts +45 -17
  70. package/dist/navigation-instruments/watch/watch.d.ts.map +1 -1
  71. package/dist/navigation-instruments/watch/watch.js +53 -38
  72. package/dist/navigation-instruments/watch/watch.js.map +1 -1
  73. package/dist/svghelpers/radial-frame.d.ts +122 -0
  74. package/dist/svghelpers/radial-frame.d.ts.map +1 -0
  75. package/dist/svghelpers/radial-frame.js +259 -0
  76. package/dist/svghelpers/radial-frame.js.map +1 -0
  77. package/package.json +1 -1
  78. package/script/check-slot-event-docs.ts +35 -0
  79. package/src/bars-graphs/donut-chart/donut-chart.ts +18 -16
  80. package/src/bars-graphs/pie-chart/pie-chart.ts +14 -12
  81. package/src/bars-graphs/polar-chart/polar-chart.ts +19 -16
  82. package/src/bars-graphs/radial-bar-chart/radial-bar-chart.ts +8 -8
  83. package/src/building-blocks/chart-line/chart-line-base.ts +8 -25
  84. package/src/building-blocks/instrument-radial/instrument-radial.stories.ts +187 -6
  85. package/src/building-blocks/instrument-radial/instrument-radial.ts +98 -47
  86. package/src/integration-systems/integration-bar/integration-bar.ts +20 -24
  87. package/src/integration-systems/integration-button/integration-button.ts +15 -13
  88. package/src/navigation-instruments/azimuth-thruster/azimuth-thruster.stories.ts +78 -1
  89. package/src/navigation-instruments/azimuth-thruster/azimuth-thruster.ts +55 -11
  90. package/src/navigation-instruments/compass/compass.stories.ts +88 -1
  91. package/src/navigation-instruments/compass/compass.ts +99 -46
  92. package/src/navigation-instruments/compass-flat/compass-flat.ts +8 -9
  93. package/src/navigation-instruments/compass-sector/compass-sector.ts +7 -0
  94. package/src/navigation-instruments/gauge-radial/gauge-radial.css +32 -2
  95. package/src/navigation-instruments/gauge-radial/gauge-radial.stories.ts +265 -1
  96. package/src/navigation-instruments/gauge-radial/gauge-radial.ts +59 -1
  97. package/src/navigation-instruments/gauge-trend/gauge-trend.ts +0 -5
  98. package/src/navigation-instruments/graph-mini/graph-mini.ts +1 -1
  99. package/src/navigation-instruments/heading/heading.stories.ts +92 -1
  100. package/src/navigation-instruments/heading/heading.ts +50 -16
  101. package/src/navigation-instruments/indicator-graph/indicator-graph.ts +1 -1
  102. package/src/navigation-instruments/rudder/rudder.stories.ts +83 -1
  103. package/src/navigation-instruments/rudder/rudder.ts +61 -34
  104. package/src/navigation-instruments/speed-gauge/speed-gauge.stories.ts +94 -1
  105. package/src/navigation-instruments/speed-gauge/speed-gauge.ts +57 -4
  106. package/src/navigation-instruments/watch/watch.stories.ts +140 -1
  107. package/src/navigation-instruments/watch/watch.ts +100 -60
  108. package/src/storybook-util.ts +59 -0
  109. package/src/svghelpers/radial-frame.spec.ts +307 -0
  110. package/src/svghelpers/radial-frame.ts +508 -0
@@ -35,36 +35,38 @@ export enum IntegrationButtonType {
35
35
  *
36
36
  * @fires click - Fired when the internal button is activated.
37
37
  *
38
- * @property {boolean} hasTrailingIcon - Shows the `trailing-icon` slot.
39
- * @property {boolean} hasTrailingIcon2 - Shows the `trailing-icon2` slot.
40
- * @property {boolean} hasLeadingIcon - Shows the `leading-icon` slot.
41
- * @property {boolean} hasstatus - Shows the `status` slot.
42
- * @property {IntegrationButtonReadout[]} readouts - List of readout items shown in the rich type.
43
- * @property {boolean} disabled - Disables the internal button.
44
- * @property {boolean} activated - Applies active state styling while a selection is pending.
45
- * @property {boolean} selected - Applies selected state styling.
46
- * @property {boolean} dividerBottom - Shows a bottom divider under the button.
47
- * @property {boolean} dividerRight - Shows a right divider to separate from adjacent buttons.
48
- * @property {IntegrationButtonVariant} variant - Visual variant (`normal` or `flat`).
49
- * @property {IntegrationButtonType} type - Layout type (`hug`, `regular`, or `rich`).
50
38
  * @experimental
51
39
  */
52
40
  @customElement('obc-integration-button')
53
41
  export class ObcIntegrationButton extends LitElement {
42
+ /** Shows the `trailing-icon` slot. */
54
43
  @property({type: Boolean}) hasTrailingIcon = false;
55
- /** @availableWhen hasTrailingIcon==true */
44
+ /**
45
+ * Shows the `trailing-icon2` slot.
46
+ * @availableWhen hasTrailingIcon==true
47
+ */
56
48
  @property({type: Boolean}) hasTrailingIcon2 = false;
49
+ /** Shows the `leading-icon` slot. */
57
50
  @property({type: Boolean}) hasLeadingIcon = false;
51
+ /** Shows the `status` slot. */
58
52
  @property({type: Boolean}) hasStatus = false;
53
+ /** List of readout items shown in the rich type. */
59
54
  @property({type: Array, attribute: false})
60
55
  readouts: IntegrationButtonReadout[] = [];
56
+ /** Disables the internal button. */
61
57
  @property({type: Boolean}) disabled = false;
58
+ /** Applies active state styling while a selection is pending. */
62
59
  @property({type: Boolean}) activated = false;
60
+ /** Applies selected state styling. */
63
61
  @property({type: Boolean}) selected = false;
62
+ /** Shows a bottom divider under the button. */
64
63
  @property({type: Boolean}) dividerBottom = false;
64
+ /** Shows a right divider to separate from adjacent buttons. */
65
65
  @property({type: Boolean}) dividerRight = false;
66
+ /** Visual variant (`normal` or `flat`). */
66
67
  @property({type: String}) variant: IntegrationButtonVariant =
67
68
  IntegrationButtonVariant.normal;
69
+ /** Layout type (`hug`, `regular`, or `rich`). */
68
70
  @property({type: String}) type: IntegrationButtonType =
69
71
  IntegrationButtonType.regular;
70
72
 
@@ -2,7 +2,13 @@ import type {Meta, StoryObj} from '@storybook/web-components-vite';
2
2
  import {ObcAzimuthThruster} from './azimuth-thruster.js';
3
3
  import './azimuth-thruster.js';
4
4
  import {InstrumentState, Priority, Size} from '../types.js';
5
- import {widthDecorator} from '../../storybook-util.js';
5
+ import {html} from 'lit';
6
+ import {
7
+ playgroundColumn,
8
+ resizableStoryBox,
9
+ storyHint,
10
+ widthDecorator,
11
+ } from '../../storybook-util.js';
6
12
  import {AdviceType} from '../watch/advice.js';
7
13
  import {TickmarkStyle} from '../watch/tickmark.js';
8
14
  import {PropellerType} from '../thruster/propeller.js';
@@ -12,6 +18,11 @@ const meta: Meta<typeof ObcAzimuthThruster> = {
12
18
  tags: ['autodocs', '6.0'],
13
19
  component: 'obc-azimuth-thruster',
14
20
  argTypes: {
21
+ faceDiameter: {
22
+ control: {type: 'range', min: 100, max: 600, step: 10},
23
+ description:
24
+ 'Pins the outer-ring diameter in px (fixed intrinsic size, equal circumference across instruments). Clear to return to fill-the-container sizing.',
25
+ },
15
26
  thrust: {control: {type: 'range', min: -100, max: 100, step: 1}},
16
27
  thrustSetpoint: {control: {type: 'range', min: -100, max: 100, step: 1}},
17
28
  angle: {control: {type: 'range', min: -180, max: 180, step: 1}},
@@ -256,3 +267,69 @@ export const OffWithAngleSetpointOverride: Story = {
256
267
  priority: Priority.enhanced,
257
268
  },
258
269
  };
270
+
271
+ type SizingPlaygroundArgs = Partial<ObcAzimuthThruster> & {
272
+ lockFaceDiameter?: boolean;
273
+ };
274
+
275
+ /**
276
+ * Interactive sizing playground: drag the dashed box's bottom-right corner to
277
+ * resize it. The first thruster is pinned to a fixed intrinsic size by the
278
+ * `faceDiameter` control, while the second adapts to the remaining flex
279
+ * space, reserving room for its degree labels adaptively (issue #1021).
280
+ * Enable `lockFaceDiameter` to pin both to the same circumference. Related:
281
+ * *Sizing Playground* stories under Building Blocks/Watch, Building
282
+ * Blocks/Instrument Radial and Instruments/Gauge Radial.
283
+ */
284
+ export const SizingPlayground: StoryObj<SizingPlaygroundArgs> = {
285
+ name: 'Sizing Playground — FaceDiameter + Resizable (Manual)',
286
+ tags: ['skip-test'],
287
+ parameters: {widthDecorator: false},
288
+ args: {
289
+ faceDiameter: 240,
290
+ lockFaceDiameter: false,
291
+ },
292
+ argTypes: {
293
+ lockFaceDiameter: {
294
+ control: 'boolean',
295
+ description:
296
+ 'Apply faceDiameter to every instance (equal circumference) instead of only the first.',
297
+ },
298
+ },
299
+ render: (args) => {
300
+ const instances = [
301
+ {label: 'thruster A', angle: 45, thrust: 60},
302
+ {label: 'thruster B', angle: -120, thrust: 35},
303
+ ];
304
+ const fd = (index: number) =>
305
+ index === 0 || args.lockFaceDiameter ? args.faceDiameter : undefined;
306
+ const caption = (index: number, label: string) =>
307
+ fd(index) !== undefined
308
+ ? `${label} — pinned ${fd(index)}px`
309
+ : `${label} — adaptive (flex)`;
310
+ return html`
311
+ ${storyHint(
312
+ 'Drag the bottom-right corner of the dashed box to resize it. The first thruster is pinned by the faceDiameter control; the second adapts to the remaining flex space. Enable lockFaceDiameter to pin both to the same circumference.'
313
+ )}
314
+ ${resizableStoryBox(
315
+ html`
316
+ ${instances.map((g, index) =>
317
+ playgroundColumn(
318
+ caption(index, g.label),
319
+ html`
320
+ <obc-azimuth-thruster
321
+ .angle=${g.angle}
322
+ .thrust=${g.thrust}
323
+ .showLabels=${true}
324
+ .faceDiameter=${fd(index)}
325
+ ></obc-azimuth-thruster>
326
+ `,
327
+ {pinned: fd(index) !== undefined}
328
+ )
329
+ )}
330
+ `,
331
+ {width: 680, height: 400}
332
+ )}
333
+ `;
334
+ },
335
+ };
@@ -1,12 +1,20 @@
1
1
  import {html, LitElement, unsafeCSS} from 'lit';
2
2
  import {property} from 'lit/decorators.js';
3
3
  import type {PropertyValues} from 'lit';
4
+ import {ResizeController} from '@lit-labs/observers/resize-controller.js';
5
+ import {
6
+ applyPinnedHostSize,
7
+ computeRadialFrame,
8
+ estimateLabelWidthPx,
9
+ measureContainerPx,
10
+ observeInnerBox,
11
+ type RadialFrame,
12
+ } from '../../svghelpers/radial-frame.js';
4
13
  import {InstrumentState, Priority} from '../types.js';
5
14
  import {SetpointBundle} from '../../svghelpers/setpoint-bundle.js';
6
15
  import {thruster} from '../thruster/thruster.js';
7
16
  import '../watch/watch.js';
8
17
  import componentStyle from './azimuth-thruster.css?inline';
9
- import {ifDefined} from 'lit/directives/if-defined.js';
10
18
  import {AdviceState, AngleAdvice, AngleAdviceRaw} from '../watch/advice.js';
11
19
  import {Tickmark, TickmarkStyle, TickmarkType} from '../watch/tickmark.js';
12
20
  import {LinearAdvice} from '../thruster/advice.js';
@@ -134,6 +142,36 @@ export class ObcAzimuthThruster extends LitElement {
134
142
  @property({type: String}) tickmarkStyle: TickmarkStyle =
135
143
  TickmarkStyle.regular;
136
144
  @property({type: Boolean}) starboardPortIndicator: boolean = false;
145
+ /**
146
+ * Outer-ring diameter in CSS pixels. When set, the instrument renders at a
147
+ * fixed intrinsic size derived from the ring, arc shape and label reserve —
148
+ * so instruments sharing the same value have identical ring circumference
149
+ * regardless of label width or arc extent (like obc-donut-chart's
150
+ * fixedHeight). When unset (default), the instrument fills its container.
151
+ */
152
+ @property({type: Number, attribute: 'face-diameter'})
153
+ faceDiameter: number | undefined;
154
+
155
+ private _frame: RadialFrame | undefined;
156
+
157
+ /** Whether the host size styles were set by applyPinnedHostSize. */
158
+ private _hostSizePinned = false;
159
+
160
+ private _resizeController = new ResizeController(this, {});
161
+
162
+ override firstUpdated(changed: PropertyValues): void {
163
+ super.firstUpdated(changed);
164
+ observeInnerBox(this._resizeController, this.renderRoot);
165
+ }
166
+
167
+ override updated(changed: PropertyValues): void {
168
+ super.updated(changed);
169
+ this._hostSizePinned = applyPinnedHostSize(
170
+ this,
171
+ this._frame,
172
+ this._hostSizePinned
173
+ );
174
+ }
137
175
 
138
176
  private get angleAdviceRaw(): AngleAdviceRaw[] {
139
177
  return this.angleAdvices.map((advice) => {
@@ -236,20 +274,27 @@ export class ObcAzimuthThruster extends LitElement {
236
274
 
237
275
  const tickmarks = this.getTickmarks();
238
276
 
239
- let viewBox: string;
240
- if (!this.hasLabelSpacer) {
241
- viewBox = '-192 -192 384 384';
242
- } else if (this.showLabels && !this.tickmarksInside) {
243
- viewBox = '-236 -236 472 472';
244
- } else {
245
- viewBox = '-200 -200 400 400';
246
- }
277
+ const frame = computeRadialFrame({
278
+ basePadding: this.hasLabelSpacer ? 24 : 16,
279
+ labelWidthPx:
280
+ this.hasLabelSpacer && !this.tickmarksInside
281
+ ? estimateLabelWidthPx(tickmarks.map((t) => t.text))
282
+ : 0,
283
+ containerPx: measureContainerPx(this),
284
+ faceDiameter: this.faceDiameter,
285
+ });
286
+ this._frame = frame;
287
+ const shownTickmarks = frame.labelsHidden
288
+ ? tickmarks.map((t) => ({...t, text: undefined}))
289
+ : tickmarks;
290
+ const viewBox = frame.viewBox;
247
291
 
248
292
  return html`
249
293
  <div class="container">
250
294
  <obc-watch
251
295
  .touching=${this.touching}
252
- .tickmarks=${tickmarks}
296
+ .arcFrame=${frame}
297
+ .tickmarks=${shownTickmarks}
253
298
  .state=${this.state}
254
299
  .priority=${this.priority}
255
300
  .angleSetpoint=${this.angleSetpoint}
@@ -260,7 +305,6 @@ export class ObcAzimuthThruster extends LitElement {
260
305
  .animateSetpoint=${this.animateSetpoint}
261
306
  .tickmarksInside=${this.tickmarksInside}
262
307
  .tickmarkStyle=${this.tickmarkStyle}
263
- padding=${ifDefined(!this.hasLabelSpacer ? 16 : undefined)}
264
308
  .advices=${this.angleAdviceRaw}
265
309
  .starboardPortIndicator=${this.starboardPortIndicator}
266
310
  ></obc-watch>
@@ -6,7 +6,13 @@ import {
6
6
  RotType,
7
7
  } from './compass.js';
8
8
  import './compass.js';
9
- import {widthDecorator} from '../../storybook-util.js';
9
+ import {html} from 'lit';
10
+ import {
11
+ playgroundColumn,
12
+ resizableStoryBox,
13
+ storyHint,
14
+ widthDecorator,
15
+ } from '../../storybook-util.js';
10
16
  import {AdviceType} from '../watch/advice.js';
11
17
  import {VesselImage} from '../watch/watch.js';
12
18
  import {topVessels} from '../watch/vessels/storybook-helper.js';
@@ -48,6 +54,11 @@ const meta: Meta<typeof ObcCompass> = {
48
54
  },
49
55
  argTypes: {
50
56
  width: {control: {type: 'range', min: 32, max: 1028, step: 1}},
57
+ faceDiameter: {
58
+ control: {type: 'range', min: 100, max: 600, step: 10},
59
+ description:
60
+ 'Pins the outer-ring diameter in px (fixed intrinsic size, equal circumference across instruments). Clear to return to fill-the-container sizing.',
61
+ },
51
62
  heading: {control: {type: 'range', min: 0, max: 360, step: 1}},
52
63
  courseOverGround: {control: {type: 'range', min: 0, max: 360, step: 1}},
53
64
  headingSetpoint: {control: {type: 'range', min: 0, max: 360, step: 1}},
@@ -171,3 +182,79 @@ export const WithRateOfTurnDegreesPerMinute: Story = {
171
182
  priorityElements: [CompassPriorityElement.hdg, CompassPriorityElement.rot],
172
183
  },
173
184
  };
185
+
186
+ export const SmallContainer: Story = {
187
+ name: 'Small Container (250px, Labels + Decor Reserve)',
188
+ args: {
189
+ width: 250,
190
+ },
191
+ };
192
+
193
+ type SizingPlaygroundArgs = Partial<ObcCompass> & {
194
+ lockFaceDiameter?: boolean;
195
+ };
196
+
197
+ /**
198
+ * Interactive sizing playground: drag the dashed box's bottom-right corner to
199
+ * resize it. The first compass is pinned to a fixed intrinsic size by the
200
+ * `faceDiameter` control, while the second adapts to the remaining flex
201
+ * space, reserving room for the NSWE labels, north arrow and wind symbols
202
+ * adaptively (issue #1021). Enable `lockFaceDiameter` to pin both to the same
203
+ * circumference. Related: *Sizing Playground* stories under Building
204
+ * Blocks/Watch, Building Blocks/Instrument Radial and Instruments/Gauge
205
+ * Radial.
206
+ */
207
+ export const SizingPlayground: StoryObj<SizingPlaygroundArgs> = {
208
+ name: 'Sizing Playground — FaceDiameter + Resizable (Manual)',
209
+ tags: ['skip-test'],
210
+ parameters: {widthDecorator: false},
211
+ args: {
212
+ faceDiameter: 240,
213
+ lockFaceDiameter: false,
214
+ },
215
+ argTypes: {
216
+ lockFaceDiameter: {
217
+ control: 'boolean',
218
+ description:
219
+ 'Apply faceDiameter to every instance (equal circumference) instead of only the first.',
220
+ },
221
+ },
222
+ render: (args) => {
223
+ const instances = [
224
+ {label: 'compass A', heading: 311},
225
+ {label: 'compass B', heading: 45},
226
+ ];
227
+ const fd = (index: number) =>
228
+ index === 0 || args.lockFaceDiameter ? args.faceDiameter : undefined;
229
+ const caption = (index: number, label: string) =>
230
+ fd(index) !== undefined
231
+ ? `${label} — pinned ${fd(index)}px`
232
+ : `${label} — adaptive (flex)`;
233
+ return html`
234
+ ${storyHint(
235
+ 'Drag the bottom-right corner of the dashed box to resize it. The first compass is pinned by the faceDiameter control; the second adapts to the remaining flex space. Enable lockFaceDiameter to pin both to the same circumference.'
236
+ )}
237
+ ${resizableStoryBox(
238
+ html`
239
+ ${instances.map((g, index) =>
240
+ playgroundColumn(
241
+ caption(index, g.label),
242
+ html`
243
+ <obc-compass
244
+ .heading=${g.heading}
245
+ .courseOverGround=${g.heading + 27}
246
+ .currentWindSpeedKnots=${20}
247
+ .windFromDirection=${45}
248
+ .showLabels=${true}
249
+ .faceDiameter=${fd(index)}
250
+ ></obc-compass>
251
+ `,
252
+ {pinned: fd(index) !== undefined}
253
+ )
254
+ )}
255
+ `,
256
+ {width: 680, height: 400}
257
+ )}
258
+ `;
259
+ },
260
+ };
@@ -5,6 +5,14 @@ import {Tickmark, TickmarkType} from '../watch/tickmark.js';
5
5
  import {arrow, ArrowStyle} from './arrow.js';
6
6
  import {AdviceState, AngleAdvice, AngleAdviceRaw} from '../watch/advice.js';
7
7
  import {ResizeController} from '@lit-labs/observers/resize-controller.js';
8
+ import {
9
+ applyPinnedHostSize,
10
+ computeRadialFrame,
11
+ measureContainerPx,
12
+ NORTH_ARROW_WIDTH_PX,
13
+ NSWE_LABEL_WIDTH_PX,
14
+ type RadialFrame,
15
+ } from '../../svghelpers/radial-frame.js';
8
16
  import {
9
17
  VesselImage,
10
18
  VesselImageSize,
@@ -59,9 +67,9 @@ export enum CompassPriorityElement {
59
67
  * Supports spinning dots (`rotType="dots"`) — the dot animation is
60
68
  * amplified by `rotDotAnimationFactor` so small physical values still
61
69
  * read at a glance — and a banana-shaped arc bar (`rotType="bar"`)
62
- * showing the HDG→COG span. Bar extent is driven by the physical value
63
- * only (gain is not applied). Position on the outer scale ring or inner
64
- * circle via `rotPosition`.
70
+ * showing the rate of turn as an arc anchored at the current heading.
71
+ * Bar extent is driven by the physical value only (gain is not applied).
72
+ * Position on the outer scale ring or inner circle via `rotPosition`.
65
73
  * - **Environmental overlays**: Wind speed/direction and current
66
74
  * speed/direction indicators on the watch face.
67
75
  * - **Vessel image**: Configurable vessel silhouette centered on the
@@ -94,25 +102,6 @@ export enum CompassPriorityElement {
94
102
  * ></obc-compass>
95
103
  * ```
96
104
  *
97
- * @property {number} heading - The current heading of the vessel in degrees.
98
- * @property {number} courseOverGround - The current course over ground in degrees.
99
- * @property {number | null} headingSetpoint - The set point for the heading in degrees.
100
- * @property {boolean} atHeadingSetpoint - Indicates if the vessel is at the heading set point.
101
- * @property {boolean} autoAtHeadingSetpoint - Enables automatic at heading set point calculation.
102
- * @property {number} autoAtHeadingSetpointDeadband - The deadband for the heading set point in degrees.
103
- * @property {boolean} touching - Indicates if the compass is being touched.
104
- * @property {Array<AngleAdvice>} headingAdvices - An array of angle advices for the compass.
105
- * @property {number | null} currentWindSpeedKnots - The wind speed in knots.
106
- * @property {number | null} windFromDirection - The direction the wind is coming from in degrees.
107
- * @property {number | null} currentSpeed - The current speed, number of arrows.
108
- * @property {number | null} currentFromDirection - The direction the current is coming from in degrees.
109
- * @property {VesselImage} vesselImage - The image of the vessel.
110
- * @property {number|undefined} rateOfTurnDegreesPerMinute - Measured rate of turn in degrees per minute (the AIS / ITU-R M.1371 convention). Sign controls direction (positive = starboard / clockwise). Drives both the bar extent and the dot animation.
111
- * @property {number} rotDotAnimationFactor - Visual amplification for the dot animation only. Default `18` (≈1 rpm at 20°/min).
112
- * @property {number} rotationsPerMinute - **Deprecated.** Use `rateOfTurnDegreesPerMinute` instead. Kept as a backward-compatible fallback when `rateOfTurnDegreesPerMinute` is `undefined`.
113
- * @property {RotType} rotType - ROT display mode: `'dots'` (spinning dots, default) or `'bar'` (arc bar from HDG to COG).
114
- * @property {RotPosition} rotPosition - ROT track position: `'innerCircle'` (default) or `'scale'` (on the outer ring).
115
- * @property {Priority} priority - Color priority: `Priority.enhanced` uses the blue/enhanced color palette, `Priority.regular` (default) uses the standard palette.
116
105
  *
117
106
  * @ignition-base-height: 512px
118
107
  * @ignition-base-width: 512px
@@ -120,36 +109,65 @@ export enum CompassPriorityElement {
120
109
  */
121
110
  @customElement('obc-compass')
122
111
  export class ObcCompass extends LitElement {
112
+ /** The current heading of the vessel in degrees. */
123
113
  @property({type: Number}) heading = 0;
114
+ /** The current course over ground in degrees. */
124
115
  @property({type: Number}) courseOverGround = 0;
125
116
 
117
+ /** The set point for the heading in degrees. */
126
118
  @property({type: Number}) headingSetpoint: number | null = null;
127
119
  /** @availableWhen headingSetpoint!=null */
128
120
  @property({type: Number}) newHeadingSetpoint: number | undefined;
129
- /** @availableWhen headingSetpoint!=null && autoAtHeadingSetpoint==false */
121
+ /**
122
+ * Indicates if the vessel is at the heading set point.
123
+ * @availableWhen headingSetpoint!=null && autoAtHeadingSetpoint==false
124
+ */
130
125
  @property({type: Boolean}) atHeadingSetpoint: boolean = false;
131
126
  /** @availableWhen headingSetpoint!=null */
132
127
  @property({type: Number}) headingSetpointAtZeroDeadband: number = 0.5;
133
128
  /** @availableWhen headingSetpoint!=null */
134
129
  @property({type: Boolean}) headingSetpointOverride: boolean = false;
135
- /** @availableWhen headingSetpoint!=null */
130
+ /**
131
+ * Enables automatic at heading set point calculation.
132
+ * @availableWhen headingSetpoint!=null
133
+ */
136
134
  @property({type: Boolean, attribute: false}) autoAtHeadingSetpoint: boolean =
137
135
  true;
138
- /** @availableWhen headingSetpoint!=null && autoAtHeadingSetpoint==true */
136
+ /**
137
+ * The deadband for the heading set point in degrees.
138
+ * @availableWhen headingSetpoint!=null && autoAtHeadingSetpoint==true
139
+ */
139
140
  @property({type: Number}) autoAtHeadingSetpointDeadband: number = 2;
140
141
  /** @availableWhen headingSetpoint!=null */
141
142
  @property({type: Boolean}) animateSetpoint: boolean = false;
142
- /** @availableWhen headingSetpoint!=null */
143
+ /**
144
+ * Indicates if the compass is being touched.
145
+ * @availableWhen headingSetpoint!=null
146
+ */
143
147
  @property({type: Boolean}) touching: boolean = false;
148
+ /** An array of angle advices for the compass. */
144
149
  @property({type: Array, attribute: false}) headingAdvices: AngleAdvice[] = [];
145
- /** @availableWhen windFromDirection!=null */
150
+ /**
151
+ * The wind speed in knots.
152
+ * @availableWhen windFromDirection!=null
153
+ */
146
154
  @property({type: Number}) currentWindSpeedKnots: number | null = null;
147
- /** @availableWhen currentWindSpeedKnots!=null */
155
+ /**
156
+ * The direction the wind is coming from in degrees.
157
+ * @availableWhen currentWindSpeedKnots!=null
158
+ */
148
159
  @property({type: Number}) windFromDirection: number | null = null;
149
- /** @availableWhen currentFromDirection!=null */
160
+ /**
161
+ * The current speed, number of arrows.
162
+ * @availableWhen currentFromDirection!=null
163
+ */
150
164
  @property({type: Number}) currentSpeed: number | null = null;
151
- /** @availableWhen currentSpeed!=null */
165
+ /**
166
+ * The direction the current is coming from in degrees.
167
+ * @availableWhen currentSpeed!=null
168
+ */
152
169
  @property({type: Number}) currentFromDirection: number | null = null;
170
+ /** The image of the vessel. */
153
171
  @property({type: String}) vesselImage: VesselImage = VesselImage.genericTop;
154
172
  /**
155
173
  * Measured rate of turn in degrees per minute (positive = starboard).
@@ -166,12 +184,18 @@ export class ObcCompass extends LitElement {
166
184
  */
167
185
  @property({type: Number}) rotDotAnimationFactor: number = 18;
168
186
  /**
187
+ * Legacy rate-of-turn input, in rotations per minute. When
188
+ * `rateOfTurnDegreesPerMinute` is `undefined`, this value is used
189
+ * (unconverted) as the fallback ROT, driving both the spinning dot ring
190
+ * and the bar extent.
169
191
  * @deprecated Use `rateOfTurnDegreesPerMinute` (and optionally
170
192
  * `rotDotAnimationFactor`) instead. Takes effect only when
171
193
  * `rateOfTurnDegreesPerMinute` is `undefined`.
172
194
  */
173
195
  @property({type: Number}) rotationsPerMinute: number = 1;
196
+ /** ROT display mode: `'dots'` (spinning dots, default) or `'bar'` (a rate-of-turn arc anchored at the current heading, its length proportional to the rate of turn). */
174
197
  @property({type: String}) rotType: RotType = RotType.dots;
198
+ /** ROT track position: `'innerCircle'` (default) or `'scale'` (on the outer ring). */
175
199
  @property({type: String}) rotPosition: RotPosition = RotPosition.innerCircle;
176
200
  /**
177
201
  * Bar-extent reference value in **degrees per minute**. The bar fills the
@@ -192,6 +216,7 @@ export class ObcCompass extends LitElement {
192
216
  @property({type: String}) direction: CompassDirection =
193
217
  CompassDirection.NorthUp;
194
218
  @property({type: String}) state: InstrumentState = InstrumentState.active;
219
+ /** Color priority: `Priority.enhanced` uses the blue/enhanced color palette, `Priority.regular` (default) uses the standard palette. */
195
220
  @property({type: String}) priority: Priority = Priority.regular;
196
221
  /** @availableWhen priority==enhanced */
197
222
  @property({type: Array, attribute: false})
@@ -200,6 +225,15 @@ export class ObcCompass extends LitElement {
200
225
  @property({type: Boolean}) showLabels: boolean = false;
201
226
  /** When true, labels and north arrow are placed inside the outer ring. */
202
227
  @property({type: Boolean}) tickmarksInside: boolean = false;
228
+ /**
229
+ * Outer-ring diameter in CSS pixels. When set, the instrument renders at a
230
+ * fixed intrinsic size derived from the ring, arc shape and label reserve —
231
+ * so instruments sharing the same value have identical ring circumference
232
+ * regardless of label width or arc extent (like obc-donut-chart's
233
+ * fixedHeight). When unset (default), the instrument fills its container.
234
+ */
235
+ @property({type: Number, attribute: 'face-diameter'})
236
+ faceDiameter: number | undefined;
203
237
 
204
238
  private _headingSp = new SetpointBundle({
205
239
  angularWraparound: true,
@@ -240,18 +274,32 @@ export class ObcCompass extends LitElement {
240
274
  return this.rateOfTurnDegreesPerMinute ?? this.rotationsPerMinute;
241
275
  }
242
276
 
243
- private getPadding() {
244
- const size = Math.min(this.clientHeight, this.clientWidth);
245
- const deltaWidth = 512 - size;
246
- const steps = deltaWidth / 128;
247
- let deltaPadding;
248
- if (deltaWidth > 0) {
249
- deltaPadding = steps * 48;
250
- } else {
251
- deltaPadding = steps * 6;
277
+ /**
278
+ * Pixel cost of the outside decor: the always-rendered north-arrow glyph
279
+ * plus, only while shown, the NSWE labels (both keep a constant on-screen
280
+ * size via `1/scale` terms). Feeds the frame's width-aware reserve,
281
+ * replacing the former empirical `72 + delta(clientSize)` padding.
282
+ * Wind/current symbols at their default radius are covered by the base
283
+ * padding of 72.
284
+ */
285
+ private getOutsideDecorPx(): number {
286
+ if (this.tickmarksInside) {
287
+ return 0;
252
288
  }
289
+ return NORTH_ARROW_WIDTH_PX + (this.showLabels ? NSWE_LABEL_WIDTH_PX : 0);
290
+ }
291
+
292
+ /** Whether the host size styles were set by applyPinnedHostSize. */
293
+ private _hostSizePinned = false;
294
+ private _frame: RadialFrame | undefined;
253
295
 
254
- return 72 + deltaPadding;
296
+ override updated(changed: PropertyValues): void {
297
+ super.updated(changed);
298
+ this._hostSizePinned = applyPinnedHostSize(
299
+ this,
300
+ this._frame,
301
+ this._hostSizePinned
302
+ );
255
303
  }
256
304
 
257
305
  private get angleAdviceRaw(): AngleAdviceRaw[] {
@@ -298,23 +346,28 @@ export class ObcCompass extends LitElement {
298
346
  {angle: 270, type: TickmarkType.main},
299
347
  ];
300
348
 
301
- const padding = this.getPadding();
302
- const width = (176 + padding) * 2;
303
- const viewBox = `-${width / 2} -${width / 2} ${width} ${width}`;
349
+ const frame = computeRadialFrame({
350
+ basePadding: 72,
351
+ labelWidthPx: this.getOutsideDecorPx(),
352
+ containerPx: measureContainerPx(this),
353
+ faceDiameter: this.faceDiameter,
354
+ });
355
+ this._frame = frame;
356
+ const viewBox = frame.viewBox;
304
357
 
305
358
  return html`
306
359
  <div class="container">
307
360
  <obc-watch
308
361
  .touching=${this.touching}
309
- .padding=${padding}
362
+ .arcFrame=${frame}
310
363
  .advices=${this.angleAdviceRaw}
311
364
  .tickmarks=${tickmarks}
312
365
  .state=${this.state}
313
366
  .watchCircleType=${WatchCircleType.triple}
314
- .showLabels=${this.showLabels}
367
+ .showLabels=${this.showLabels && !frame.labelsHidden}
315
368
  .tickmarksInside=${this.tickmarksInside}
316
369
  .crosshairEnabled=${true}
317
- .northArrow=${true}
370
+ .northArrow=${!frame.labelsHidden}
318
371
  .angleSetpoint=${this.headingSetpoint ?? undefined}
319
372
  .newAngleSetpoint=${this.newHeadingSetpoint}
320
373
  .atAngleSetpoint=${this._headingSp.computeAtSetpoint(this.heading)}
@@ -58,14 +58,6 @@ export interface Label {
58
58
  * - **Color priority**: Per-element priority for HDG, COG, and ROT via
59
59
  * `priorityElements`.
60
60
  *
61
- * @property {number} heading - Current heading in degrees.
62
- * @property {number} courseOverGround - Current COG in degrees.
63
- * @property {RotType|undefined} rotType - ROT display mode: `'dots'`, `'bar'`, or `undefined` (hidden).
64
- * @property {number|undefined} rateOfTurnDegreesPerMinute - Measured rate of turn in degrees per minute (positive = starboard). Drives the bar extent and (after `× rotDotAnimationFactor`) the dot animation.
65
- * @property {number} rotDotAnimationFactor - Visual amplification for the dot animation only. Default `18` (≈1 rpm at 20°/min).
66
- * @property {number} rotationsPerMinute - **Deprecated.** Use `rateOfTurnDegreesPerMinute` instead.
67
- * @property {number} rotMaxValue - Bar-extent reference value in **degrees per minute**. Default `60` per ES-TRIN 2025/1 Art. 3.02.
68
- * @property {number} rotArcExtent - Degrees of bar arc per max-value ROT (default 60).
69
61
  *
70
62
  * @ignition-base-height: 170px
71
63
  * @ignition-base-width: 512px
@@ -74,7 +66,9 @@ export interface Label {
74
66
  @customElement('obc-compass-flat')
75
67
  export class ObcCompassFlat extends LitElement {
76
68
  @property({type: Boolean}) FOVIndicator: boolean = false;
69
+ /** Current heading in degrees. */
77
70
  @property({type: Number}) heading = 0;
71
+ /** Current COG in degrees. */
78
72
  @property({type: Number}) courseOverGround = 0;
79
73
  @property({type: Number}) tickInterval = 5;
80
74
  @property({type: Number}) FOV = 45;
@@ -85,6 +79,7 @@ export class ObcCompassFlat extends LitElement {
85
79
  priorityElements: CompassFlatPriorityElement[] = [
86
80
  CompassFlatPriorityElement.hdg,
87
81
  ];
82
+ /** ROT display mode: `'dots'`, `'bar'`, or `undefined` (hidden). */
88
83
  @property({type: String}) rotType: RotType | undefined;
89
84
  /**
90
85
  * Measured rate of turn in degrees per minute (positive = starboard).
@@ -98,6 +93,7 @@ export class ObcCompassFlat extends LitElement {
98
93
  */
99
94
  @property({type: Number}) rotDotAnimationFactor: number = 18;
100
95
  /**
96
+ * **Deprecated.** Use `rateOfTurnDegreesPerMinute` instead.
101
97
  * @deprecated Use `rateOfTurnDegreesPerMinute` instead.
102
98
  * @availableWhen rotType!=undefined
103
99
  */
@@ -108,7 +104,10 @@ export class ObcCompassFlat extends LitElement {
108
104
  * @availableWhen rotType!=undefined
109
105
  */
110
106
  @property({type: Number}) rotMaxValue: number = 60;
111
- /** @availableWhen rotType!=undefined */
107
+ /**
108
+ * Degrees of bar arc per max-value ROT (default 60).
109
+ * @availableWhen rotType!=undefined
110
+ */
112
111
  @property({type: Number}) rotArcExtent: number = 60;
113
112
  /** @availableWhen rotType!=undefined */
114
113
  @property({type: Boolean}) rotPortStarboard: boolean = false;
@@ -31,6 +31,13 @@ export enum CompassSectorPriorityElement {
31
31
  rot = 'rot',
32
32
  }
33
33
 
34
+ // Fixed frame padding for both the zoomed and un-zoomed paths. This component
35
+ // deliberately keeps its bespoke FOV-compression geometry and does NOT use
36
+ // svghelpers/radial-frame.ts: the viewBox is cached per FOV (a
37
+ // container-size-dependent label reserve would invalidate that), and the
38
+ // 72-unit padding covers the 3-char degree labels at typical sizes.
39
+ // TODO(#1021): adopt computeRadialFrame if degree labels ever clip when the
40
+ // component is shrunk far below its design size.
34
41
  const PADDING = 72;
35
42
  const WATCH_TYPE = WatchCircleType.triple;
36
43
  const INNER_RADIUS = innerRingRadiusFor(WATCH_TYPE);