WIP: waypoint edit screen
11 files changed,  +428, -228
M client/Makefile
+0, -1
1@@ -28,7 +28,6 @@ static:
2 js:
3 	@cp src/ffi/*.js lib/es6/src/ffi/
4 	@cp src/ffi/*.ts lib/es6/src/ffi/
5-	@cp src/ui/*.js lib/es6/src/ui/
6 	@echo "moved updated JS files"
7 .PHONY: js
8 
M client/lib/es6/src/App.res.mjs
+24, -0
 1@@ -8,6 +8,7 @@ import * as Menugroup from "./ui/Menugroup.res.mjs";
 2 import * as PreactDOM from "./ffi/PreactDOM.res.mjs";
 3 import * as MapInstance from "./ui/MapInstance.res.mjs";
 4 import * as Core__Option from "@rescript/core/lib/es6/src/Core__Option.res.mjs";
 5+import * as EditWaypoint from "./ui/EditWaypoint.res.mjs";
 6 import * as FloatingMenu from "./ui/FloatingMenu.res.mjs";
 7 import * as React from "@tiptap/react";
 8 import * as FormattingMenu from "./ui/FormattingMenu.res.mjs";
 9@@ -71,10 +72,33 @@ function handleLocalChanges(rawEvent) {
10   }
11 }
12 
13+function handleWaypointClick(rawEvent) {
14+  let gid = rawEvent.gid;
15+  let docID = Array.from(State.waypoints.keys()).find(docID => Core__Option.map(State.waypoints.get(docID), wps => wps.value.find(wp => wp.gid === gid)) !== undefined);
16+  if (docID === undefined) {
17+    return;
18+  }
19+  let wps = Core__Option.map(State.waypoints.get(docID), wps => wps.value);
20+  let attrs = Core__Option.flatMap(wps, wps => wps.find(wp => wp.gid === gid));
21+  if (wps !== undefined && attrs !== undefined) {
22+    Core__Option.map(State.Editor.get(docID), ed => {
23+      ed.commands.blur();
24+    });
25+    State.content.value = JsxRuntime.jsx(EditWaypoint.make, {
26+      docID: docID,
27+      waypoints: wps,
28+      attrs: attrs
29+    });
30+    return;
31+  }
32+}
33+
34 PreactDOM.addEventListener(doc, "ysweet-connection-status", handleConnectionStatus);
35 
36 PreactDOM.addEventListener(doc, "ysweet-local-changes", handleLocalChanges);
37 
38+PreactDOM.addEventListener(doc, "waypoint:click", handleWaypointClick);
39+
40 let data = Initdata.getInitdata();
41 
42 if (data !== undefined) {
M client/lib/es6/src/ffi/rtw-waypoint.ts
+121, -105
  1@@ -3,16 +3,16 @@
  2  * Includes the gid (global ID) of the waypoint.
  3  */
  4 export class WaypointHoverEvent extends Event {
  5-    readonly gid: string;
  6-
  7-    constructor(gid: string) {
  8-        super("waypoint:hover", {
  9-            bubbles: true,
 10-            cancelable: true,
 11-            composed: true,
 12-        });
 13-        this.gid = gid;
 14-    }
 15+  readonly gid: string;
 16+
 17+  constructor(gid: string) {
 18+    super("waypoint:hover", {
 19+      bubbles: true,
 20+      cancelable: true,
 21+      composed: true,
 22+    });
 23+    this.gid = gid;
 24+  }
 25 }
 26 
 27 /**
 28@@ -20,16 +20,16 @@ export class WaypointHoverEvent extends Event {
 29  * Includes the gid (global ID) of the waypoint.
 30  */
 31 export class WaypointClickEvent extends Event {
 32-    readonly gid: string;
 33-
 34-    constructor(gid: string) {
 35-        super("waypoint:click", {
 36-            bubbles: true,
 37-            cancelable: true,
 38-            composed: true,
 39-        });
 40-        this.gid = gid;
 41-    }
 42+  readonly gid: string;
 43+
 44+  constructor(gid: string) {
 45+    super("waypoint:click", {
 46+      bubbles: true,
 47+      cancelable: true,
 48+      composed: true,
 49+    });
 50+    this.gid = gid;
 51+  }
 52 }
 53 
 54 /**
 55@@ -47,99 +47,115 @@ export class WaypointClickEvent extends Event {
 56  * - error: Array of routing error codes (e.g., ["RouteTooFar"])
 57  */
 58 export class RTWWaypoint extends HTMLElement {
 59-    private boundHandleHover: (() => void) | null = null;
 60-    private boundHandleClick: (() => void) | null = null;
 61-
 62-    static get observedAttributes() {
 63-        return ["type", "id", "point", "label", "gid", "country", "from", "cons", "nonroutable", "error"];
 64+  private boundHandleHover: (() => void) | null = null;
 65+  private boundHandleClick: ((ev: MouseEvent) => void) | null = null;
 66+
 67+  static get observedAttributes() {
 68+    return [
 69+      "type",
 70+      "id",
 71+      "point",
 72+      "label",
 73+      "gid",
 74+      "country",
 75+      "from",
 76+      "cons",
 77+      "nonroutable",
 78+      "error",
 79+    ];
 80+  }
 81+
 82+  private updateErrorState() {
 83+    const errorAttr = this.getAttribute("error");
 84+    let errors: string[] = [];
 85+
 86+    try {
 87+      if (errorAttr) {
 88+        errors = JSON.parse(errorAttr);
 89+      }
 90+    } catch {
 91+      errors = [];
 92     }
 93 
 94-    private updateErrorState() {
 95-        const errorAttr = this.getAttribute("error");
 96-        let errors: string[] = [];
 97-
 98-        try {
 99-            if (errorAttr) {
100-                errors = JSON.parse(errorAttr);
101-            }
102-        } catch {
103-            errors = [];
104-        }
105-
106-        const hasErrors = Array.isArray(errors) && errors.length > 0;
107-
108-        if (hasErrors) {
109-            this.classList.add("error");
110-            if (errors.includes("RouteTooFar")) {
111-                this.title = "Too far away. Add a closer point.";
112-            } else {
113-                this.title = "Routing error: " + errors.join(", ");
114-            }
115-        } else {
116-            this.classList.remove("error");
117-            this.title = "";
118-        }
119+    const hasErrors = Array.isArray(errors) && errors.length > 0;
120+
121+    if (hasErrors) {
122+      this.classList.add("error");
123+      if (errors.includes("RouteTooFar")) {
124+        this.title = "Too far away. Add a closer point.";
125+      } else {
126+        this.title = "Routing error: " + errors.join(", ");
127+      }
128+    } else {
129+      this.classList.remove("error");
130+      this.title = "";
131     }
132-
133-    attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
134-        if (oldValue === newValue) return;
135-
136-        switch (name) {
137-            case "label":
138-                this.textContent = newValue || "";
139-                break;
140-            case "error":
141-                this.updateErrorState();
142-                break;
143-            // Other attributes are stored on the element and accessed via getAttribute when needed
144-        }
145-    }
146-
147-    connectedCallback() {
148-        // Set text content directly on the element (idempotent - safe on reconnect)
149-        this.textContent = this.getAttribute("label") || "";
150-
151-        // Set initial error state
152+  }
153+
154+  attributeChangedCallback(
155+    name: string,
156+    oldValue: string | null,
157+    newValue: string | null,
158+  ) {
159+    if (oldValue === newValue) return;
160+
161+    switch (name) {
162+      case "label":
163+        this.textContent = newValue || "";
164+        break;
165+      case "error":
166         this.updateErrorState();
167-
168-        // Create bound event handlers
169-        this.boundHandleHover = () => {
170-            console.debug("waypoint hover");
171-            const gid = this.getAttribute("gid");
172-            if (gid) {
173-                this.dispatchEvent(new WaypointHoverEvent(gid));
174-            }
175-        };
176-
177-        this.boundHandleClick = () => {
178-            console.debug("waypoint click");
179-            const gid = this.getAttribute("gid");
180-            if (gid) {
181-                this.dispatchEvent(new WaypointClickEvent(gid));
182-            }
183-        };
184-
185-        // Attach event listeners to the element itself
186-        this.addEventListener("mouseenter", this.boundHandleHover);
187-        this.addEventListener("click", this.boundHandleClick);
188+        break;
189+      // Other attributes are stored on the element and accessed via getAttribute when needed
190     }
191-
192-    disconnectedCallback() {
193-        // Remove event listeners from the element
194-        if (this.boundHandleHover) {
195-            this.removeEventListener("mouseenter", this.boundHandleHover);
196-        }
197-        if (this.boundHandleClick) {
198-            this.removeEventListener("click", this.boundHandleClick);
199-        }
200-
201-        // Clean up references
202-        this.boundHandleHover = null;
203-        this.boundHandleClick = null;
204+  }
205+
206+  connectedCallback() {
207+    // Set text content directly on the element (idempotent - safe on reconnect)
208+    this.textContent = this.getAttribute("label") || "";
209+
210+    // Set initial error state
211+    this.updateErrorState();
212+
213+    // Create bound event handlers
214+    this.boundHandleHover = () => {
215+      console.debug("waypoint hover");
216+      const gid = this.getAttribute("gid");
217+      if (gid) {
218+        this.dispatchEvent(new WaypointHoverEvent(gid));
219+      }
220+    };
221+
222+    this.boundHandleClick = (ev) => {
223+      ev.preventDefault();
224+      console.debug("waypoint click");
225+      const gid = this.getAttribute("gid");
226+      if (gid) {
227+        this.dispatchEvent(new WaypointClickEvent(gid));
228+      }
229+    };
230+
231+    // Attach event listeners to the element itself
232+    this.addEventListener("mouseenter", this.boundHandleHover);
233+    this.addEventListener("click", this.boundHandleClick);
234+  }
235+
236+  disconnectedCallback() {
237+    // Remove event listeners from the element
238+    if (this.boundHandleHover) {
239+      this.removeEventListener("mouseenter", this.boundHandleHover);
240     }
241+    if (this.boundHandleClick) {
242+      this.removeEventListener("click", this.boundHandleClick);
243+    }
244+
245+    // Clean up references
246+    this.boundHandleHover = null;
247+    this.boundHandleClick = null;
248+  }
249 }
250 
251 // Register the custom element if not already defined
252 if (!customElements.get("rtw-waypoint")) {
253-    customElements.define("rtw-waypoint", RTWWaypoint);
254+  customElements.define("rtw-waypoint", RTWWaypoint);
255 }
A client/lib/es6/src/ui/Content.res.mjs
+32, -0
 1@@ -0,0 +1,32 @@
 2+// Generated by ReScript, PLEASE EDIT WITH CARE
 3+
 4+import * as State from "../State.res.mjs";
 5+import * as LucidePreact from "lucide-preact";
 6+import * as JsxRuntime from "preact/jsx-runtime";
 7+
 8+function Content$CloseButton(props) {
 9+  let onClose = props.onClose;
10+  return JsxRuntime.jsx("div", {
11+    children: JsxRuntime.jsx("button", {
12+      children: JsxRuntime.jsx(LucidePreact.X, {}),
13+      className: "cursor-pointer",
14+      onClick: param => {
15+        State.content.value = undefined;
16+        if (onClose !== undefined) {
17+          return onClose();
18+        }
19+      }
20+    }),
21+    className: "text-gray-600 self-end w-fit cursor-pointer",
22+    title: "Close"
23+  });
24+}
25+
26+let CloseButton = {
27+  make: Content$CloseButton
28+};
29+
30+export {
31+  CloseButton,
32+}
33+/* State Not a pure module */
M client/lib/es6/src/ui/EditWaypoint.res.mjs
+46, -1
 1@@ -1,2 +1,47 @@
 2 // Generated by ReScript, PLEASE EDIT WITH CARE
 3-/* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */
 4+
 5+import * as State from "../State.res.mjs";
 6+import * as Content from "./Content.res.mjs";
 7+import * as Core__Option from "@rescript/core/lib/es6/src/Core__Option.res.mjs";
 8+import * as JsxRuntime from "preact/jsx-runtime";
 9+
10+function EditWaypoint(props) {
11+  let attrs = props.attrs;
12+  let docID = props.docID;
13+  State.docs.get(docID);
14+  let editor = State.Editor.get(docID);
15+  console.log("waypoints", props.waypoints);
16+  return JsxRuntime.jsxs("div", {
17+    children: [
18+      JsxRuntime.jsx(Content.CloseButton.make, {
19+        onClose: () => {
20+          setTimeout(() => {
21+            Core__Option.map(editor, ed => {
22+              ed.commands.blur();
23+            });
24+          }, 100);
25+        }
26+      }),
27+      JsxRuntime.jsx("h2", {
28+        children: attrs.label
29+      }),
30+      JsxRuntime.jsxs("div", {
31+        children: [
32+          JsxRuntime.jsx("div", {
33+            children: `Yearly weather averages for ` + attrs.label + `. Coming soon!`
34+          }),
35+          JsxRuntime.jsx("div", {})
36+        ],
37+        className: "w-full h-20 bg-blue-100 text-center flex flex-col items-center justify-center"
38+      })
39+    ],
40+    className: "flex flex-col"
41+  });
42+}
43+
44+let make = EditWaypoint;
45+
46+export {
47+  make,
48+}
49+/* State Not a pure module */
M client/lib/es6/src/ui/Login.res.mjs
+2, -11
 1@@ -3,9 +3,9 @@
 2 import * as Fetch from "../ffi/Fetch.res.mjs";
 3 import * as State from "../State.res.mjs";
 4 import * as Preact from "../ffi/Preact.res.mjs";
 5+import * as Content from "./Content.res.mjs";
 6 import * as Core__JSON from "@rescript/core/lib/es6/src/Core__JSON.res.mjs";
 7 import * as Core__Option from "@rescript/core/lib/es6/src/Core__Option.res.mjs";
 8-import * as LucidePreact from "lucide-preact";
 9 import * as Signals from "@preact/signals";
10 import * as JsxRuntime from "preact/jsx-runtime";
11 
12@@ -140,16 +140,7 @@ function Login(props) {
13   let err = errors.value;
14   return JsxRuntime.jsxs("div", {
15     children: [
16-      JsxRuntime.jsx("div", {
17-        children: JsxRuntime.jsx("button", {
18-          children: JsxRuntime.jsx(LucidePreact.X, {}),
19-          onClick: param => {
20-            State.content.value = undefined;
21-          }
22-        }),
23-        className: "text-gray-600 self-end w-fit cursor-pointer",
24-        title: "Close"
25-      }),
26+      JsxRuntime.jsx(Content.CloseButton.make, {}),
27       JsxRuntime.jsxs("div", {
28         children: [
29           JsxRuntime.jsxs("div", {
M client/src/App.res
+34, -0
 1@@ -40,8 +40,42 @@ let root = <BaseUIRoot />
 2     }
 3   }
 4 
 5+  let handleWaypointClick = (rawEvent: {..}) => {
 6+    let gid: string = Obj.magic(rawEvent["gid"])
 7+    // get the document ID that contains this waypoint gid
 8+    // TODO: for multiple docs that may contain the same gid, need to compose the event and add the document ID
 9+    // as it bubbles up through the editor
10+    let docID =
11+      State.waypoints
12+      ->Map.keys
13+      ->Iterator.toArray
14+      ->Array.find(docID => {
15+        switch State.waypoints
16+        ->Map.get(docID)
17+        ->Option.map(wps => wps->Preact.Signal.get->Array.find(wp => wp.gid == gid)) {
18+        | Some(_) => true
19+        | None => false
20+        }
21+      })
22+    switch docID {
23+    | Some(docID) => {
24+        let wps = State.waypoints->Map.get(docID)->Option.map(wps => wps->Preact.Signal.get)
25+        let attrs = wps->Option.flatMap(wps => wps->Array.find(wp => wp.gid == gid))
26+        switch (wps, attrs) {
27+        | (Some(waypoints), Some(attrs)) => {
28+            let _ = State.Editor.get(docID)->Option.map(ed => ed->Tiptap.Commands.blur)
29+            State.content->Preact.Signal.set(Some(<EditWaypoint docID waypoints attrs />))
30+          }
31+        | (_, _) => ()
32+        }
33+      }
34+    | None => ()
35+    }
36+  }
37+
38   let _ = PreactDOM.addEventListener(doc, "ysweet-connection-status", handleConnectionStatus)
39   let _ = PreactDOM.addEventListener(doc, "ysweet-local-changes", handleLocalChanges)
40+  let _ = PreactDOM.addEventListener(doc, "waypoint:click", handleWaypointClick)
41 
42   switch Initdata.getInitdata() {
43   | Some(data) => {
M client/src/ffi/rtw-waypoint.ts
+121, -105
  1@@ -3,16 +3,16 @@
  2  * Includes the gid (global ID) of the waypoint.
  3  */
  4 export class WaypointHoverEvent extends Event {
  5-    readonly gid: string;
  6-
  7-    constructor(gid: string) {
  8-        super("waypoint:hover", {
  9-            bubbles: true,
 10-            cancelable: true,
 11-            composed: true,
 12-        });
 13-        this.gid = gid;
 14-    }
 15+  readonly gid: string;
 16+
 17+  constructor(gid: string) {
 18+    super("waypoint:hover", {
 19+      bubbles: true,
 20+      cancelable: true,
 21+      composed: true,
 22+    });
 23+    this.gid = gid;
 24+  }
 25 }
 26 
 27 /**
 28@@ -20,16 +20,16 @@ export class WaypointHoverEvent extends Event {
 29  * Includes the gid (global ID) of the waypoint.
 30  */
 31 export class WaypointClickEvent extends Event {
 32-    readonly gid: string;
 33-
 34-    constructor(gid: string) {
 35-        super("waypoint:click", {
 36-            bubbles: true,
 37-            cancelable: true,
 38-            composed: true,
 39-        });
 40-        this.gid = gid;
 41-    }
 42+  readonly gid: string;
 43+
 44+  constructor(gid: string) {
 45+    super("waypoint:click", {
 46+      bubbles: true,
 47+      cancelable: true,
 48+      composed: true,
 49+    });
 50+    this.gid = gid;
 51+  }
 52 }
 53 
 54 /**
 55@@ -47,99 +47,115 @@ export class WaypointClickEvent extends Event {
 56  * - error: Array of routing error codes (e.g., ["RouteTooFar"])
 57  */
 58 export class RTWWaypoint extends HTMLElement {
 59-    private boundHandleHover: (() => void) | null = null;
 60-    private boundHandleClick: (() => void) | null = null;
 61-
 62-    static get observedAttributes() {
 63-        return ["type", "id", "point", "label", "gid", "country", "from", "cons", "nonroutable", "error"];
 64+  private boundHandleHover: (() => void) | null = null;
 65+  private boundHandleClick: ((ev: MouseEvent) => void) | null = null;
 66+
 67+  static get observedAttributes() {
 68+    return [
 69+      "type",
 70+      "id",
 71+      "point",
 72+      "label",
 73+      "gid",
 74+      "country",
 75+      "from",
 76+      "cons",
 77+      "nonroutable",
 78+      "error",
 79+    ];
 80+  }
 81+
 82+  private updateErrorState() {
 83+    const errorAttr = this.getAttribute("error");
 84+    let errors: string[] = [];
 85+
 86+    try {
 87+      if (errorAttr) {
 88+        errors = JSON.parse(errorAttr);
 89+      }
 90+    } catch {
 91+      errors = [];
 92     }
 93 
 94-    private updateErrorState() {
 95-        const errorAttr = this.getAttribute("error");
 96-        let errors: string[] = [];
 97-
 98-        try {
 99-            if (errorAttr) {
100-                errors = JSON.parse(errorAttr);
101-            }
102-        } catch {
103-            errors = [];
104-        }
105-
106-        const hasErrors = Array.isArray(errors) && errors.length > 0;
107-
108-        if (hasErrors) {
109-            this.classList.add("error");
110-            if (errors.includes("RouteTooFar")) {
111-                this.title = "Too far away. Add a closer point.";
112-            } else {
113-                this.title = "Routing error: " + errors.join(", ");
114-            }
115-        } else {
116-            this.classList.remove("error");
117-            this.title = "";
118-        }
119+    const hasErrors = Array.isArray(errors) && errors.length > 0;
120+
121+    if (hasErrors) {
122+      this.classList.add("error");
123+      if (errors.includes("RouteTooFar")) {
124+        this.title = "Too far away. Add a closer point.";
125+      } else {
126+        this.title = "Routing error: " + errors.join(", ");
127+      }
128+    } else {
129+      this.classList.remove("error");
130+      this.title = "";
131     }
132-
133-    attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
134-        if (oldValue === newValue) return;
135-
136-        switch (name) {
137-            case "label":
138-                this.textContent = newValue || "";
139-                break;
140-            case "error":
141-                this.updateErrorState();
142-                break;
143-            // Other attributes are stored on the element and accessed via getAttribute when needed
144-        }
145-    }
146-
147-    connectedCallback() {
148-        // Set text content directly on the element (idempotent - safe on reconnect)
149-        this.textContent = this.getAttribute("label") || "";
150-
151-        // Set initial error state
152+  }
153+
154+  attributeChangedCallback(
155+    name: string,
156+    oldValue: string | null,
157+    newValue: string | null,
158+  ) {
159+    if (oldValue === newValue) return;
160+
161+    switch (name) {
162+      case "label":
163+        this.textContent = newValue || "";
164+        break;
165+      case "error":
166         this.updateErrorState();
167-
168-        // Create bound event handlers
169-        this.boundHandleHover = () => {
170-            console.debug("waypoint hover");
171-            const gid = this.getAttribute("gid");
172-            if (gid) {
173-                this.dispatchEvent(new WaypointHoverEvent(gid));
174-            }
175-        };
176-
177-        this.boundHandleClick = () => {
178-            console.debug("waypoint click");
179-            const gid = this.getAttribute("gid");
180-            if (gid) {
181-                this.dispatchEvent(new WaypointClickEvent(gid));
182-            }
183-        };
184-
185-        // Attach event listeners to the element itself
186-        this.addEventListener("mouseenter", this.boundHandleHover);
187-        this.addEventListener("click", this.boundHandleClick);
188+        break;
189+      // Other attributes are stored on the element and accessed via getAttribute when needed
190     }
191-
192-    disconnectedCallback() {
193-        // Remove event listeners from the element
194-        if (this.boundHandleHover) {
195-            this.removeEventListener("mouseenter", this.boundHandleHover);
196-        }
197-        if (this.boundHandleClick) {
198-            this.removeEventListener("click", this.boundHandleClick);
199-        }
200-
201-        // Clean up references
202-        this.boundHandleHover = null;
203-        this.boundHandleClick = null;
204+  }
205+
206+  connectedCallback() {
207+    // Set text content directly on the element (idempotent - safe on reconnect)
208+    this.textContent = this.getAttribute("label") || "";
209+
210+    // Set initial error state
211+    this.updateErrorState();
212+
213+    // Create bound event handlers
214+    this.boundHandleHover = () => {
215+      console.debug("waypoint hover");
216+      const gid = this.getAttribute("gid");
217+      if (gid) {
218+        this.dispatchEvent(new WaypointHoverEvent(gid));
219+      }
220+    };
221+
222+    this.boundHandleClick = (ev) => {
223+      ev.preventDefault();
224+      console.debug("waypoint click");
225+      const gid = this.getAttribute("gid");
226+      if (gid) {
227+        this.dispatchEvent(new WaypointClickEvent(gid));
228+      }
229+    };
230+
231+    // Attach event listeners to the element itself
232+    this.addEventListener("mouseenter", this.boundHandleHover);
233+    this.addEventListener("click", this.boundHandleClick);
234+  }
235+
236+  disconnectedCallback() {
237+    // Remove event listeners from the element
238+    if (this.boundHandleHover) {
239+      this.removeEventListener("mouseenter", this.boundHandleHover);
240     }
241+    if (this.boundHandleClick) {
242+      this.removeEventListener("click", this.boundHandleClick);
243+    }
244+
245+    // Clean up references
246+    this.boundHandleHover = null;
247+    this.boundHandleClick = null;
248+  }
249 }
250 
251 // Register the custom element if not already defined
252 if (!customElements.get("rtw-waypoint")) {
253-    customElements.define("rtw-waypoint", RTWWaypoint);
254+  customElements.define("rtw-waypoint", RTWWaypoint);
255 }
A client/src/ui/Content.res
+22, -0
 1@@ -0,0 +1,22 @@
 2+open Preact
 3+
 4+// Content overlay close button, common to WaypointEdit, Login, etc.
 5+module CloseButton = {
 6+  @jsx.component
 7+  let make = (~onClose: option<unit => unit>=?) => {
 8+    <div className="text-gray-600 self-end w-fit cursor-pointer" title="Close">
 9+      <button
10+        className="cursor-pointer"
11+        onClick={_ => {
12+          State.content->Signal.set(None)
13+          switch onClose {
14+          | Some(fn) => fn()
15+          | None => ()
16+          }
17+        }}
18+      >
19+        <Icons.X />
20+      </button>
21+    </div>
22+  }
23+}
M client/src/ui/EditWaypoint.res
+25, -0
 1@@ -0,0 +1,25 @@
 2[email protected]
 3+let make = (
 4+  ~docID: string,
 5+  ~waypoints: array<Waypoint.attributes>,
 6+  ~attrs: Waypoint.attributes,
 7+) => {
 8+  let _ydoc = State.docs->Map.get(docID)
 9+  let editor = State.Editor.get(docID)
10+  Console.log2("waypoints", waypoints)
11+  <div className="flex flex-col">
12+    <Content.CloseButton
13+      onClose={() => {
14+        let _ = Window.setTimeout(() => {
15+          // little delay, then blur editor so the clicked waypoint isn't selected
16+          let _ = editor->Option.map(ed => ed->Tiptap.Commands.blur)
17+        }, 100)
18+      }}
19+    />
20+    <h2> {attrs.label->Preact.string} </h2>
21+    <div className="w-full h-20 bg-blue-100 text-center flex flex-col items-center justify-center">
22+      <div> {`Yearly weather averages for ${attrs.label}. Coming soon!`->Preact.string} </div>
23+      <div></div>
24+    </div>
25+  </div>
26+}
M client/src/ui/Login.res
+1, -5
 1@@ -120,11 +120,7 @@ let fields = "bg-white border border-gray-300 text-gray-900 rounded-lg focus:rin
 2 @jsx.component
 3 let make = () => {
 4   <div className="flex flex-col p-10 md:p-15">
 5-    <div className="text-gray-600 self-end w-fit cursor-pointer" title="Close">
 6-      <button onClick={_ => State.content->Signal.set(None)}>
 7-        <Icons.X />
 8-      </button>
 9-    </div>
10+    <Content.CloseButton />
11     <div className="space-y-4 md:space-y-6">
12       <div className="">
13         <label