@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
@@ -0,0 +1,122 @@
1
+ import { WatchArea } from '../navigation-instruments/watch/watch.js';
2
+ import { ZoomToFitArcFrame } from './arc-frame.js';
3
+ /** Approximate digit advance at 12px font-instrument-label. */
4
+ export declare const DIGIT_WIDTH_PX = 7;
5
+ /** Compass NSWE label width assumed by label.ts (`labelWidth = 16`). */
6
+ export declare const NSWE_LABEL_WIDTH_PX = 16;
7
+ /**
8
+ * North-arrow glyph allowance for outside-decor reserves (px). The arrow
9
+ * renders regardless of `showLabels`, so consumers charge this portion
10
+ * whenever the arrow sits outside the ring.
11
+ */
12
+ export declare const NORTH_ARROW_WIDTH_PX = 16;
13
+ /**
14
+ * Labels are dropped (not clipped) when their reserve would exceed this share
15
+ * of the box. 0.45 makes a 4-digit gauge hide labels below a ~182px container
16
+ * — near the canvas charts' fixed MIN_HEIGHT_WITH_LABELS = 192px threshold —
17
+ * while 2-digit labels survive down to ~120px. Designers may later prefer a
18
+ * fixed pixel threshold instead (see charthelpers/constants.ts).
19
+ */
20
+ export declare const LABEL_RESERVE_MAX_FRACTION = 0.45;
21
+ /**
22
+ * Pixel drop of "Max-min" end labels (`endLabelsMaxMin`) below the ±90° line:
23
+ * anchored at `12px/scale` with `dominant-baseline: central` (tickmark.ts),
24
+ * so the glyph bottom reaches ~12 + 6 + descender ≈ 20px — plus 1px pad.
25
+ */
26
+ export declare const END_MAXMIN_LABEL_DROP_PX = 21;
27
+ /**
28
+ * Pixel drop of regular side labels at the ±90° ends: glyph centered on the
29
+ * line (half-height ~6px + descender), plus pad.
30
+ */
31
+ export declare const SIDE_LABEL_DROP_PX = 13;
32
+ /** Per-edge viewBox crops in percent (0–100), matching obc-watch `clip*`. */
33
+ export interface RadialClips {
34
+ top: number;
35
+ bottom: number;
36
+ left: number;
37
+ right: number;
38
+ }
39
+ export interface RadialFrameOptions {
40
+ /** Legacy padding: the frame never shrinks below `(176 + basePadding) * 2`. */
41
+ basePadding: number;
42
+ /** Max label pixel width at 12px font; 0/undefined = no outside labels. */
43
+ labelWidthPx?: number;
44
+ /** Un-zoomed viewBox crops; ignored when `zoomToFitArc` is set. */
45
+ clips?: RadialClips;
46
+ /**
47
+ * Pixel extent the outside labels need past the ±90° horizontal line
48
+ * toward a top/bottom crop edge (see END_MAXMIN_LABEL_DROP_PX /
49
+ * SIDE_LABEL_DROP_PX). When a `clips.top`/`clips.bottom` crop would cut
50
+ * into that drop, the clip is lowered so the labels stay inside the box.
51
+ * Absent/0 keeps the requested clips untouched (legacy geometry).
52
+ */
53
+ labelDropPx?: number;
54
+ /** Host client box in px; absent or zero ⇒ scale-1 fallback (first paint). */
55
+ containerPx?: {
56
+ width: number;
57
+ height: number;
58
+ };
59
+ /** Outer-ring diameter in px; pins scale for equal circumference (mode b). */
60
+ faceDiameter?: number;
61
+ zoomToFitArc?: boolean;
62
+ /** Visible arc segments; required for the zoom path. */
63
+ areas?: WatchArea[];
64
+ /** Innermost visible radius (depends on WatchCircleType); zoom path only. */
65
+ innerRadius?: number;
66
+ }
67
+ export interface RadialFrame extends ZoomToFitArcFrame {
68
+ /** Px per SVG unit implied by container or faceDiameter (1 when unknown). */
69
+ scale: number;
70
+ /** SVG units added beyond the base geometry for labels (0 when none/hidden). */
71
+ labelReserve: number;
72
+ /** True when the label reserve exceeded the cap; callers strip tick texts. */
73
+ labelsHidden: boolean;
74
+ /**
75
+ * True when `labelDropPx` lowered a top/bottom clip: the box's aspect no
76
+ * longer matches the requested clip fractions, so hosts with an
77
+ * aspect-ratio derived from those fractions must follow the frame instead.
78
+ */
79
+ clipsAdjusted: boolean;
80
+ /** Fixed host size in px; defined only when `faceDiameter` is set. */
81
+ hostWidthPx?: number;
82
+ /** Fixed host size in px; defined only when `faceDiameter` is set. */
83
+ hostHeightPx?: number;
84
+ }
85
+ /**
86
+ * Measure the host's available box in px. Radial instrument hosts render
87
+ * inline by default, so clientWidth/Height are often 0 — fall back to the
88
+ * parent box, like obc-watch's getScale() has always done (issue #1032).
89
+ */
90
+ export declare function measureContainerPx(host: HTMLElement): {
91
+ width: number;
92
+ height: number;
93
+ };
94
+ /**
95
+ * Apply/remove the fixed intrinsic host size a `faceDiameter` frame pins.
96
+ * The hosts render inline by default, so a pinned size also needs
97
+ * `display: block` for width/height to apply. Returns whether the size is
98
+ * currently pinned; pass that back as `wasPinned` on the next call so an
99
+ * unpinned host's own inline styles are never clobbered.
100
+ */
101
+ export declare function applyPinnedHostSize(host: HTMLElement, frame: RadialFrame | undefined, wasPinned: boolean): boolean;
102
+ /**
103
+ * Additionally observe an internal element that always generates a box (the
104
+ * shadow `<svg>` or `.container`). Several radial hosts render inline (no
105
+ * `:host {display}` rule), and ResizeObserver never fires for inline boxes —
106
+ * the host reports a permanent 0×0 size, so a controller observing only the
107
+ * host misses every resize. That freezes the container-dependent frame and
108
+ * the `--scale` font counter-scaling at their first-paint values.
109
+ */
110
+ export declare function observeInnerBox(controller: {
111
+ observe(target: Element): void;
112
+ }, renderRoot: ParentNode): void;
113
+ /**
114
+ * Estimate the widest label's pixel width at the 12px instrument label font.
115
+ */
116
+ export declare function estimateLabelWidthPx(texts: readonly (string | undefined)[]): number;
117
+ /**
118
+ * Compute the viewBox frame for a radial instrument, reserving width-aware
119
+ * room for outside tick labels. See the module documentation for the model.
120
+ */
121
+ export declare function computeRadialFrame(opts: RadialFrameOptions): RadialFrame;
122
+ //# sourceMappingURL=radial-frame.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"radial-frame.d.ts","sourceRoot":"","sources":["../../src/svghelpers/radial-frame.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,0CAA0C,CAAC;AACxE,OAAO,EAA2B,KAAK,iBAAiB,EAAC,MAAM,gBAAgB,CAAC;AA6DhF,+DAA+D;AAC/D,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,KAAK,CAAC;AACtC;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAC/C;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAC3C;;;GAGG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,6EAA6E;AAC7E,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,+EAA+E;IAC/E,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,WAAW,CAAC,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAC;IAC9C,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wDAAwD;IACxD,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;IACpB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAY,SAAQ,iBAAiB;IACpD,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,YAAY,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,YAAY,EAAE,OAAO,CAAC;IACtB;;;;OAIG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAWA;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,WAAW,GAAG,SAAS,EAC9B,SAAS,EAAE,OAAO,GACjB,OAAO,CAaT;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE;IAAC,OAAO,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAA;CAAC,EAC5C,UAAU,EAAE,UAAU,GACrB,IAAI,CAKN;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,GACrC,MAAM,CAQR;AAoJD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,GAAG,WAAW,CA4DxE"}
@@ -0,0 +1,259 @@
1
+ import { computeZoomToFitArcFrame } from "./arc-frame.js";
2
+ const OUTER_RING_RADIUS = 368 / 2;
3
+ const RADIAL_VIEWBOX_BASE = 176;
4
+ const LABEL_RADIAL_PAD_UNITS = 3;
5
+ const LABEL_ANCHOR_OFFSET_PX = 9;
6
+ const LABEL_EDGE_PAD_PX = 4;
7
+ const DIGIT_WIDTH_PX = 7;
8
+ const NSWE_LABEL_WIDTH_PX = 16;
9
+ const NORTH_ARROW_WIDTH_PX = 16;
10
+ const LABEL_RESERVE_MAX_FRACTION = 0.45;
11
+ const END_MAXMIN_LABEL_DROP_PX = 21;
12
+ const SIDE_LABEL_DROP_PX = 13;
13
+ function measureContainerPx(host) {
14
+ let width = host.clientWidth;
15
+ let height = host.clientHeight;
16
+ if (width === 0 || height === 0) {
17
+ const box = host.parentElement?.getBoundingClientRect();
18
+ if (box) {
19
+ width = box.width;
20
+ height = box.height;
21
+ }
22
+ }
23
+ return { width, height };
24
+ }
25
+ function applyPinnedHostSize(host, frame, wasPinned) {
26
+ if (frame?.hostWidthPx !== void 0 && frame.hostHeightPx !== void 0) {
27
+ host.style.width = `${frame.hostWidthPx}px`;
28
+ host.style.height = `${frame.hostHeightPx}px`;
29
+ host.style.display = "block";
30
+ return true;
31
+ }
32
+ if (wasPinned) {
33
+ host.style.removeProperty("width");
34
+ host.style.removeProperty("height");
35
+ host.style.removeProperty("display");
36
+ }
37
+ return false;
38
+ }
39
+ function observeInnerBox(controller, renderRoot) {
40
+ const box = renderRoot.querySelector("svg, .container");
41
+ if (box) {
42
+ controller.observe(box);
43
+ }
44
+ }
45
+ function estimateLabelWidthPx(texts) {
46
+ let maxChars = 0;
47
+ for (const t of texts) {
48
+ if (t !== void 0 && t.length > maxChars) {
49
+ maxChars = t.length;
50
+ }
51
+ }
52
+ return maxChars * DIGIT_WIDTH_PX;
53
+ }
54
+ function clipFractions(clips) {
55
+ if (!clips) {
56
+ return { fx: 1, fy: 1 };
57
+ }
58
+ const fx = 1 - (clips.left + clips.right) / 100;
59
+ const fy = 1 - (clips.top + clips.bottom) / 100;
60
+ return { fx: fx > 0 ? fx : 1, fy: fy > 0 ? fy : 1 };
61
+ }
62
+ function buildViewBox(side, clips) {
63
+ const { fx, fy } = clipFractions(clips);
64
+ const width = side * fx;
65
+ const height = side * fy;
66
+ const x = -side / 2 + side * (clips?.left ?? 0) / 100;
67
+ const y = -side / 2 + side * (clips?.top ?? 0) / 100;
68
+ return {
69
+ x,
70
+ y,
71
+ width,
72
+ height,
73
+ viewBox: `${x} ${y} ${width} ${height}`,
74
+ radiusOffset: 0
75
+ };
76
+ }
77
+ function pinnedScale(opts) {
78
+ if (opts.faceDiameter === void 0) {
79
+ return void 0;
80
+ }
81
+ const scale = opts.faceDiameter / (2 * OUTER_RING_RADIUS);
82
+ return Number.isFinite(scale) && scale > 0 ? scale : void 0;
83
+ }
84
+ function effectiveContainer(opts, fx, fy) {
85
+ const w = opts.containerPx?.width ?? 0;
86
+ const h = opts.containerPx?.height ?? 0;
87
+ if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
88
+ return void 0;
89
+ }
90
+ return Math.min(w / fx, h / fy);
91
+ }
92
+ function containerScale(opts, frame) {
93
+ const w = opts.containerPx?.width ?? 0;
94
+ const h = opts.containerPx?.height ?? 0;
95
+ if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
96
+ return void 0;
97
+ }
98
+ const scale = Math.min(w / frame.width, h / frame.height);
99
+ return Number.isFinite(scale) && scale > 0 ? scale : void 0;
100
+ }
101
+ function withHostPx(frame, pinned, scale, labelReserve, labelsHidden, clipsAdjusted = false) {
102
+ const result = {
103
+ ...frame,
104
+ scale,
105
+ labelReserve,
106
+ labelsHidden,
107
+ clipsAdjusted
108
+ };
109
+ if (pinned) {
110
+ result.hostWidthPx = frame.width * scale;
111
+ result.hostHeightPx = frame.height * scale;
112
+ }
113
+ return result;
114
+ }
115
+ function solveSide(opts, labelCostPx, clips) {
116
+ const baseSide = (RADIAL_VIEWBOX_BASE + opts.basePadding) * 2;
117
+ const { fx, fy } = clipFractions(clips);
118
+ const labelSideAt = (scale) => 2 * (OUTER_RING_RADIUS + LABEL_RADIAL_PAD_UNITS + labelCostPx / scale);
119
+ let side = baseSide;
120
+ let labelsHidden = false;
121
+ if (labelCostPx > 0) {
122
+ const pinned = pinnedScale(opts);
123
+ if (pinned !== void 0) {
124
+ const grown = Math.max(baseSide, labelSideAt(pinned));
125
+ labelsHidden = 2 * (labelCostPx / pinned) / grown > LABEL_RESERVE_MAX_FRACTION;
126
+ side = labelsHidden ? baseSide : grown;
127
+ } else {
128
+ const container = effectiveContainer(opts, fx, fy);
129
+ if (container === void 0) {
130
+ side = Math.max(baseSide, labelSideAt(1));
131
+ } else if (2 * labelCostPx / container > LABEL_RESERVE_MAX_FRACTION) {
132
+ labelsHidden = true;
133
+ } else {
134
+ side = Math.max(
135
+ baseSide,
136
+ 2 * (OUTER_RING_RADIUS + LABEL_RADIAL_PAD_UNITS) / (1 - 2 * labelCostPx / container)
137
+ );
138
+ }
139
+ }
140
+ }
141
+ return { side, labelsHidden };
142
+ }
143
+ function adjustClipsForLabelDrop(clips, side, scale, labelDropPx) {
144
+ const requiredUnits = labelDropPx / scale;
145
+ const requiredClipPct = Math.max(0, (0.5 - requiredUnits / side) * 100);
146
+ const bottom = clips.bottom > requiredClipPct ? requiredClipPct : clips.bottom;
147
+ const top = clips.top > requiredClipPct ? requiredClipPct : clips.top;
148
+ if (bottom === clips.bottom && top === clips.top) {
149
+ return clips;
150
+ }
151
+ return { ...clips, top, bottom };
152
+ }
153
+ function computeRadialFrame(opts) {
154
+ const labelWidthPx = opts.labelWidthPx ?? 0;
155
+ const labelCostPx = labelWidthPx > 0 ? LABEL_ANCHOR_OFFSET_PX + labelWidthPx + LABEL_EDGE_PAD_PX : 0;
156
+ if (opts.zoomToFitArc && opts.areas && opts.areas.length > 0) {
157
+ return computeZoomedFrame(opts, labelCostPx);
158
+ }
159
+ const baseSide = (RADIAL_VIEWBOX_BASE + opts.basePadding) * 2;
160
+ let clips = opts.clips;
161
+ let { side, labelsHidden } = solveSide(opts, labelCostPx, clips);
162
+ const labelDropPx = opts.labelDropPx ?? 0;
163
+ if (labelDropPx > 0 && labelCostPx > 0 && !labelsHidden && clips) {
164
+ for (let i = 0; i < 2; i++) {
165
+ const scale2 = pinnedScale(opts) ?? containerScale(opts, buildViewBox(side, clips)) ?? 1;
166
+ const adjusted = adjustClipsForLabelDrop(
167
+ clips,
168
+ side,
169
+ scale2,
170
+ labelDropPx
171
+ );
172
+ if (adjusted === clips) {
173
+ break;
174
+ }
175
+ clips = adjusted;
176
+ const solved = solveSide(opts, labelCostPx, clips);
177
+ side = solved.side;
178
+ labelsHidden = solved.labelsHidden;
179
+ }
180
+ if (labelsHidden) {
181
+ clips = opts.clips;
182
+ const solved = solveSide(opts, labelCostPx, clips);
183
+ side = solved.side;
184
+ labelsHidden = solved.labelsHidden;
185
+ }
186
+ }
187
+ const frame = buildViewBox(side, clips);
188
+ const pinned = pinnedScale(opts);
189
+ const scale = pinned ?? containerScale(opts, frame) ?? 1;
190
+ return withHostPx(
191
+ frame,
192
+ pinned !== void 0,
193
+ scale,
194
+ (side - baseSide) / 2,
195
+ labelsHidden,
196
+ clips !== opts.clips
197
+ );
198
+ }
199
+ function computeZoomedFrame(opts, labelCostPx) {
200
+ let reserve = 0;
201
+ let labelsHidden = false;
202
+ let frame = zoomFrameAt(opts, opts.basePadding);
203
+ if (labelCostPx > 0) {
204
+ for (let i = 0; i < 2; i++) {
205
+ const passScale = zoomedScale(opts, frame);
206
+ const reserveUnits = labelCostPx / passScale;
207
+ if (2 * reserveUnits / Math.max(frame.width, frame.height) > LABEL_RESERVE_MAX_FRACTION) {
208
+ labelsHidden = true;
209
+ reserve = 0;
210
+ frame = zoomFrameAt(opts, opts.basePadding);
211
+ break;
212
+ }
213
+ reserve = reserveUnits;
214
+ frame = zoomFrameAt(opts, opts.basePadding + reserve);
215
+ }
216
+ }
217
+ const pinned = pinnedZoomedScale(opts, frame);
218
+ const scale = pinned ?? containerScale(opts, frame) ?? 1;
219
+ return withHostPx(
220
+ frame,
221
+ pinned !== void 0,
222
+ scale,
223
+ labelsHidden ? 0 : reserve,
224
+ labelsHidden
225
+ );
226
+ }
227
+ function zoomFrameAt(opts, extension) {
228
+ return computeZoomToFitArcFrame({
229
+ areas: opts.areas,
230
+ outerRadius: OUTER_RING_RADIUS,
231
+ innerRadius: opts.innerRadius ?? OUTER_RING_RADIUS,
232
+ extension,
233
+ targetSize: (RADIAL_VIEWBOX_BASE + extension) * 2
234
+ });
235
+ }
236
+ function pinnedZoomedScale(opts, frame) {
237
+ if (opts.faceDiameter === void 0) {
238
+ return void 0;
239
+ }
240
+ const scale = opts.faceDiameter / (2 * (OUTER_RING_RADIUS + frame.radiusOffset));
241
+ return Number.isFinite(scale) && scale > 0 ? scale : void 0;
242
+ }
243
+ function zoomedScale(opts, frame) {
244
+ return pinnedZoomedScale(opts, frame) ?? containerScale(opts, frame) ?? 1;
245
+ }
246
+ export {
247
+ DIGIT_WIDTH_PX,
248
+ END_MAXMIN_LABEL_DROP_PX,
249
+ LABEL_RESERVE_MAX_FRACTION,
250
+ NORTH_ARROW_WIDTH_PX,
251
+ NSWE_LABEL_WIDTH_PX,
252
+ SIDE_LABEL_DROP_PX,
253
+ applyPinnedHostSize,
254
+ computeRadialFrame,
255
+ estimateLabelWidthPx,
256
+ measureContainerPx,
257
+ observeInnerBox
258
+ };
259
+ //# sourceMappingURL=radial-frame.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"radial-frame.js","sources":["../../src/svghelpers/radial-frame.ts"],"sourcesContent":["import type {WatchArea} from '../navigation-instruments/watch/watch.js';\nimport {computeZoomToFitArcFrame, type ZoomToFitArcFrame} from './arc-frame.js';\n\n/**\n * Shared frame computation for circular/radial watch-based instruments.\n *\n * Every radial instrument (watch, instrument-radial/gauge-radial, speed-gauge,\n * azimuth-thruster, compass, heading, rudder, pitch/roll, …) needs the same\n * three outputs to render: an SVG viewBox for `<obc-watch>` and its overlay,\n * a zoom radius offset, and — since issue #1021 — enough reserved room around\n * the outer ring that tick labels are not clipped when the instrument shrinks\n * inside a flex container.\n *\n * `computeRadialFrame()` is the single source of truth for that geometry.\n * Consumers call it once per render and pass the resulting frame to\n * `<obc-watch .arcFrame=...>` (which uses it verbatim) and to their own\n * overlay `<svg viewBox=...>`, guaranteeing the two layers stay aligned.\n *\n * ## Label reserve (issue #1021)\n *\n * Tick labels render at `font-size: calc(12px / scale)` so they keep a\n * constant on-screen size while the ring scales. Their footprint in SVG units\n * is therefore `px / scale`, where `scale = containerPx / viewBoxSide` — the\n * smaller the instrument, the more SVG units a label needs. The frame solves\n * the resulting equation in closed form:\n *\n * ```\n * side/2 >= (OUTER_RING_RADIUS + LABEL_RADIAL_PAD_UNITS) + P/scale\n * scale = containerPx / side\n * => side = 2 * (OUTER_RING_RADIUS + LABEL_RADIAL_PAD_UNITS) / (1 - 2P/containerPx)\n * ```\n *\n * where `P` is the label's pixel cost beyond the ring (anchor offsets + text\n * width + edge padding). The viewBox never shrinks below the legacy\n * `(176 + basePadding) * 2` box, so instruments without labels — or with\n * labels that already fit — render byte-identical to the pre-#1021 output.\n *\n * When the reserve would exceed `LABEL_RESERVE_MAX_FRACTION` of the box, the\n * frame reports `labelsHidden: true` and falls back to the base box; callers\n * must then strip tick label texts (graceful degradation instead of clipping,\n * mirroring the canvas charts).\n *\n * ## Equal circumference (`faceDiameter`)\n *\n * When `faceDiameter` (px) is given, the outer-ring diameter is pinned:\n * `scale = faceDiameter / 368`. The frame then reports `hostWidthPx` /\n * `hostHeightPx` so the host element can adopt a fixed intrinsic size, and\n * instruments sharing a `faceDiameter` value have identical ring\n * circumference regardless of label width or arc extent — the SVG counterpart\n * of `obc-donut-chart`'s fixed-height ⇒ fixed-circumference contract.\n */\n\n// Keep in sync with watch.ts OUTER_RING_RADIUS (importing it would be circular).\nconst OUTER_RING_RADIUS = 368 / 2;\n/** Legacy viewBox base: `side = (RADIAL_VIEWBOX_BASE + padding) * 2`. */\nconst RADIAL_VIEWBOX_BASE = 176;\n/** Non-scaled part of the tickmark text radius pad (`3/scale + 3` in tickmark.ts). */\nconst LABEL_RADIAL_PAD_UNITS = 3;\n/** Scaled label anchor offsets in px: `3/scale` textRadius pad + `6/scale` xOffset. */\nconst LABEL_ANCHOR_OFFSET_PX = 9;\n/** Breathing room between the label's far edge and the viewBox edge, in px. */\nconst LABEL_EDGE_PAD_PX = 4;\n/** Approximate digit advance at 12px font-instrument-label. */\nexport const DIGIT_WIDTH_PX = 7;\n/** Compass NSWE label width assumed by label.ts (`labelWidth = 16`). */\nexport const NSWE_LABEL_WIDTH_PX = 16;\n/**\n * North-arrow glyph allowance for outside-decor reserves (px). The arrow\n * renders regardless of `showLabels`, so consumers charge this portion\n * whenever the arrow sits outside the ring.\n */\nexport const NORTH_ARROW_WIDTH_PX = 16;\n/**\n * Labels are dropped (not clipped) when their reserve would exceed this share\n * of the box. 0.45 makes a 4-digit gauge hide labels below a ~182px container\n * — near the canvas charts' fixed MIN_HEIGHT_WITH_LABELS = 192px threshold —\n * while 2-digit labels survive down to ~120px. Designers may later prefer a\n * fixed pixel threshold instead (see charthelpers/constants.ts).\n */\nexport const LABEL_RESERVE_MAX_FRACTION = 0.45;\n/**\n * Pixel drop of \"Max-min\" end labels (`endLabelsMaxMin`) below the ±90° line:\n * anchored at `12px/scale` with `dominant-baseline: central` (tickmark.ts),\n * so the glyph bottom reaches ~12 + 6 + descender ≈ 20px — plus 1px pad.\n */\nexport const END_MAXMIN_LABEL_DROP_PX = 21;\n/**\n * Pixel drop of regular side labels at the ±90° ends: glyph centered on the\n * line (half-height ~6px + descender), plus pad.\n */\nexport const SIDE_LABEL_DROP_PX = 13;\n\n/** Per-edge viewBox crops in percent (0–100), matching obc-watch `clip*`. */\nexport interface RadialClips {\n top: number;\n bottom: number;\n left: number;\n right: number;\n}\n\nexport interface RadialFrameOptions {\n /** Legacy padding: the frame never shrinks below `(176 + basePadding) * 2`. */\n basePadding: number;\n /** Max label pixel width at 12px font; 0/undefined = no outside labels. */\n labelWidthPx?: number;\n /** Un-zoomed viewBox crops; ignored when `zoomToFitArc` is set. */\n clips?: RadialClips;\n /**\n * Pixel extent the outside labels need past the ±90° horizontal line\n * toward a top/bottom crop edge (see END_MAXMIN_LABEL_DROP_PX /\n * SIDE_LABEL_DROP_PX). When a `clips.top`/`clips.bottom` crop would cut\n * into that drop, the clip is lowered so the labels stay inside the box.\n * Absent/0 keeps the requested clips untouched (legacy geometry).\n */\n labelDropPx?: number;\n /** Host client box in px; absent or zero ⇒ scale-1 fallback (first paint). */\n containerPx?: {width: number; height: number};\n /** Outer-ring diameter in px; pins scale for equal circumference (mode b). */\n faceDiameter?: number;\n zoomToFitArc?: boolean;\n /** Visible arc segments; required for the zoom path. */\n areas?: WatchArea[];\n /** Innermost visible radius (depends on WatchCircleType); zoom path only. */\n innerRadius?: number;\n}\n\nexport interface RadialFrame extends ZoomToFitArcFrame {\n /** Px per SVG unit implied by container or faceDiameter (1 when unknown). */\n scale: number;\n /** SVG units added beyond the base geometry for labels (0 when none/hidden). */\n labelReserve: number;\n /** True when the label reserve exceeded the cap; callers strip tick texts. */\n labelsHidden: boolean;\n /**\n * True when `labelDropPx` lowered a top/bottom clip: the box's aspect no\n * longer matches the requested clip fractions, so hosts with an\n * aspect-ratio derived from those fractions must follow the frame instead.\n */\n clipsAdjusted: boolean;\n /** Fixed host size in px; defined only when `faceDiameter` is set. */\n hostWidthPx?: number;\n /** Fixed host size in px; defined only when `faceDiameter` is set. */\n hostHeightPx?: number;\n}\n\n/**\n * Measure the host's available box in px. Radial instrument hosts render\n * inline by default, so clientWidth/Height are often 0 — fall back to the\n * parent box, like obc-watch's getScale() has always done (issue #1032).\n */\nexport function measureContainerPx(host: HTMLElement): {\n width: number;\n height: number;\n} {\n let width = host.clientWidth;\n let height = host.clientHeight;\n if (width === 0 || height === 0) {\n const box = host.parentElement?.getBoundingClientRect();\n if (box) {\n width = box.width;\n height = box.height;\n }\n }\n return {width, height};\n}\n\n/**\n * Apply/remove the fixed intrinsic host size a `faceDiameter` frame pins.\n * The hosts render inline by default, so a pinned size also needs\n * `display: block` for width/height to apply. Returns whether the size is\n * currently pinned; pass that back as `wasPinned` on the next call so an\n * unpinned host's own inline styles are never clobbered.\n */\nexport function applyPinnedHostSize(\n host: HTMLElement,\n frame: RadialFrame | undefined,\n wasPinned: boolean\n): boolean {\n if (frame?.hostWidthPx !== undefined && frame.hostHeightPx !== undefined) {\n host.style.width = `${frame.hostWidthPx}px`;\n host.style.height = `${frame.hostHeightPx}px`;\n host.style.display = 'block';\n return true;\n }\n if (wasPinned) {\n host.style.removeProperty('width');\n host.style.removeProperty('height');\n host.style.removeProperty('display');\n }\n return false;\n}\n\n/**\n * Additionally observe an internal element that always generates a box (the\n * shadow `<svg>` or `.container`). Several radial hosts render inline (no\n * `:host {display}` rule), and ResizeObserver never fires for inline boxes —\n * the host reports a permanent 0×0 size, so a controller observing only the\n * host misses every resize. That freezes the container-dependent frame and\n * the `--scale` font counter-scaling at their first-paint values.\n */\nexport function observeInnerBox(\n controller: {observe(target: Element): void},\n renderRoot: ParentNode\n): void {\n const box = renderRoot.querySelector('svg, .container');\n if (box) {\n controller.observe(box);\n }\n}\n\n/**\n * Estimate the widest label's pixel width at the 12px instrument label font.\n */\nexport function estimateLabelWidthPx(\n texts: readonly (string | undefined)[]\n): number {\n let maxChars = 0;\n for (const t of texts) {\n if (t !== undefined && t.length > maxChars) {\n maxChars = t.length;\n }\n }\n return maxChars * DIGIT_WIDTH_PX;\n}\n\nfunction clipFractions(clips?: RadialClips): {fx: number; fy: number} {\n if (!clips) {\n return {fx: 1, fy: 1};\n }\n const fx = 1 - (clips.left + clips.right) / 100;\n const fy = 1 - (clips.top + clips.bottom) / 100;\n return {fx: fx > 0 ? fx : 1, fy: fy > 0 ? fy : 1};\n}\n\nfunction buildViewBox(side: number, clips?: RadialClips): ZoomToFitArcFrame {\n const {fx, fy} = clipFractions(clips);\n const width = side * fx;\n const height = side * fy;\n const x = -side / 2 + (side * (clips?.left ?? 0)) / 100;\n const y = -side / 2 + (side * (clips?.top ?? 0)) / 100;\n return {\n x,\n y,\n width,\n height,\n viewBox: `${x} ${y} ${width} ${height}`,\n radiusOffset: 0,\n };\n}\n\nfunction pinnedScale(opts: RadialFrameOptions): number | undefined {\n if (opts.faceDiameter === undefined) {\n return undefined;\n }\n const scale = opts.faceDiameter / (2 * OUTER_RING_RADIUS);\n return Number.isFinite(scale) && scale > 0 ? scale : undefined;\n}\n\n/** Container px available for the full (un-clipped) square, or undefined. */\nfunction effectiveContainer(\n opts: RadialFrameOptions,\n fx: number,\n fy: number\n): number | undefined {\n const w = opts.containerPx?.width ?? 0;\n const h = opts.containerPx?.height ?? 0;\n if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {\n return undefined;\n }\n return Math.min(w / fx, h / fy);\n}\n\nfunction containerScale(\n opts: RadialFrameOptions,\n frame: {width: number; height: number}\n): number | undefined {\n const w = opts.containerPx?.width ?? 0;\n const h = opts.containerPx?.height ?? 0;\n if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {\n return undefined;\n }\n const scale = Math.min(w / frame.width, h / frame.height);\n return Number.isFinite(scale) && scale > 0 ? scale : undefined;\n}\n\nfunction withHostPx(\n frame: ZoomToFitArcFrame,\n pinned: boolean,\n scale: number,\n labelReserve: number,\n labelsHidden: boolean,\n clipsAdjusted = false\n): RadialFrame {\n const result: RadialFrame = {\n ...frame,\n scale,\n labelReserve,\n labelsHidden,\n clipsAdjusted,\n };\n if (pinned) {\n result.hostWidthPx = frame.width * scale;\n result.hostHeightPx = frame.height * scale;\n }\n return result;\n}\n\n/**\n * Solve the un-zoomed box side and label visibility for a given clip window.\n */\nfunction solveSide(\n opts: RadialFrameOptions,\n labelCostPx: number,\n clips: RadialClips | undefined\n): {side: number; labelsHidden: boolean} {\n const baseSide = (RADIAL_VIEWBOX_BASE + opts.basePadding) * 2;\n const {fx, fy} = clipFractions(clips);\n const labelSideAt = (scale: number): number =>\n 2 * (OUTER_RING_RADIUS + LABEL_RADIAL_PAD_UNITS + labelCostPx / scale);\n\n let side = baseSide;\n let labelsHidden = false;\n\n if (labelCostPx > 0) {\n const pinned = pinnedScale(opts);\n if (pinned !== undefined) {\n const grown = Math.max(baseSide, labelSideAt(pinned));\n labelsHidden =\n (2 * (labelCostPx / pinned)) / grown > LABEL_RESERVE_MAX_FRACTION;\n side = labelsHidden ? baseSide : grown;\n } else {\n const container = effectiveContainer(opts, fx, fy);\n if (container === undefined) {\n side = Math.max(baseSide, labelSideAt(1));\n } else if ((2 * labelCostPx) / container > LABEL_RESERVE_MAX_FRACTION) {\n labelsHidden = true;\n } else {\n side = Math.max(\n baseSide,\n (2 * (OUTER_RING_RADIUS + LABEL_RADIAL_PAD_UNITS)) /\n (1 - (2 * labelCostPx) / container)\n );\n }\n }\n }\n\n return {side, labelsHidden};\n}\n\n/**\n * Lower a top/bottom clip so the crop edge keeps `labelDropPx / scale` SVG\n * units past the ±90° horizontal line (where the arc end labels hang).\n * Returns the input object unchanged when the requested clips already fit.\n */\nfunction adjustClipsForLabelDrop(\n clips: RadialClips,\n side: number,\n scale: number,\n labelDropPx: number\n): RadialClips {\n const requiredUnits = labelDropPx / scale;\n const requiredClipPct = Math.max(0, (0.5 - requiredUnits / side) * 100);\n const bottom =\n clips.bottom > requiredClipPct ? requiredClipPct : clips.bottom;\n const top = clips.top > requiredClipPct ? requiredClipPct : clips.top;\n if (bottom === clips.bottom && top === clips.top) {\n return clips;\n }\n return {...clips, top, bottom};\n}\n\n/**\n * Compute the viewBox frame for a radial instrument, reserving width-aware\n * room for outside tick labels. See the module documentation for the model.\n */\nexport function computeRadialFrame(opts: RadialFrameOptions): RadialFrame {\n const labelWidthPx = opts.labelWidthPx ?? 0;\n const labelCostPx =\n labelWidthPx > 0\n ? LABEL_ANCHOR_OFFSET_PX + labelWidthPx + LABEL_EDGE_PAD_PX\n : 0;\n\n if (opts.zoomToFitArc && opts.areas && opts.areas.length > 0) {\n return computeZoomedFrame(opts, labelCostPx);\n }\n\n const baseSide = (RADIAL_VIEWBOX_BASE + opts.basePadding) * 2;\n let clips = opts.clips;\n let {side, labelsHidden} = solveSide(opts, labelCostPx, clips);\n\n // Label drop past the ±90° line: lower a top/bottom crop that would cut\n // into it. The clip change alters the container fit (fy grows), so run a\n // second pass; visually exact after two.\n const labelDropPx = opts.labelDropPx ?? 0;\n if (labelDropPx > 0 && labelCostPx > 0 && !labelsHidden && clips) {\n for (let i = 0; i < 2; i++) {\n const scale =\n pinnedScale(opts) ??\n containerScale(opts, buildViewBox(side, clips)) ??\n 1;\n const adjusted = adjustClipsForLabelDrop(\n clips!,\n side,\n scale,\n labelDropPx\n );\n if (adjusted === clips) {\n break;\n }\n clips = adjusted;\n const solved = solveSide(opts, labelCostPx, clips);\n side = solved.side;\n labelsHidden = solved.labelsHidden;\n }\n if (labelsHidden) {\n // The taller window pushed the reserve past the cap: labels are\n // stripped, so the drop is moot — restore the requested clips.\n clips = opts.clips;\n const solved = solveSide(opts, labelCostPx, clips);\n side = solved.side;\n labelsHidden = solved.labelsHidden;\n }\n }\n\n const frame = buildViewBox(side, clips);\n const pinned = pinnedScale(opts);\n const scale = pinned ?? containerScale(opts, frame) ?? 1;\n return withHostPx(\n frame,\n pinned !== undefined,\n scale,\n (side - baseSide) / 2,\n labelsHidden,\n clips !== opts.clips\n );\n}\n\nfunction computeZoomedFrame(\n opts: RadialFrameOptions,\n labelCostPx: number\n): RadialFrame {\n let reserve = 0;\n let labelsHidden = false;\n let frame: ZoomToFitArcFrame = zoomFrameAt(opts, opts.basePadding);\n\n if (labelCostPx > 0) {\n // Fixed-point on the extension: the reserve depends on the scale, and the\n // scale on the frame size. Converges monotonically; two passes suffice.\n for (let i = 0; i < 2; i++) {\n const passScale = zoomedScale(opts, frame);\n const reserveUnits = labelCostPx / passScale;\n if (\n (2 * reserveUnits) / Math.max(frame.width, frame.height) >\n LABEL_RESERVE_MAX_FRACTION\n ) {\n labelsHidden = true;\n reserve = 0;\n frame = zoomFrameAt(opts, opts.basePadding);\n break;\n }\n reserve = reserveUnits;\n frame = zoomFrameAt(opts, opts.basePadding + reserve);\n }\n }\n\n const pinned = pinnedZoomedScale(opts, frame);\n const scale = pinned ?? containerScale(opts, frame) ?? 1;\n return withHostPx(\n frame,\n pinned !== undefined,\n scale,\n labelsHidden ? 0 : reserve,\n labelsHidden\n );\n}\n\nfunction zoomFrameAt(\n opts: RadialFrameOptions,\n extension: number\n): ZoomToFitArcFrame {\n return computeZoomToFitArcFrame({\n areas: opts.areas!,\n outerRadius: OUTER_RING_RADIUS,\n innerRadius: opts.innerRadius ?? OUTER_RING_RADIUS,\n extension,\n targetSize: (RADIAL_VIEWBOX_BASE + extension) * 2,\n });\n}\n\n/** Pin scale for a zoomed frame, or undefined when `faceDiameter` is unusable. */\nfunction pinnedZoomedScale(\n opts: RadialFrameOptions,\n frame: ZoomToFitArcFrame\n): number | undefined {\n if (opts.faceDiameter === undefined) {\n return undefined;\n }\n const scale =\n opts.faceDiameter / (2 * (OUTER_RING_RADIUS + frame.radiusOffset));\n return Number.isFinite(scale) && scale > 0 ? scale : undefined;\n}\n\nfunction zoomedScale(\n opts: RadialFrameOptions,\n frame: ZoomToFitArcFrame\n): number {\n return pinnedZoomedScale(opts, frame) ?? containerScale(opts, frame) ?? 1;\n}\n"],"names":["scale"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oicl/openbridge-webcomponents-full-bundle",
3
- "version": "2.0.0-next.101",
3
+ "version": "2.0.0-next.103",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,6 +31,13 @@
31
31
  * - **Empty description (warning):** a `@slot`/`@fires` tag with only a name and
32
32
  * no descriptive text. These become blank cells in the manifest / Storybook
33
33
  * controls; warnings do not fail CI.
34
+ * - **Class-level `@property` tag (error):** properties must be documented
35
+ * inline above their field declarations (AGENTS.md §3.6). A tag in the class
36
+ * JSDoc block overrides the inline doc and can inject a ghost member for a
37
+ * non-existent property (issue #1043). A short allowlist covers documented
38
+ * exceptions (property-injecting mixins, one legacy chart-base tag). A
39
+ * class-level `@attr`/`@attribute` that documents a CSS-only attribute with
40
+ * no backing `@property` field is allowed and is not flagged.
34
41
  *
35
42
  * There is deliberately no phantom-`@fires` *error*: unlike slots (which must be
36
43
  * a `<slot>` element in the component's own shadow DOM), a documented event may
@@ -304,6 +311,34 @@ async function run(): Promise<void> {
304
311
  }
305
312
  }
306
313
 
314
+ // Class-level @property / @attr JSDoc tags (AGENTS.md §3.6): properties must be
315
+ // documented inline above their field declarations. A class-level tag overrides
316
+ // the inline doc and can inject a ghost member for a property that does not
317
+ // exist (issue #1043). Allowlisted files are documented exceptions: mixins that
318
+ // inject properties into consumers, and one legacy `fill` tag on the chart base.
319
+ const PROPERTY_TAG_ALLOWLIST = [
320
+ 'svghelpers/setpoint-mixin.ts',
321
+ 'svghelpers/setpoint-bundle.ts',
322
+ 'building-blocks/chart-line/chart-line-base.ts',
323
+ ];
324
+ // Only `@property` is flagged. `@attr`/`@attribute` in a class block is the
325
+ // legitimate way to document a CSS-only attribute that has no backing
326
+ // `@property` field (e.g. slider's `hugcontainer`), so it is not an error.
327
+ // Matches both the multi-line (` * @property`) and single-line
328
+ // (`/** @property`) JSDoc block-comment forms.
329
+ const propTagRe = /^[ \t]*(?:\/\*\*|\*)[ \t]*@property\b/gm;
330
+ for (const rel of files.sort()) {
331
+ if (PROPERTY_TAG_ALLOWLIST.some((a) => rel.endsWith(a))) continue;
332
+ const source = fs.readFileSync(path.join(cwd, rel), 'utf8');
333
+ const matches = source.match(propTagRe);
334
+ if (matches && matches.length > 0) {
335
+ errors.push({
336
+ file: rel,
337
+ message: `has ${matches.length} class-level @property JSDoc tag(s) — document each property inline above its field declaration instead (AGENTS.md §3.6; these override inline docs and can create ghost manifest members)`,
338
+ });
339
+ }
340
+ }
341
+
307
342
  console.log('Slot & event documentation audit');
308
343
  console.log(`Scanned ${componentCount} registered components under src/`);
309
344
 
@@ -138,75 +138,77 @@ export type DonutChartDataItem = {
138
138
  * </script>
139
139
  * ```
140
140
  *
141
- * @property {Array<{label: string, value: number}>} data - Chart data segments (set via JavaScript)
142
- * @property {string[]} colors - Custom segment colors (set via JavaScript) with fallback to theme palette
143
- * @property {boolean} half - Whether to display as half-circle (180°) or full circle (360°), default: false
144
- * @property {boolean} showOuterLabels - Show outer labels, default: false
145
- * @property {boolean} showUnit - Whether to show unit in labels, default: false
146
- * @property {string} outerLabelUnit - Unit string to append to outer labels, default: "%"
147
- * @property {number} outerLabelMaxLength - Maximum character length for labels before trim (0 = no limit), default: 0
148
- * @property {number} outerLabelDecimalPlaces - Number of decimal places in labels, default: 0
149
- * @property {string} centerReadoutLabel - Text label shown below the center total value, default: "Total"
150
- * @property {string} centerReadoutUnit - Unit string shown inline after the label in the center readout, default: "%"
151
- * @property {number} max - Maximum value for calculating remaining empty sector, default: 100
152
- * @property {number} thickness - Donut ring thickness in pixels, default: 24
153
- * @property {boolean} showDebugOverlay - Show debug overlay for development, default: false
154
- * @property {number} fixedHeight - Fixed height of the chart in pixels (mandatory, determines chart circumference), default: 320. The chart's circumference is always based on this fixed height to match other radial instruments.
155
- * @property {boolean} legend - Whether to display the legend below the chart, default: false
156
141
  * @beta
157
142
  */
158
143
  @customElement('obc-donut-chart')
159
144
  export class ObcDonutChart extends LitElement {
145
+ /** Chart data segments (set via JavaScript) */
160
146
  @property({type: Array, attribute: false})
161
147
  data: DonutChartDataItem[] = [];
162
148
 
149
+ /** Custom segment colors (set via JavaScript) with fallback to theme palette */
163
150
  @property({type: Array, attribute: false})
164
151
  colors: string[] = [];
165
152
 
166
153
  @property({type: String})
167
154
  priority: Priority = Priority.regular;
168
155
 
156
+ /** Whether to display as half-circle (180°) or full circle (360°), default: false */
169
157
  @property({type: Boolean, reflect: true})
170
158
  half = false;
171
159
 
160
+ /** Show outer labels, default: false */
172
161
  @property({type: Boolean})
173
162
  showOuterLabels = false;
174
163
 
164
+ /** Whether to show unit in labels, default: false */
175
165
  @property({type: Boolean})
176
166
  showUnit = false;
177
167
 
168
+ /** Unit string to append to outer labels, default: "%" */
178
169
  @property({type: String})
179
170
  outerLabelUnit = '%';
180
171
 
181
- /** @availableWhen showOuterLabels==true */
172
+ /**
173
+ * Maximum character length for labels before trim (0 = no limit), default: 0
174
+ * @availableWhen showOuterLabels==true
175
+ */
182
176
  @property({
183
177
  type: Number,
184
178
  })
185
179
  outerLabelMaxLength = 0;
186
180
 
181
+ /** Number of decimal places in labels, default: 0 */
187
182
  @property({
188
183
  type: Number,
189
184
  })
190
185
  outerLabelDecimalPlaces = 0;
191
186
 
187
+ /** Text label shown below the center total value, default: "Total" */
192
188
  @property({type: String})
193
189
  centerReadoutLabel = 'Total';
194
190
 
191
+ /** Unit string shown inline after the label in the center readout, default: "%" */
195
192
  @property({type: String})
196
193
  centerReadoutUnit = '%';
197
194
 
195
+ /** Maximum value for calculating remaining empty sector, default: 100 */
198
196
  @property({type: Number})
199
197
  max = 100;
200
198
 
199
+ /** Donut ring thickness in pixels, default: 24 */
201
200
  @property({type: Number})
202
201
  thickness = 24;
203
202
 
203
+ /** Whether to display the legend below the chart, default: false */
204
204
  @property({type: Boolean, reflect: true})
205
205
  legend = false;
206
206
 
207
+ /** Show debug overlay for development, default: false */
207
208
  @property({type: Boolean, reflect: true})
208
209
  showDebugOverlay = false;
209
210
 
211
+ /** Fixed height of the chart in pixels (determines chart circumference), default: 320. The chart's circumference is always based on this fixed height to match other radial instruments. */
210
212
  @property({type: Number, reflect: true})
211
213
  fixedHeight = 320;
212
214
 
@@ -156,59 +156,61 @@ export type PieChartDataItem = {
156
156
  * </script>
157
157
  * ```
158
158
  *
159
- * @property {Array<{label: string, value: number, children?: Array<{label: string, value: number}>}>} data - Chart data segments with optional children subsegments for sunburst mode (set via JavaScript)
160
- * @property {string[]} colors - Custom segment colors (set via JavaScript) with fallback to theme palette
161
- * @property {boolean} showOuterLabels - Show outer labels, default: false
162
- * @property {boolean} showUnit - Whether to show unit in labels, default: false
163
- * @property {boolean} sunburst - Enable sunburst mode with interactive children subsegments, default: false
164
- * @property {string} outerLabelUnit - Unit string to append to outer labels, default: "%"
165
- * @property {number} outerLabelMaxLength - Maximum character length for labels before trim (0 = no limit), default: 0
166
- * @property {number} outerLabelDecimalPlaces - Number of decimal places in labels, default: 0
167
- * @property {boolean} showDebugOverlay - Show debug overlay for development, default: false
168
- * @property {number} fixedHeight - Fixed height of the chart in pixels (mandatory, determines chart circumference), default: 320. The chart's circumference is always based on this fixed height to match other radial instruments.
169
- * @property {boolean} legend - Whether to display the legend below the chart, default: false
170
159
  * @beta
171
160
  */
172
161
  @customElement('obc-pie-chart')
173
162
  export class ObcPieChart extends LitElement {
163
+ /** Chart data segments with optional children subsegments for sunburst mode (set via JavaScript). */
174
164
  @property({type: Array, attribute: false})
175
165
  data: PieChartDataItem[] = [];
176
166
 
167
+ /** Custom segment colors (set via JavaScript) with fallback to theme palette */
177
168
  @property({type: Array, attribute: false})
178
169
  colors: string[] = [];
179
170
 
180
171
  @property({type: String})
181
172
  priority: Priority = Priority.regular;
182
173
 
174
+ /** Show outer labels, default: false */
183
175
  @property({type: Boolean})
184
176
  showOuterLabels = false;
185
177
 
178
+ /** Whether to show unit in labels, default: false */
186
179
  @property({type: Boolean})
187
180
  showUnit = false;
188
181
 
182
+ /** Enable sunburst mode with interactive children subsegments, default: false */
189
183
  @property({type: Boolean})
190
184
  sunburst = false;
191
185
 
186
+ /** Unit string to append to outer labels, default: "%" */
192
187
  @property({type: String})
193
188
  outerLabelUnit = '%';
194
189
 
195
- /** @availableWhen showOuterLabels==true */
190
+ /**
191
+ * Maximum character length for labels before trim (0 = no limit), default: 0
192
+ * @availableWhen showOuterLabels==true
193
+ */
196
194
  @property({
197
195
  type: Number,
198
196
  })
199
197
  outerLabelMaxLength = 0;
200
198
 
199
+ /** Number of decimal places in labels, default: 0 */
201
200
  @property({
202
201
  type: Number,
203
202
  })
204
203
  outerLabelDecimalPlaces = 0;
205
204
 
205
+ /** Whether to display the legend below the chart, default: false */
206
206
  @property({type: Boolean, reflect: true})
207
207
  legend = false;
208
208
 
209
+ /** Show debug overlay for development, default: false */
209
210
  @property({type: Boolean, reflect: true})
210
211
  showDebugOverlay = false;
211
212
 
213
+ /** Fixed height of the chart in pixels (determines chart circumference), default: 320. The chart's circumference is always based on this fixed height to match other radial instruments. */
212
214
  @property({type: Number, reflect: true})
213
215
  fixedHeight = 320;
214
216