@meri-imperiumi/signalk-dead-reckoning 0.12.1 → 0.12.2

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.12.2] - 2026-09-28
11
+
12
+ ### Fixed
13
+ - **The top-right plotter-extension area now behaves as a top-right
14
+ anchor.** Two defects made it read as "top left of the GPS status
15
+ bar": the area rode its column's left edge instead of the GPS
16
+ panel's right edge (it now shares the panel's right alignment), and
17
+ the widget grid rendered logical col 0 at the physical left for
18
+ every anchor — so the first placed widget sat farthest from the
19
+ viewport corner with the free slots between it and the edge.
20
+ Right-hand anchors now mirror the column axis: col 0 hugs the
21
+ right-hand corner and widgets pack corner-inward, symmetric with
22
+ the top-left area.
23
+
10
24
  ## [0.12.1] - 2026-09-28
11
25
 
12
26
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meri-imperiumi/signalk-dead-reckoning",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "description": "Offline-first dead reckoning and sensor fusion engine for Signal K",
5
5
  "main": "plugin/index.js",
6
6
  "scripts": {
@@ -27,7 +27,7 @@
27
27
  "signalk-plugin-enabled-by-default": false,
28
28
  "signalk": {
29
29
  "displayName": "Dead Reckoning",
30
- "appIcon": "./icon.png",
30
+ "appIcon": "./icon-256.png",
31
31
  "requires": [
32
32
  "@meri-imperiumi/signalk-internet"
33
33
  ],
package/public/dr-app.js CHANGED
@@ -256,7 +256,13 @@ template.innerHTML = /* html */ `
256
256
  gap: 0.5rem;
257
257
  align-items: flex-start;
258
258
  }
259
- .dr-top-col > .dr-gps {
259
+ /* The right column is as wide as its widest child (usually the GPS
260
+ panel), so without this both the panel and the top-right widget
261
+ area would need explicit right edges: the panel rides the right
262
+ edge, and the top-right area must follow it — left-aligned it
263
+ would park under the panel's left half, reading as "top left". */
264
+ .dr-top-col > .dr-gps,
265
+ .dr-top-col > dr-ext-widget-area[anchor="top-right"] {
260
266
  align-self: flex-end;
261
267
  }
262
268
  /* Water-track readout: bottom-right panel — figures + the manual
@@ -151,6 +151,38 @@ export function occupiedCells(placements) {
151
151
  return occ;
152
152
  }
153
153
 
154
+ /**
155
+ * Do placements in this anchor mirror their column axis? Right-hand
156
+ * anchors render mirrored: logical col 0 is the viewport corner (the
157
+ * RIGHT edge), so widgets pack corner-inward — symmetric with left
158
+ * anchors, where col 0 is the left edge. Without the mirror the first
159
+ * placed widget lands in the cell farthest from the corner and the
160
+ * free space sits between it and the edge.
161
+ *
162
+ * @param {string} anchor
163
+ * @returns {boolean}
164
+ */
165
+ export function isMirroredAnchor(anchor) {
166
+ return typeof anchor === "string" && anchor.endsWith("-right");
167
+ }
168
+
169
+ /**
170
+ * CSS grid column line (1-based, physical left→right) where a
171
+ * placement's left edge renders for the given anchor. Mirrored
172
+ * (right-hand) anchors flip the column axis: a placement at logical
173
+ * column `col` spans lines `GRID_COLS - col - cols + 1 ..
174
+ * GRID_COLS - col`.
175
+ *
176
+ * @param {string} anchor
177
+ * @param {number} col - 0-based logical column (0 = the anchor corner)
178
+ * @param {number} cols - placement column span
179
+ * @returns {number} 1-based CSS grid column line
180
+ */
181
+ export function gridColLine(anchor, col, cols) {
182
+ if (isMirroredAnchor(anchor)) return GRID_COLS - col - cols + 1;
183
+ return col + 1;
184
+ }
185
+
154
186
  /**
155
187
  * First position (packing from the grid origin, row-major) where a
156
188
  * widget of this size fits without overlapping existing placements.
@@ -1032,7 +1032,11 @@ export class DrExtWidgetArea extends AreaBase {
1032
1032
  const size = core.parseSize(p.size) ?? { cols: 1, rows: 1 };
1033
1033
  cell = document.createElement("div");
1034
1034
  cell.className = "cell";
1035
- cell.style.gridArea = `${p.row + 1} / ${p.col + 1} / span ${size.rows} / span ${size.cols}`;
1035
+ // Column line via core.gridColLine: right-hand anchors mirror
1036
+ // the column axis so col 0 renders at the viewport corner.
1037
+ cell.style.gridArea =
1038
+ `${p.row + 1} / ${core.gridColLine(this.anchor, p.col, size.cols)} ` +
1039
+ `/ span ${size.rows} / span ${size.cols}`;
1036
1040
  this.grid.appendChild(cell);
1037
1041
  this.cells.set(p.instanceId, cell);
1038
1042
  }
Binary file
@@ -101,6 +101,23 @@ test("firstFreeSlot: packs row-major, skips overlaps", () => {
101
101
  assert.equal(core.firstFreeSlot(full, "1x1"), null);
102
102
  });
103
103
 
104
+ test("gridColLine: right-hand anchors mirror the column axis", () => {
105
+ // Left anchor: col 0 is the leftmost CSS column (the corner).
106
+ assert.equal(core.isMirroredAnchor("top-left"), false);
107
+ assert.equal(core.gridColLine("top-left", 0, 1), 1);
108
+ assert.equal(core.gridColLine("top-left", 1, 1), 2);
109
+ // Right anchor: col 0 is the RIGHTMOST CSS column (the corner), so
110
+ // the first placed widget hugs the viewport edge and free space
111
+ // extends inward.
112
+ assert.equal(core.isMirroredAnchor("top-right"), true);
113
+ assert.equal(core.gridColLine("top-right", 0, 1), 2);
114
+ assert.equal(core.gridColLine("top-right", 1, 1), 1);
115
+ // Full-width spans cover both columns either way; a 1x2 in the
116
+ // corner column stays on the right edge for both rows.
117
+ assert.equal(core.gridColLine("top-right", 0, 2), 1);
118
+ assert.equal(core.gridColLine("top-right", 0, 1), core.GRID_COLS);
119
+ });
120
+
104
121
  test("layoutFromJSON: validates shape, ignores unknown anchors", () => {
105
122
  const good = core.layoutFromJSON({
106
123
  version: 1,
@@ -353,6 +353,16 @@ test("dr-app: widget areas sit below the app's own top panels", () => {
353
353
  );
354
354
  assert.ok(colRight, "top-right area follows the GPS panel in its column");
355
355
  assert.match(appSrc, /\.dr-top-col \{[^}]*flex-direction: column/s);
356
+ assert.match(
357
+ appSrc,
358
+ /\.dr-top-col > \.dr-gps,\s*\.dr-top-col > dr-ext-widget-area\[anchor="top-right"\]\s*\{\s*align-self: flex-end;/s,
359
+ "top-right area right-aligns with the GPS panel in its column",
360
+ );
361
+ assert.match(
362
+ hostSrc,
363
+ /core\.gridColLine\(this\.anchor, p\.col, size\.cols\)/,
364
+ "cell columns route through the anchor-aware mirroring helper",
365
+ );
356
366
  assert.match(
357
367
  appSrc,
358
368
  /new PlotterExtHost\(\{\s*stream: window\.drSignalkStream,\s*mount: root,/s,