@dennisrongo/dsh-weather 0.3.2 → 0.4.0

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/README.md CHANGED
@@ -9,7 +9,7 @@ Weather bar for the [DeepSeek Harness](https://github.com/deepseek-ai/dsh) web U
9
9
 
10
10
  - **Data:** [Open-Meteo](https://open-meteo.com) (free, no API key) via the browser.
11
11
  - **Location:** `localStorage["dsh-weather:location"] = "Your City"` if set, else a coarse IP-geolocation provider chain, else New York.
12
- - **Placement:** centred on the space the shell has actually left it, not on the viewport. The bar measures the shell frame's content box which already excludes `dsh-mission-control`'s docked rail and subtracts any overlay flying a `data-dsh-overlay-claim="right"` marker, today `dsh-plan-board`'s plan panel. So it slides aside and sheds detail as the free span narrows instead of sitting underneath them.
12
+ - **Placement:** drag the pill anywhere. Until you move it, it stays centred on the space the shell has actually left it, not on the viewport it measures the shell frame's content box (which already excludes `dsh-mission-control`'s docked rail) and subtracts any overlay flying a `data-dsh-overlay-claim="right"` marker, today `dsh-plan-board`'s plan panel. After a drag the spot is remembered (cookie `dsh-weather-pos`, with `localStorage["dsh-weather:pos"]` as a fallback) so it survives a web UI or DSH Desktop restart. Click the temperature to toggle units; that does not start a drag.
13
13
  - **Mount point:** additive `shell.overlay` slot — pure-consumer client plugin, empty host half.
14
14
  - **Units:** Fahrenheit by default — click the temperature to toggle °F/°C. The choice is remembered in `localStorage["dsh-weather:unit"]`.
15
15
  - **Refresh:** every 15 min, plus a manual ⟳ button.
package/lib/client.js CHANGED
@@ -44,6 +44,81 @@ __export(client_exports, {
44
44
  });
45
45
  module.exports = __toCommonJS(client_exports);
46
46
  var import_react = __toESM(require("react"), 1);
47
+
48
+ // src/position.ts
49
+ var POS_COOKIE = "dsh-weather-pos";
50
+ var POS_KEY = "dsh-weather:pos";
51
+ var MAX_AGE = 31536e4;
52
+ function formatPos(pos) {
53
+ return `${pos.x.toFixed(4)},${pos.y.toFixed(4)}`;
54
+ }
55
+ function parsePos(raw) {
56
+ if (raw == null || raw === "") return null;
57
+ const comma = raw.indexOf(",");
58
+ if (comma === -1) return null;
59
+ const x = Number(raw.slice(0, comma));
60
+ const y = Number(raw.slice(comma + 1));
61
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
62
+ if (x < 0 || x > 1 || y < 0 || y > 1) return null;
63
+ return { x, y };
64
+ }
65
+ function rangeOf(box) {
66
+ const minLeft = box.pad;
67
+ const maxLeft = Math.max(minLeft, box.viewW - box.width - box.pad);
68
+ const minTop = Math.max(box.pad, box.minTop);
69
+ const maxTop = Math.max(minTop, box.viewH - box.height - box.pad);
70
+ return { minLeft, maxLeft, minTop, maxTop };
71
+ }
72
+ function clampPx(left, top, box) {
73
+ const { minLeft, maxLeft, minTop, maxTop } = rangeOf(box);
74
+ return {
75
+ left: Math.min(maxLeft, Math.max(minLeft, left)),
76
+ top: Math.min(maxTop, Math.max(minTop, top))
77
+ };
78
+ }
79
+ function posToPx(pos, box) {
80
+ const { minLeft, maxLeft, minTop, maxTop } = rangeOf(box);
81
+ return {
82
+ left: minLeft + pos.x * (maxLeft - minLeft),
83
+ top: minTop + pos.y * (maxTop - minTop)
84
+ };
85
+ }
86
+ function pxToPos(left, top, box) {
87
+ const { minLeft, maxLeft, minTop, maxTop } = rangeOf(box);
88
+ const spanX = maxLeft - minLeft;
89
+ const spanY = maxTop - minTop;
90
+ return {
91
+ x: spanX <= 0 ? 0 : (left - minLeft) / spanX,
92
+ y: spanY <= 0 ? 0 : (top - minTop) / spanY
93
+ };
94
+ }
95
+ function readCookie(jar, name) {
96
+ for (const part of jar.split(";")) {
97
+ const at = part.indexOf("=");
98
+ if (at === -1) continue;
99
+ if (part.slice(0, at).trim() !== name) continue;
100
+ return decodeURIComponent(part.slice(at + 1).trim());
101
+ }
102
+ return void 0;
103
+ }
104
+ function posCookieWrite(pos) {
105
+ return `${POS_COOKIE}=${encodeURIComponent(formatPos(pos))}; Path=/; Max-Age=${MAX_AGE}; SameSite=Lax`;
106
+ }
107
+ function loadPosFromStores(jar, storageGet) {
108
+ try {
109
+ const cookie = readCookie(jar, POS_COOKIE);
110
+ const fromCookie = parsePos(cookie);
111
+ if (fromCookie) return fromCookie;
112
+ } catch {
113
+ }
114
+ try {
115
+ return parsePos(storageGet(POS_KEY));
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ // src/client.tsx
47
122
  var import_jsx_runtime = require("react/jsx-runtime");
48
123
  var inject = ["slots"];
49
124
  function describeCode(code, isDay = true) {
@@ -94,6 +169,24 @@ function saveUnit(unit) {
94
169
  } catch {
95
170
  }
96
171
  }
172
+ function loadPos() {
173
+ try {
174
+ return loadPosFromStores(document.cookie, (key) => window.localStorage.getItem(key));
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+ function savePos(pos) {
180
+ try {
181
+ document.cookie = posCookieWrite(pos);
182
+ } catch {
183
+ }
184
+ try {
185
+ window.localStorage.setItem(POS_KEY, formatPos(pos));
186
+ } catch {
187
+ }
188
+ }
189
+ var DRAG_THRESHOLD_PX = 4;
97
190
  var DEFAULT_LOCATION = { latitude: 40.7128, longitude: -74.006, label: "New York" };
98
191
  var GEO_TIMEOUT_MS = 4e3;
99
192
  var GEO_PROVIDERS = [
@@ -211,9 +304,10 @@ var BAR_STYLES = `
211
304
  font: 400 13px/1.4 var(--dsw-font-family, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif);
212
305
  font-variant-numeric: tabular-nums;
213
306
  box-shadow: var(--dsw-shadow-lv3, 0 0 1px rgba(0,0,0,0.2), 0 8px 24px rgba(0,0,0,0.12));
214
- cursor: default;
307
+ cursor: grab;
215
308
  user-select: none;
216
309
  white-space: nowrap;
310
+ touch-action: none;
217
311
  /* DSH Desktop on Windows overlays a 36px window-drag strip at the top of the
218
312
  viewport (#dsh-desktop-windows-drag-region: -webkit-app-region: drag,
219
313
  z-index 2147483644, pointer-events: none) that the compositor resolves
@@ -229,9 +323,12 @@ var BAR_STYLES = `
229
323
  body.dsh-desktop-windows-titlebar-layout .dshwx {
230
324
  /* Clear the desktop drag strip: 36px strip + the usual 8px gap. The body
231
325
  class is added by DSH Desktop's preload on Windows only, so the browser
232
- and non-Windows builds keep top: 8px. */
326
+ and non-Windows builds keep top: 8px. A user-placed inline top overrides
327
+ this; clampPx's minTop keeps a drag from parking back inside the strip. */
233
328
  top: 44px;
234
329
  }
330
+ .dshwx[data-placed] { transform: none; }
331
+ .dshwx[data-dragging] { cursor: grabbing; }
235
332
  body[data-ds-dark-theme] .dshwx { box-shadow: 0 0 0 1px rgba(0,0,0,0.5), 0 8px 24px rgba(0,0,0,0.5); }
236
333
  .dshwx[hidden] { display: none; }
237
334
  .dshwx-icon { font-size: 16px; }
@@ -356,6 +453,20 @@ function zoomOf(el) {
356
453
  const width = el.getBoundingClientRect().width;
357
454
  return el.offsetWidth > 0 && width > 0 ? width / el.offsetWidth : 1;
358
455
  }
456
+ function desktopMinTop() {
457
+ return document.body.classList.contains("dsh-desktop-windows-titlebar-layout") ? 44 : 8;
458
+ }
459
+ function clampBoxOf(el, zoom) {
460
+ const rect = el.getBoundingClientRect();
461
+ return {
462
+ width: rect.width / zoom,
463
+ height: rect.height / zoom,
464
+ viewW: window.innerWidth / zoom,
465
+ viewH: window.innerHeight / zoom,
466
+ minTop: desktopMinTop(),
467
+ pad: 8
468
+ };
469
+ }
359
470
  function tierOf(width) {
360
471
  if (width === void 0) return "full";
361
472
  if (width < 200) return "none";
@@ -437,24 +548,126 @@ function WeatherBar() {
437
548
  const [state, setState] = import_react.default.useState({ status: "loading" });
438
549
  const [busy, setBusy] = import_react.default.useState(false);
439
550
  const [unit, setUnit] = import_react.default.useState(loadUnit);
551
+ const [placed, setPlaced] = import_react.default.useState(loadPos);
552
+ const [livePx, setLivePx] = import_react.default.useState(null);
553
+ const [dragging, setDragging] = import_react.default.useState(false);
554
+ const drag = import_react.default.useRef(null);
440
555
  const ref = import_react.default.useRef(null);
441
556
  const fit = useBandFit(ref);
557
+ const parked = placed !== null || dragging;
558
+ const measured = tierOf(fit?.width);
559
+ const fitTier = parked && measured === "none" ? "tiny" : measured;
560
+ import_react.default.useLayoutEffect(() => {
561
+ if (placed === null || dragging) return;
562
+ const el = ref.current;
563
+ if (el === null) return;
564
+ const apply2 = () => {
565
+ const zoom = zoomOf(el);
566
+ if (el.getBoundingClientRect().height === 0) return;
567
+ const next = posToPx(placed, clampBoxOf(el, zoom));
568
+ setLivePx(
569
+ (prev) => prev !== null && Math.abs(prev.left - next.left) < 0.5 && Math.abs(prev.top - next.top) < 0.5 ? prev : next
570
+ );
571
+ };
572
+ apply2();
573
+ const resize = new ResizeObserver(apply2);
574
+ resize.observe(el);
575
+ const scaleWatch = new MutationObserver(apply2);
576
+ scaleWatch.observe(document.body, { attributes: true, attributeFilter: ["style", "class"] });
577
+ window.addEventListener("resize", apply2);
578
+ return () => {
579
+ resize.disconnect();
580
+ scaleWatch.disconnect();
581
+ window.removeEventListener("resize", apply2);
582
+ };
583
+ }, [placed, dragging]);
584
+ const onPointerDown = (event) => {
585
+ if (event.button !== 0) return;
586
+ const node = event.target;
587
+ if (node instanceof Element && node.closest("button")) return;
588
+ const el = ref.current;
589
+ if (el === null) return;
590
+ const zoom = zoomOf(el);
591
+ const rect = el.getBoundingClientRect();
592
+ el.setPointerCapture(event.pointerId);
593
+ drag.current = {
594
+ pointerId: event.pointerId,
595
+ startX: event.clientX,
596
+ startY: event.clientY,
597
+ origLeft: rect.left / zoom,
598
+ origTop: rect.top / zoom,
599
+ zoom,
600
+ moved: false
601
+ };
602
+ };
603
+ const onPointerMove = (event) => {
604
+ const session = drag.current;
605
+ if (session === null || event.pointerId !== session.pointerId) return;
606
+ const el = ref.current;
607
+ if (el === null) return;
608
+ const dx = event.clientX - session.startX;
609
+ const dy = event.clientY - session.startY;
610
+ if (!session.moved) {
611
+ if (dx * dx + dy * dy < DRAG_THRESHOLD_PX * DRAG_THRESHOLD_PX) return;
612
+ session.moved = true;
613
+ setDragging(true);
614
+ }
615
+ const next = clampPx(
616
+ session.origLeft + dx / session.zoom,
617
+ session.origTop + dy / session.zoom,
618
+ clampBoxOf(el, session.zoom)
619
+ );
620
+ setLivePx(next);
621
+ };
622
+ const endDrag = (event) => {
623
+ const session = drag.current;
624
+ if (session === null || event.pointerId !== session.pointerId) return;
625
+ drag.current = null;
626
+ const el = ref.current;
627
+ if (!session.moved || el === null) {
628
+ setDragging(false);
629
+ return;
630
+ }
631
+ const box = clampBoxOf(el, session.zoom);
632
+ const next = clampPx(
633
+ session.origLeft + (event.clientX - session.startX) / session.zoom,
634
+ session.origTop + (event.clientY - session.startY) / session.zoom,
635
+ box
636
+ );
637
+ const pos = pxToPos(next.left, next.top, box);
638
+ setPlaced(pos);
639
+ setLivePx(next);
640
+ setDragging(false);
641
+ savePos(pos);
642
+ };
643
+ const bandStyle = fit === null || fit.width <= 0 ? {} : {
644
+ left: `${fit.centre / fit.zoom}px`,
645
+ maxWidth: `${fit.width / fit.zoom}px`
646
+ };
647
+ const parkedStyle = livePx === null ? { transform: "none" } : {
648
+ left: `${livePx.left}px`,
649
+ top: `${livePx.top}px`,
650
+ transform: "none"
651
+ };
442
652
  const shell = {
443
653
  ref,
444
654
  className: "dshwx",
445
- "data-fit": tierOf(fit?.width),
655
+ // A parked bar must not vanish when a dock claims the top band — the user
656
+ // put it somewhere on purpose. Map the squeezed-out tier up to tiny so it
657
+ // still sheds rather than hiding.
658
+ "data-fit": fitTier,
446
659
  // A non-positive width is the squeezed-out case: the `none` tier hides the
447
660
  // bar, and writing a negative max-width would be an ignored declaration
448
661
  // that left it at full size behind the overlay.
449
662
  //
450
663
  // Both lengths are divided by the zoom: `fit` is measured in viewport px
451
664
  // and these are author px the zoom scales again (see zoomOf).
452
- ...fit === null || fit.width <= 0 ? {} : {
453
- style: {
454
- left: `${fit.centre / fit.zoom}px`,
455
- maxWidth: `${fit.width / fit.zoom}px`
456
- }
457
- }
665
+ ...parked ? { "data-placed": "", style: parkedStyle } : fit === null || fit.width <= 0 ? {} : { style: bandStyle },
666
+ ...dragging ? { "data-dragging": "" } : {},
667
+ onPointerDown,
668
+ onPointerMove,
669
+ onPointerUp: endDrag,
670
+ onPointerCancel: endDrag
458
671
  };
459
672
  const toggleUnit = () => {
460
673
  setUnit((prev) => {
package/lib/index.js CHANGED
@@ -1,6 +1,88 @@
1
+ // src/position.ts
2
+ var POS_COOKIE = "dsh-weather-pos";
3
+ var POS_KEY = "dsh-weather:pos";
4
+ var MAX_AGE = 31536e4;
5
+ function formatPos(pos) {
6
+ return `${pos.x.toFixed(4)},${pos.y.toFixed(4)}`;
7
+ }
8
+ function parsePos(raw) {
9
+ if (raw == null || raw === "") return null;
10
+ const comma = raw.indexOf(",");
11
+ if (comma === -1) return null;
12
+ const x = Number(raw.slice(0, comma));
13
+ const y = Number(raw.slice(comma + 1));
14
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
15
+ if (x < 0 || x > 1 || y < 0 || y > 1) return null;
16
+ return { x, y };
17
+ }
18
+ function rangeOf(box) {
19
+ const minLeft = box.pad;
20
+ const maxLeft = Math.max(minLeft, box.viewW - box.width - box.pad);
21
+ const minTop = Math.max(box.pad, box.minTop);
22
+ const maxTop = Math.max(minTop, box.viewH - box.height - box.pad);
23
+ return { minLeft, maxLeft, minTop, maxTop };
24
+ }
25
+ function clampPx(left, top, box) {
26
+ const { minLeft, maxLeft, minTop, maxTop } = rangeOf(box);
27
+ return {
28
+ left: Math.min(maxLeft, Math.max(minLeft, left)),
29
+ top: Math.min(maxTop, Math.max(minTop, top))
30
+ };
31
+ }
32
+ function posToPx(pos, box) {
33
+ const { minLeft, maxLeft, minTop, maxTop } = rangeOf(box);
34
+ return {
35
+ left: minLeft + pos.x * (maxLeft - minLeft),
36
+ top: minTop + pos.y * (maxTop - minTop)
37
+ };
38
+ }
39
+ function pxToPos(left, top, box) {
40
+ const { minLeft, maxLeft, minTop, maxTop } = rangeOf(box);
41
+ const spanX = maxLeft - minLeft;
42
+ const spanY = maxTop - minTop;
43
+ return {
44
+ x: spanX <= 0 ? 0 : (left - minLeft) / spanX,
45
+ y: spanY <= 0 ? 0 : (top - minTop) / spanY
46
+ };
47
+ }
48
+ function readCookie(jar, name) {
49
+ for (const part of jar.split(";")) {
50
+ const at = part.indexOf("=");
51
+ if (at === -1) continue;
52
+ if (part.slice(0, at).trim() !== name) continue;
53
+ return decodeURIComponent(part.slice(at + 1).trim());
54
+ }
55
+ return void 0;
56
+ }
57
+ function posCookieWrite(pos) {
58
+ return `${POS_COOKIE}=${encodeURIComponent(formatPos(pos))}; Path=/; Max-Age=${MAX_AGE}; SameSite=Lax`;
59
+ }
60
+ function loadPosFromStores(jar, storageGet) {
61
+ try {
62
+ const cookie = readCookie(jar, POS_COOKIE);
63
+ const fromCookie = parsePos(cookie);
64
+ if (fromCookie) return fromCookie;
65
+ } catch {
66
+ }
67
+ try {
68
+ return parsePos(storageGet(POS_KEY));
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
1
74
  // src/index.ts
2
75
  function apply() {
3
76
  }
4
77
  export {
5
- apply
78
+ POS_COOKIE,
79
+ POS_KEY,
80
+ apply,
81
+ clampPx,
82
+ formatPos,
83
+ loadPosFromStores,
84
+ parsePos,
85
+ posCookieWrite,
86
+ posToPx,
87
+ pxToPos
6
88
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dennisrongo/dsh-weather",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Weather bar for DeepSeek Harness (dsh) — current conditions at the bottom of the web UI via Open-Meteo, rendered into shell.overlay",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,7 +29,7 @@
29
29
  "postbuild": "node build/postbuild.mjs",
30
30
  "deploy": "node build/postbuild.mjs",
31
31
  "typecheck": "tsc --noEmit",
32
- "test": "node build/build.mjs && node test/smoke.mjs"
32
+ "test": "node build/build.mjs && node test/smoke.mjs && node test/position.mjs"
33
33
  },
34
34
  "dsh": {
35
35
  "bundle": {