@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
@@ -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 {WatchCircleType} from '../watch/watch.js';
9
17
  import {SetpointBundle} from '../../svghelpers/setpoint-bundle.js';
10
18
  import {Priority} from '../types.js';
@@ -58,6 +66,15 @@ export class ObcHeading extends LitElement {
58
66
  @property({type: Boolean}) showLabels: boolean = false;
59
67
  /** When true, labels and north arrow are placed inside the outer ring. */
60
68
  @property({type: Boolean}) tickmarksInside: boolean = false;
69
+ /**
70
+ * Outer-ring diameter in CSS pixels. When set, the instrument renders at a
71
+ * fixed intrinsic size derived from the ring, arc shape and label reserve —
72
+ * so instruments sharing the same value have identical ring circumference
73
+ * regardless of label width or arc extent (like obc-donut-chart's
74
+ * fixedHeight). When unset (default), the instrument fills its container.
75
+ */
76
+ @property({type: Number, attribute: 'face-diameter'})
77
+ faceDiameter: number | undefined;
61
78
 
62
79
  private _headingSp = new SetpointBundle({
63
80
  angularWraparound: true,
@@ -88,18 +105,30 @@ export class ObcHeading extends LitElement {
88
105
  // function is called on resize of the element
89
106
  private _resizeController = new ResizeController(this, {});
90
107
 
91
- private getPadding() {
92
- const size = Math.min(this.clientHeight, this.clientWidth);
93
- const deltaWidth = 512 - size;
94
- const steps = deltaWidth / 128;
95
- let deltaPadding;
96
- if (deltaWidth > 0) {
97
- deltaPadding = steps * 48;
98
- } else {
99
- deltaPadding = steps * 6;
108
+ /**
109
+ * Pixel cost of the outside decor: the always-rendered north-arrow glyph
110
+ * plus, only while shown, the NSWE labels (both keep a constant on-screen
111
+ * size via `1/scale` terms). Feeds the frame's width-aware reserve,
112
+ * replacing the former empirical `72 + delta(clientSize)` padding.
113
+ */
114
+ private getOutsideDecorPx(): number {
115
+ if (this.tickmarksInside) {
116
+ return 0;
100
117
  }
118
+ return NORTH_ARROW_WIDTH_PX + (this.showLabels ? NSWE_LABEL_WIDTH_PX : 0);
119
+ }
101
120
 
102
- return 72 + deltaPadding;
121
+ /** Whether the host size styles were set by applyPinnedHostSize. */
122
+ private _hostSizePinned = false;
123
+ private _frame: RadialFrame | undefined;
124
+
125
+ override updated(changed: PropertyValues): void {
126
+ super.updated(changed);
127
+ this._hostSizePinned = applyPinnedHostSize(
128
+ this,
129
+ this._frame,
130
+ this._hostSizePinned
131
+ );
103
132
  }
104
133
 
105
134
  private get angleAdviceRaw(): AngleAdviceRaw[] {
@@ -140,22 +169,27 @@ export class ObcHeading extends LitElement {
140
169
  {angle: 270, type: TickmarkType.main},
141
170
  ];
142
171
 
143
- const padding = this.getPadding();
144
- const width = (176 + padding) * 2;
145
- const viewBox = `-${width / 2} -${width / 2} ${width} ${width}`;
172
+ const frame = computeRadialFrame({
173
+ basePadding: 72,
174
+ labelWidthPx: this.getOutsideDecorPx(),
175
+ containerPx: measureContainerPx(this),
176
+ faceDiameter: this.faceDiameter,
177
+ });
178
+ this._frame = frame;
179
+ const viewBox = frame.viewBox;
146
180
 
147
181
  return html`
148
182
  <div class="container">
149
183
  <obc-watch
150
184
  .touching=${this.touching}
151
- .padding=${padding}
185
+ .arcFrame=${frame}
152
186
  .advices=${this.angleAdviceRaw}
153
187
  .tickmarks=${tickmarks}
154
188
  .watchCircleType=${WatchCircleType.single}
155
- .showLabels=${this.showLabels}
189
+ .showLabels=${this.showLabels && !frame.labelsHidden}
156
190
  .tickmarksInside=${this.tickmarksInside}
157
191
  .crosshairEnabled=${true}
158
- .northArrow=${true}
192
+ .northArrow=${!frame.labelsHidden}
159
193
  .angleSetpoint=${this.headingSetpoint ?? undefined}
160
194
  .newAngleSetpoint=${this.newHeadingSetpoint}
161
195
  .atAngleSetpoint=${this._headingSp.computeAtSetpoint(this.heading)}
@@ -26,11 +26,11 @@ export interface ObcIndicatorGraphLayout {
26
26
  * @element obc-indicator-graph
27
27
  * @description A mini graph component
28
28
  *
29
- * @property {Array} data - The data to display in the graph, first array is the x values, second array is the y values
30
29
  * @beta
31
30
  */
32
31
  @customElement('obc-indicator-graph')
33
32
  export class ObcIndicatorGraph extends LitElement {
33
+ /** The data to display in the graph, first array is the x values, second array is the y values */
34
34
  @property({type: Array})
35
35
  data: [number[], number[]] = [[], []];
36
36
 
@@ -1,7 +1,13 @@
1
1
  import type {Meta, StoryObj} from '@storybook/web-components-vite';
2
2
  import {ObcRudder, ObcRudderVariant} from './rudder.js';
3
3
  import './rudder.js';
4
- import {widthDecorator} from '../../storybook-util.js';
4
+ import {html} from 'lit';
5
+ import {
6
+ playgroundColumn,
7
+ resizableStoryBox,
8
+ storyHint,
9
+ widthDecorator,
10
+ } from '../../storybook-util.js';
5
11
  import {TickmarkStyle} from '../watch/tickmark.js';
6
12
  import {InstrumentState, Priority} from '../types.js';
7
13
  const meta: Meta<typeof ObcRudder> = {
@@ -20,6 +26,11 @@ const meta: Meta<typeof ObcRudder> = {
20
26
  },
21
27
  argTypes: {
22
28
  width: {control: {type: 'range', min: 32, max: 1028, step: 1}},
29
+ faceDiameter: {
30
+ control: {type: 'range', min: 100, max: 600, step: 10},
31
+ description:
32
+ 'Pins the outer-ring diameter in px (fixed intrinsic size, equal circumference across instruments). Clear to return to fill-the-container sizing.',
33
+ },
23
34
  angle: {control: {type: 'range', min: -90, max: 90, step: 1}},
24
35
  maxAngle: {control: {type: 'range', min: 2, max: 90, step: 1}},
25
36
  setpoint: {control: {type: 'range', min: -90, max: 90, step: 1}},
@@ -72,3 +83,74 @@ export const ZoomedInNarrow: Story = {
72
83
  showLabels: true,
73
84
  },
74
85
  };
86
+
87
+ type SizingPlaygroundArgs = Partial<ObcRudder> & {
88
+ lockFaceDiameter?: boolean;
89
+ };
90
+
91
+ /**
92
+ * Interactive sizing playground: drag the dashed box's bottom-right corner to
93
+ * resize it. The first rudder is pinned to a fixed intrinsic size by the
94
+ * `faceDiameter` control (its box is wider than tall, matching the 40% top
95
+ * clip), the second adapts to the remaining flex space, and the third is a
96
+ * zoomed narrow arc (`zoomToFitArc`) showing that the reserve composes with
97
+ * zoom (issue #1021). Enable `lockFaceDiameter` to pin all three to the same
98
+ * circumference. Related: *Sizing Playground* stories under Building
99
+ * Blocks/Watch, Building Blocks/Instrument Radial and Instruments/Gauge
100
+ * Radial.
101
+ */
102
+ export const SizingPlayground: StoryObj<SizingPlaygroundArgs> = {
103
+ name: 'Sizing Playground — FaceDiameter + Resizable (Manual)',
104
+ tags: ['skip-test'],
105
+ parameters: {widthDecorator: false},
106
+ args: {
107
+ faceDiameter: 260,
108
+ lockFaceDiameter: false,
109
+ },
110
+ argTypes: {
111
+ lockFaceDiameter: {
112
+ control: 'boolean',
113
+ description:
114
+ 'Apply faceDiameter to every instance (equal circumference) instead of only the first.',
115
+ },
116
+ },
117
+ render: (args) => {
118
+ const instances = [
119
+ {label: 'half circle', maxAngle: 45, zoom: false},
120
+ {label: 'half circle', maxAngle: 45, zoom: false},
121
+ {label: 'zoomed ±20°', maxAngle: 20, zoom: true},
122
+ ];
123
+ const fd = (index: number) =>
124
+ index === 0 || args.lockFaceDiameter ? args.faceDiameter : undefined;
125
+ const caption = (index: number, label: string) =>
126
+ fd(index) !== undefined
127
+ ? `${label} — pinned ${fd(index)}px`
128
+ : `${label} — adaptive (flex)`;
129
+ return html`
130
+ ${storyHint(
131
+ 'Drag the bottom-right corner of the dashed box to resize it. The first rudder is pinned by the faceDiameter control; the second and the zoomed narrow arc adapt to the remaining flex space. Enable lockFaceDiameter to pin all three to the same circumference.'
132
+ )}
133
+ ${resizableStoryBox(
134
+ html`
135
+ ${instances.map((g, index) =>
136
+ playgroundColumn(
137
+ caption(index, g.label),
138
+ html`
139
+ <obc-rudder
140
+ .angle=${15}
141
+ .setpoint=${30}
142
+ .maxAngle=${g.maxAngle}
143
+ .zoomToFitArc=${g.zoom}
144
+ .showLabels=${true}
145
+ .faceDiameter=${fd(index)}
146
+ ></obc-rudder>
147
+ `,
148
+ {pinned: fd(index) !== undefined}
149
+ )
150
+ )}
151
+ `,
152
+ {width: 760, height: 280}
153
+ )}
154
+ `;
155
+ },
156
+ };
@@ -1,20 +1,21 @@
1
- import {LitElement, css, html, nothing, svg} from 'lit';
1
+ import {LitElement, PropertyValues, css, html, nothing, svg} from 'lit';
2
2
  import {property} from 'lit/decorators.js';
3
+ import {ResizeController} from '@lit-labs/observers/resize-controller.js';
3
4
  import '../watch/watch.js';
4
5
  import {Tickmark, TickmarkStyle, TickmarkType} from '../watch/tickmark.js';
5
- import {
6
- OUTER_RING_RADIUS,
7
- WatchCircleType,
8
- innerRingRadiusFor,
9
- } from '../watch/watch.js';
6
+ import {WatchCircleType, innerRingRadiusFor} from '../watch/watch.js';
10
7
  import {InstrumentState, Priority} from '../types.js';
11
8
  import {SetpointMixin} from '../../svghelpers/setpoint-mixin.js';
12
9
  import {AdviceState, AngleAdvice, AngleAdviceRaw} from '../watch/advice.js';
13
10
  import {customElement} from '../../decorator.js';
14
11
  import {
15
- computeZoomToFitArcFrame,
16
- type ZoomToFitArcFrame,
17
- } from '../../svghelpers/arc-frame.js';
12
+ applyPinnedHostSize,
13
+ computeRadialFrame,
14
+ estimateLabelWidthPx,
15
+ measureContainerPx,
16
+ observeInnerBox,
17
+ type RadialFrame,
18
+ } from '../../svghelpers/radial-frame.js';
18
19
 
19
20
  export enum ObcRudderVariant {
20
21
  Bar = 'bar',
@@ -95,9 +96,37 @@ export class ObcRudder extends SetpointMixin(LitElement) {
95
96
  TickmarkStyle.regular;
96
97
  @property({type: Array, attribute: false}) advices: AngleAdvice[] = [];
97
98
  @property({type: Boolean}) zoomToFitArc: boolean = false;
99
+ /**
100
+ * Outer-ring diameter in CSS pixels. When set, the instrument renders at a
101
+ * fixed intrinsic size derived from the ring, arc shape and label reserve —
102
+ * so instruments sharing the same value have identical ring circumference
103
+ * regardless of label width or arc extent (like obc-donut-chart's
104
+ * fixedHeight). When unset (default), the instrument fills its container.
105
+ */
106
+ @property({type: Number, attribute: 'face-diameter'})
107
+ faceDiameter: number | undefined;
98
108
 
99
109
  private _radiusOffset = 0;
100
- private _arcFrame: ZoomToFitArcFrame | undefined;
110
+ private _frame: RadialFrame | undefined;
111
+
112
+ /** Whether the host size styles were set by applyPinnedHostSize. */
113
+ private _hostSizePinned = false;
114
+
115
+ private _resizeController = new ResizeController(this, {});
116
+
117
+ override firstUpdated(changed: PropertyValues): void {
118
+ super.firstUpdated(changed);
119
+ observeInnerBox(this._resizeController, this.renderRoot);
120
+ }
121
+
122
+ override updated(changed: PropertyValues): void {
123
+ super.updated(changed);
124
+ this._hostSizePinned = applyPinnedHostSize(
125
+ this,
126
+ this._frame,
127
+ this._hostSizePinned
128
+ );
129
+ }
101
130
 
102
131
  private get _needleTransform(): string {
103
132
  const rOff = this._radiusOffset;
@@ -243,33 +272,32 @@ export class ObcRudder extends SetpointMixin(LitElement) {
243
272
  };
244
273
  });
245
274
 
246
- let overlayViewBox: string;
247
- if (this.zoomToFitArc) {
248
- const ext = 48;
249
- const targetSize = (176 + ext) * 2;
250
- const frame = computeZoomToFitArcFrame({
251
- areas,
252
- outerRadius: OUTER_RING_RADIUS,
253
- innerRadius: innerRingRadiusFor(WatchCircleType.double),
254
- extension: ext,
255
- targetSize,
256
- });
257
- overlayViewBox = frame.viewBox;
258
- this._radiusOffset = frame.radiusOffset;
259
- this._arcFrame = frame;
260
- } else {
261
- overlayViewBox = '-224 -44.8 448 268.8';
262
- this._radiusOffset = 0;
263
- this._arcFrame = undefined;
264
- }
275
+ const frame = computeRadialFrame({
276
+ basePadding: 48,
277
+ labelWidthPx: this.tickmarksInside
278
+ ? 0
279
+ : estimateLabelWidthPx(tickmarks.map((t) => t.text)),
280
+ clips: this.zoomToFitArc
281
+ ? undefined
282
+ : {top: 40, bottom: 0, left: 0, right: 0},
283
+ containerPx: measureContainerPx(this),
284
+ faceDiameter: this.faceDiameter,
285
+ zoomToFitArc: this.zoomToFitArc,
286
+ areas,
287
+ innerRadius: innerRingRadiusFor(WatchCircleType.double),
288
+ });
289
+ this._radiusOffset = frame.radiusOffset;
290
+ this._frame = frame;
291
+ const shownTickmarks = frame.labelsHidden
292
+ ? tickmarks.map((t) => ({...t, text: undefined}))
293
+ : tickmarks;
294
+ const overlayViewBox = frame.viewBox;
265
295
 
266
296
  return html`
267
297
  <div class="container">
268
298
  <obc-watch
269
299
  .touching=${this.touching}
270
- .clipTop=${this.zoomToFitArc ? 0 : 40}
271
- .zoomToFitArc=${this.zoomToFitArc}
272
- .arcFrame=${this._arcFrame}
300
+ .arcFrame=${frame}
273
301
  .areas=${areas}
274
302
  .angleSetpoint=${setpointAngle}
275
303
  .newAngleSetpoint=${this.newSetpoint !== undefined
@@ -279,8 +307,7 @@ export class ObcRudder extends SetpointMixin(LitElement) {
279
307
  .angleSetpointAtZeroDeadband=${this.setpointAtZeroDeadband}
280
308
  .setpointOverride=${this.setpointOverride}
281
309
  .animateSetpoint=${this.animateSetpoint}
282
- .padding=${48}
283
- .tickmarks=${tickmarks}
310
+ .tickmarks=${shownTickmarks}
284
311
  .tickmarksInside=${this.tickmarksInside}
285
312
  .tickmarkStyle=${this.tickmarkStyle}
286
313
  .watchCircleType=${WatchCircleType.double}
@@ -1,7 +1,13 @@
1
1
  import type {Meta, StoryObj} from '@storybook/web-components-vite';
2
2
  import {ObcSpeedGauge, ObcSpeedGaugeNeedleType} from './speed-gauge.js';
3
3
  import './speed-gauge.js';
4
- import {widthDecorator} from '../../storybook-util.js';
4
+ import {html} from 'lit';
5
+ import {
6
+ playgroundColumn,
7
+ resizableStoryBox,
8
+ storyHint,
9
+ widthDecorator,
10
+ } from '../../storybook-util.js';
5
11
  import {AdviceType} from '../watch/advice.js';
6
12
  import {TickmarkStyle} from '../watch/tickmark.js';
7
13
  import {Priority} from '../types.js';
@@ -33,6 +39,11 @@ const meta: Meta<typeof ObcSpeedGauge> = {
33
39
  options: Object.values(TickmarkStyle),
34
40
  },
35
41
  hasReadout: {control: 'boolean'},
42
+ faceDiameter: {
43
+ control: {type: 'range', min: 100, max: 600, step: 10},
44
+ description:
45
+ 'Pins the outer-ring diameter in px (fixed intrinsic size, equal circumference across instruments). Clear to return to fill-the-container sizing.',
46
+ },
36
47
  label: {control: 'text'},
37
48
  unit: {control: 'text'},
38
49
  fractionDigits: {control: 'number'},
@@ -73,3 +84,85 @@ export const Readout: Story = {
73
84
  hasReadout: true,
74
85
  },
75
86
  };
87
+
88
+ export const LabelRoomHighSpeed: Story = {
89
+ name: 'Label Room — 4-Digit Scale in A Short Cell (#1021)',
90
+ args: {
91
+ minSpeed: 0,
92
+ maxSpeed: 3600,
93
+ speed: 2400,
94
+ tickmarkInterval: 900,
95
+ showLabels: true,
96
+ hasReadout: true,
97
+ width: 600,
98
+ height: 220,
99
+ } as never,
100
+ };
101
+
102
+ type SizingPlaygroundArgs = Partial<ObcSpeedGauge> & {
103
+ lockFaceDiameter?: boolean;
104
+ };
105
+
106
+ /**
107
+ * Interactive sizing playground: drag the dashed box's bottom-right corner to
108
+ * resize it. The first gauge is pinned to a fixed intrinsic size by the
109
+ * `faceDiameter` control, while the second adapts to the remaining flex
110
+ * space, reserving room for its 4-digit labels adaptively (issue #1021).
111
+ * Enable `lockFaceDiameter` to pin both to the same circumference regardless
112
+ * of their label widths. Related: *Sizing Playground* stories under Building
113
+ * Blocks/Watch, Building Blocks/Instrument Radial and Instruments/Gauge
114
+ * Radial.
115
+ */
116
+ export const SizingPlayground: StoryObj<SizingPlaygroundArgs> = {
117
+ name: 'Sizing Playground — FaceDiameter + Resizable (Manual)',
118
+ tags: ['skip-test'],
119
+ parameters: {widthDecorator: false},
120
+ args: {
121
+ faceDiameter: 220,
122
+ lockFaceDiameter: false,
123
+ },
124
+ argTypes: {
125
+ lockFaceDiameter: {
126
+ control: 'boolean',
127
+ description:
128
+ 'Apply faceDiameter to every instance (equal circumference) instead of only the first.',
129
+ },
130
+ },
131
+ render: (args) => {
132
+ const instances = [
133
+ {label: '2-digit scale', max: 40, interval: 10},
134
+ {label: '4-digit scale', max: 3600, interval: 900},
135
+ ];
136
+ const fd = (index: number) =>
137
+ index === 0 || args.lockFaceDiameter ? args.faceDiameter : undefined;
138
+ const caption = (index: number, label: string) =>
139
+ fd(index) !== undefined
140
+ ? `${label} — pinned ${fd(index)}px`
141
+ : `${label} — adaptive (flex)`;
142
+ return html`
143
+ ${storyHint(
144
+ 'Drag the bottom-right corner of the dashed box to resize it. The first gauge is pinned by the faceDiameter control; the second adapts to the remaining flex space. Enable lockFaceDiameter to pin both to the same circumference.'
145
+ )}
146
+ ${resizableStoryBox(
147
+ html`
148
+ ${instances.map((g, index) =>
149
+ playgroundColumn(
150
+ caption(index, g.label),
151
+ html`
152
+ <obc-speed-gauge
153
+ .speed=${g.max * 0.6}
154
+ .maxSpeed=${g.max}
155
+ .tickmarkInterval=${g.interval}
156
+ .showLabels=${true}
157
+ .faceDiameter=${fd(index)}
158
+ ></obc-speed-gauge>
159
+ `,
160
+ {pinned: fd(index) !== undefined}
161
+ )
162
+ )}
163
+ `,
164
+ {width: 680, height: 340}
165
+ )}
166
+ `;
167
+ },
168
+ };
@@ -1,5 +1,14 @@
1
- import {LitElement, css, html, nothing, svg} from 'lit';
1
+ import {LitElement, PropertyValues, css, html, nothing, svg} from 'lit';
2
2
  import {property} from 'lit/decorators.js';
3
+ import {ResizeController} from '@lit-labs/observers/resize-controller.js';
4
+ import {
5
+ applyPinnedHostSize,
6
+ computeRadialFrame,
7
+ estimateLabelWidthPx,
8
+ measureContainerPx,
9
+ observeInnerBox,
10
+ type RadialFrame,
11
+ } from '../../svghelpers/radial-frame.js';
3
12
  import {Tickmark, TickmarkStyle, TickmarkType} from '../watch/tickmark.js';
4
13
  import {WatchCircleType} from '../watch/watch.js';
5
14
  import {AdviceType, AngleAdviceRaw, AdviceState} from '../watch/advice.js';
@@ -113,6 +122,36 @@ export class ObcSpeedGauge extends SetpointMixin(LitElement) {
113
122
  @property({type: String}) unit = 'KN';
114
123
  /** Number of fraction digits shown in the readout. Default `1`. */
115
124
  @property({type: Number}) fractionDigits = 1;
125
+ /**
126
+ * Outer-ring diameter in CSS pixels. When set, the instrument renders at a
127
+ * fixed intrinsic size derived from the ring, arc shape and label reserve —
128
+ * so instruments sharing the same value have identical ring circumference
129
+ * regardless of label width or arc extent (like obc-donut-chart's
130
+ * fixedHeight). When unset (default), the instrument fills its container.
131
+ */
132
+ @property({type: Number, attribute: 'face-diameter'})
133
+ faceDiameter: number | undefined;
134
+
135
+ private _frame: RadialFrame | undefined;
136
+
137
+ /** Whether the host size styles were set by applyPinnedHostSize. */
138
+ private _hostSizePinned = false;
139
+
140
+ private _resizeController = new ResizeController(this, {});
141
+
142
+ override firstUpdated(changed: PropertyValues): void {
143
+ super.firstUpdated(changed);
144
+ observeInnerBox(this._resizeController, this.renderRoot);
145
+ }
146
+
147
+ override updated(changed: PropertyValues): void {
148
+ super.updated(changed);
149
+ this._hostSizePinned = applyPinnedHostSize(
150
+ this,
151
+ this._frame,
152
+ this._hostSizePinned
153
+ );
154
+ }
116
155
 
117
156
  getAngle(v: number): number {
118
157
  return (v / this.maxSpeed) * (180 + 45) - 90;
@@ -134,6 +173,20 @@ export class ObcSpeedGauge extends SetpointMixin(LitElement) {
134
173
 
135
174
  const maxDigits = 1;
136
175
 
176
+ const tickmarks = this.tickmarks;
177
+ const frame = computeRadialFrame({
178
+ basePadding: 48,
179
+ labelWidthPx: this.tickmarksInside
180
+ ? 0
181
+ : estimateLabelWidthPx(tickmarks.map((t) => t.text)),
182
+ containerPx: measureContainerPx(this),
183
+ faceDiameter: this.faceDiameter,
184
+ });
185
+ this._frame = frame;
186
+ const shownTickmarks = frame.labelsHidden
187
+ ? tickmarks.map((t) => ({...t, text: undefined}))
188
+ : tickmarks;
189
+
137
190
  return html`
138
191
  <div class="container">
139
192
  <obc-watch
@@ -146,8 +199,8 @@ export class ObcSpeedGauge extends SetpointMixin(LitElement) {
146
199
  .angleSetpointAtZeroDeadband=${this.setpointAtZeroDeadband}
147
200
  .setpointOverride=${this.setpointOverride}
148
201
  .animateSetpoint=${this.animateSetpoint}
149
- .padding=${48}
150
- .tickmarks=${this.tickmarks}
202
+ .arcFrame=${frame}
203
+ .tickmarks=${shownTickmarks}
151
204
  .tickmarksInside=${this.tickmarksInside}
152
205
  .tickmarkStyle=${this.tickmarkStyle}
153
206
  .advices=${this._advices}
@@ -168,7 +221,7 @@ export class ObcSpeedGauge extends SetpointMixin(LitElement) {
168
221
  },
169
222
  ]}
170
223
  ></obc-watch>
171
- <svg class="rudder" viewBox="-224 -224 448 448">${this.needle}</svg>
224
+ <svg class="rudder" viewBox=${frame.viewBox}>${this.needle}</svg>
172
225
  ${this.hasReadout
173
226
  ? html`
174
227
  ${renderInstrumentReadout({