11 files changed,
+428,
-228
+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
+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) {
+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 }
+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 */
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 */
+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", {
+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) => {
+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 }
+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+}
+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+}
+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