From 93c20e0c5ac37f0fbb570d68f5b17feb8b05207b Mon Sep 17 00:00:00 2001 From: Pedro Castro Date: Tue, 1 Jul 2025 12:56:43 -0300 Subject: [PATCH 1/6] Migrate to rescript webapi --- package-lock.json | 10 +++ package.json | 1 + rescript.json | 6 +- src/ApiDocs.res | 2 +- src/ConsolePanel.res | 4 +- src/Playground.res | 108 ++++++++++++++++++----------- src/Try.res | 6 +- src/bindings/Jsdom.res | 4 +- src/bindings/Webapi.res | 96 ------------------------- src/common/CompilerManagerHook.res | 2 +- src/common/EvalIFrame.res | 25 +++---- src/common/Hooks.res | 6 +- src/common/MetaTagsApi.res | 40 ++++------- src/components/CodeExample.res | 51 +++++--------- src/components/CodeMirror.res | 6 +- src/components/Docson.res | 4 +- src/components/ImageGallery.res | 4 +- src/components/Search.res | 19 +++-- src/layouts/LandingPageLayout.res | 27 ++++---- 19 files changed, 168 insertions(+), 253 deletions(-) delete mode 100644 src/bindings/Webapi.res diff --git a/package-lock.json b/package-lock.json index 9afa0d162..39df72a89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@headlessui/react": "^2.2.4", "@mdx-js/loader": "^3.1.0", "@rescript/react": "^0.14.0-rc.1", + "@rescript/webapi": "^0.1.0-experimental-03eae8b", "codemirror": "^5.54.0", "docson": "^2.1.0", "escodegen": "^2.1.0", @@ -2089,6 +2090,15 @@ "react-dom": ">=19.0.0" } }, + "node_modules/@rescript/webapi": { + "version": "0.1.0-experimental-03eae8b", + "resolved": "https://registry.npmjs.org/@rescript/webapi/-/webapi-0.1.0-experimental-03eae8b.tgz", + "integrity": "sha512-0McQ9XQlbF+/BWs70P2XJZ3wP6us7/HnNWAFnDYSnA9+Rvp6IQAuKCXfhbqJgTIge4YfiY5j+SqDt+OLfsSUTA==", + "license": "MIT", + "dependencies": { + "rescript": "^12.0.0-alpha.13" + } + }, "node_modules/@rescript/win32-x64": { "version": "12.0.0-alpha.14", "resolved": "https://registry.npmjs.org/@rescript/win32-x64/-/win32-x64-12.0.0-alpha.14.tgz", diff --git a/package.json b/package.json index dd63efb01..58869e8ac 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@headlessui/react": "^2.2.4", "@mdx-js/loader": "^3.1.0", "@rescript/react": "^0.14.0-rc.1", + "@rescript/webapi": "^0.1.0-experimental-03eae8b", "codemirror": "^5.54.0", "docson": "^2.1.0", "escodegen": "^2.1.0", diff --git a/rescript.json b/rescript.json index fc09510b1..87790843e 100644 --- a/rescript.json +++ b/rescript.json @@ -5,7 +5,11 @@ "version": 4 }, "bs-dependencies": [ - "@rescript/react" + "@rescript/react", + "@rescript/webapi" + ], + "bsc-flags": [ + "-open WebAPI.Global" ], "sources": [ { diff --git a/src/ApiDocs.res b/src/ApiDocs.res index 36f02582c..fba3abf17 100644 --- a/src/ApiDocs.res +++ b/src/ApiDocs.res @@ -95,7 +95,7 @@ module SidebarTree = { let version = url->Url.getVersionString let moduleRoute = - Webapi.URL.make("file://" ++ router.asPath).pathname + WebAPI.URL.make(~url="file://" ++ router.asPath).pathname ->String.replace(`/docs/manual/${version}/api/`, "") ->String.split("/") diff --git a/src/ConsolePanel.res b/src/ConsolePanel.res index 6c72f86f0..04a24364d 100644 --- a/src/ConsolePanel.res +++ b/src/ConsolePanel.res @@ -17,8 +17,8 @@ let make = (~logs, ~appendLog) => { | _ => () } } - Webapi.Window.addEventListener("message", cb) - Some(() => Webapi.Window.removeEventListener("message", cb)) + WebAPI.Window.addEventListener(window, WebAPI.EventAPI.Custom("message"), cb) + Some(() => WebAPI.Window.removeEventListener(window, WebAPI.EventAPI.Custom("message"), cb)) }, [appendLog])
diff --git a/src/Playground.res b/src/Playground.res index cb640325f..429327414 100644 --- a/src/Playground.res +++ b/src/Playground.res @@ -1064,7 +1064,7 @@ module ControlPanel = { let onClick = evt => { ReactEvent.Mouse.preventDefault(evt) - let ret = copyToClipboard(Webapi.Window.Location.href) + let ret = copyToClipboard(window.location.href) if ret { setState(_ => CopySuccess) } @@ -1129,12 +1129,12 @@ module ControlPanel = { } React.useEffect(() => { - Webapi.Window.addEventListener("keydown", onKeyDown) - Some(() => Webapi.Window.removeEventListener("keydown", onKeyDown)) + WebAPI.Window.addEventListener(window, WebAPI.EventAPI.Keydown, onKeyDown) + Some(() => WebAPI.Window.removeEventListener(window, WebAPI.EventAPI.Keydown, onKeyDown)) }, []) let runButtonText = { - let userAgent = Webapi.Window.Navigator.userAgent + let userAgent = window.navigator.userAgent let run = "Run" if userAgent->String.includes("iPhone") || userAgent->String.includes("Android") { run @@ -1496,9 +1496,7 @@ let make = (~versions: array) => { None }, (compilerState, compilerDispatch)) - let (layout, setLayout) = React.useState(_ => - Webapi.Window.innerWidth < breakingPoint ? Column : Row - ) + let (layout, setLayout) = React.useState(_ => window.innerWidth < breakingPoint ? Column : Row) let isDragging = React.useRef(false) @@ -1510,26 +1508,34 @@ let make = (~versions: array) => { let subPanelRef = React.useRef(Nullable.null) let onResize = () => { - let newLayout = Webapi.Window.innerWidth < breakingPoint ? Column : Row + let newLayout = window.innerWidth < breakingPoint ? Column : Row setLayout(_ => newLayout) switch panelRef.current->Nullable.toOption { | Some(element) => - let offsetTop = Webapi.Element.getBoundingClientRect(element)["top"] - Webapi.Element.Style.height(element, `calc(100vh - ${offsetTop->Float.toString}px)`) + let offsetTop = WebAPI.Element.getBoundingClientRect(element).top + WebAPI.Element.setAttribute( + element, + ~qualifiedName="style", + ~value=`height: calc(100vh - ${offsetTop->Float.toString}px)`, + ) | None => () } switch subPanelRef.current->Nullable.toOption { | Some(element) => - let offsetTop = Webapi.Element.getBoundingClientRect(element)["top"] - Webapi.Element.Style.height(element, `calc(100vh - ${offsetTop->Float.toString}px)`) + let offsetTop = WebAPI.Element.getBoundingClientRect(element).top + WebAPI.Element.setAttribute( + element, + ~qualifiedName="style", + ~value=`height: calc(100vh - ${offsetTop->Float.toString}px)`, + ) | None => () } } React.useEffect(() => { - Webapi.Window.addEventListener("resize", onResize) - Some(() => Webapi.Window.removeEventListener("resize", onResize)) + WebAPI.Window.addEventListener(window, WebAPI.EventAPI.Resize, onResize) + Some(() => WebAPI.Window.removeEventListener(window, WebAPI.EventAPI.Resize, onResize)) }, []) // To force CodeMirror render scrollbar on first render @@ -1552,30 +1558,50 @@ let make = (~versions: array) => { subPanelRef.current->Nullable.toOption, ) { | (Some(panelElement), Some(leftElement), Some(rightElement), Some(subElement)) => - let rectPanel = Webapi.Element.getBoundingClientRect(panelElement) + let rectPanel = WebAPI.Element.getBoundingClientRect(panelElement) // Update OutputPanel height - let offsetTop = Webapi.Element.getBoundingClientRect(subElement)["top"] - Webapi.Element.Style.height(subElement, `calc(100vh - ${offsetTop->Float.toString}px)`) + let offsetTop = WebAPI.Element.getBoundingClientRect(subElement).top + WebAPI.Element.setAttribute( + subElement, + ~qualifiedName="style", + ~value=`height: calc(100vh - ${offsetTop->Float.toString}px)`, + ) switch layout { | Row => - let delta = Int.toFloat(position) -. rectPanel["left"] + let delta = Int.toFloat(position) -. rectPanel.left - let leftWidth = delta /. rectPanel["width"] *. 100.0 - let rightWidth = (rectPanel["width"] -. delta) /. rectPanel["width"] *. 100.0 + let leftWidth = delta /. rectPanel.width *. 100.0 + let rightWidth = (rectPanel.width -. delta) /. rectPanel.width *. 100.0 - Webapi.Element.Style.width(leftElement, `${leftWidth->Float.toString}%`) - Webapi.Element.Style.width(rightElement, `${rightWidth->Float.toString}%`) + WebAPI.Element.setAttribute( + leftElement, + ~qualifiedName="style", + ~value=`width: ${leftWidth->Float.toString}%`, + ) + WebAPI.Element.setAttribute( + rightElement, + ~qualifiedName="style", + ~value=`width: ${rightWidth->Float.toString}%`, + ) | Column => - let delta = Int.toFloat(position) -. rectPanel["top"] + let delta = Int.toFloat(position) -. rectPanel.top - let topHeight = delta /. rectPanel["height"] *. 100. - let bottomHeight = (rectPanel["height"] -. delta) /. rectPanel["height"] *. 100. + let topHeight = delta /. rectPanel.height *. 100. + let bottomHeight = (rectPanel.height -. delta) /. rectPanel.height *. 100. - Webapi.Element.Style.height(leftElement, `${topHeight->Float.toString}%`) - Webapi.Element.Style.height(rightElement, `${bottomHeight->Float.toString}%`) + WebAPI.Element.setAttribute( + leftElement, + ~qualifiedName="style", + ~value=`height: ${topHeight->Float.toString}%`, + ) + WebAPI.Element.setAttribute( + rightElement, + ~qualifiedName="style", + ~value=`height: ${bottomHeight->Float.toString}%`, + ) } | _ => () } @@ -1594,15 +1620,15 @@ let make = (~versions: array) => { onMove(position) } - Webapi.Window.addEventListener("mousemove", onMouseMove) - Webapi.Window.addEventListener("touchmove", onTouchMove) - Webapi.Window.addEventListener("mouseup", onMouseUp) + WebAPI.Window.addEventListener(window, WebAPI.EventAPI.Mousemove, onMouseMove) + WebAPI.Window.addEventListener(window, WebAPI.EventAPI.Touchmove, onTouchMove) + WebAPI.Window.addEventListener(window, WebAPI.EventAPI.Mouseup, onMouseUp) Some( () => { - Webapi.Window.removeEventListener("mousemove", onMouseMove) - Webapi.Window.removeEventListener("touchmove", onTouchMove) - Webapi.Window.removeEventListener("mouseup", onMouseUp) + WebAPI.Window.removeEventListener(window, WebAPI.EventAPI.Mousemove, onMouseMove) + WebAPI.Window.removeEventListener(window, WebAPI.EventAPI.Touchmove, onTouchMove) + WebAPI.Window.removeEventListener(window, WebAPI.EventAPI.Mouseup, onMouseUp) }, ) }, [layout]) @@ -1765,10 +1791,10 @@ let make = (~versions: array) => { />
+ ref={ReactDOM.Ref.domRef(panelRef->Obj.magic)}> // Left Panel
>))} className={`${layout == Column ? "h-2/4" : "!h-full"} ${layout == Column ? "w-full" : "w-[50%]"}`}> @@ -1785,10 +1811,10 @@ let make = (~versions: array) => { | None => () | Some(timer) => clearTimeout(timer) } - let timer = setTimeout(() => { + let timer = setTimeout(~handler=() => { timeoutCompile.current() typingTimer.current = None - }, 100) + }, ~timeout=100) typingTimer.current = Some(timer) }} onMarkerFocus={rowCol => setFocusedRowCol(_prev => Some(rowCol))} @@ -1797,7 +1823,7 @@ let make = (~versions: array) => {
// Separator
>))} // TODO: touch-none not applied className={`flex items-center justify-center touch-none select-none bg-gray-70 opacity-30 hover:opacity-50 rounded-lg ${layout == Column @@ -1812,14 +1838,16 @@ let make = (~versions: array) => {
// Right Panel
Obj.magic)} className={`${layout == Column ? "h-6/15" : "!h-inherit"} ${layout == Column ? "w-full" : "w-[50%]"}`}>
{React.array(headers)}
-
+
>))} + className="overflow-auto">
diff --git a/src/Try.res b/src/Try.res index 15fa8f46c..feef200c7 100644 --- a/src/Try.res +++ b/src/Try.res @@ -33,10 +33,8 @@ let default = props => { let getStaticProps: Next.GetStaticProps.t = async _ => { let versions = { - let response = await Webapi.Fetch.fetch( - "https://cdn.rescript-lang.org/playground-bundles/versions.json", - ) - let json = await Webapi.Fetch.Response.json(response) + let response = await fetch("https://cdn.rescript-lang.org/playground-bundles/versions.json") + let json = await WebAPI.Response.json(response) json ->JSON.Decode.array ->Option.getOrThrow diff --git a/src/bindings/Jsdom.res b/src/bindings/Jsdom.res index 5021bab42..43f12c740 100644 --- a/src/bindings/Jsdom.res +++ b/src/bindings/Jsdom.res @@ -1,5 +1,5 @@ -type window = {document: Dom.document} -type t = {window: window} +type window = {document: WebAPI.DOMAPI.document} +type t = {window: WebAPI.DOMAPI.window} @module("jsdom") @new external make: string => t = "JSDOM" diff --git a/src/bindings/Webapi.res b/src/bindings/Webapi.res deleted file mode 100644 index ab53ceb41..000000000 --- a/src/bindings/Webapi.res +++ /dev/null @@ -1,96 +0,0 @@ -module Document = { - @val external document: Dom.element = "document" - @scope("document") @val external createElement: string => Dom.element = "createElement" - @scope("document") @val external createTextNode: string => Dom.element = "createTextNode" - @send - external querySelector: (Dom.document, string) => Nullable.t = "querySelector" - @send - external querySelectorAll: (Dom.document, string) => Js.Array2.array_like = - "querySelectorAll" -} - -module ClassList = { - type t - @send external toggle: (t, string) => unit = "toggle" - @send external remove: (t, string) => unit = "remove" -} - -module Element = { - @send external appendChild: (Dom.element, Dom.element) => unit = "appendChild" - @send external removeChild: (Dom.element, Dom.element) => unit = "removeChild" - @set external setClassName: (Dom.element, string) => unit = "className" - @get external classList: Dom.element => ClassList.t = "classList" - @send external getBoundingClientRect: Dom.element => {..} = "getBoundingClientRect" - @send external addEventListener: (Dom.element, string, unit => unit) => unit = "addEventListener" - @send external getAttribute: (Dom.element, string) => Nullable.t = "getAttribute" - - @send - external getElementById: (Dom.element, string) => Nullable.t = "getElementById" - - type contentWindow - @get external contentWindow: Dom.element => option = "contentWindow" - - @send - external postMessage: (contentWindow, string, ~targetOrigin: string=?) => unit = "postMessage" - - @send - external postMessageAny: (contentWindow, 'a, ~targetOrigin: string=?) => unit = "postMessage" - - module Style = { - @scope("style") @set external width: (Dom.element, string) => unit = "width" - @scope("style") @set external height: (Dom.element, string) => unit = "height" - } -} - -type animationFrameId - -@scope("window") @val -external requestAnimationFrame: (unit => unit) => animationFrameId = "requestAnimationFrame" - -@scope("window") @val -external cancelAnimationFrame: animationFrameId => unit = "cancelAnimationFrame" - -module Window = { - @scope("window") @val external addEventListener: (string, 'a => unit) => unit = "addEventListener" - @scope("window") @val - external removeEventListener: (string, 'a => unit) => unit = "removeEventListener" - @scope("window") @val external innerWidth: int = "innerWidth" - @scope("window") @val external innerHeight: int = "innerHeight" - @scope("window") @val external scrollY: int = "scrollY" - - module History = { - @scope(("window", "history")) @val - external pushState: (nullable<'a>, @as(json`""`) _, ~url: string=?) => unit = "pushState" - @scope(("window", "history")) @val - external replaceState: (nullable<'a>, @as(json`""`) _, ~url: string=?) => unit = "replaceState" - } - - module Location = { - @scope(("window", "location")) @val external href: string = "href" - } - - module Navigator = { - @scope(("window", "navigator")) @val external userAgent: string = "userAgent" - } -} - -module Fetch = { - module Response = { - type t - @send external text: t => promise = "text" - @send external json: t => promise = "json" - } - - @val external fetch: string => promise = "fetch" -} - -module URL = { - type t = { - hash: string, - host: string, - hostname: string, - href: string, - pathname: string, - } - @new external make: string => t = "URL" -} diff --git a/src/common/CompilerManagerHook.res b/src/common/CompilerManagerHook.res index 2d22a43e9..e17b92bac 100644 --- a/src/common/CompilerManagerHook.res +++ b/src/common/CompilerManagerHook.res @@ -583,7 +583,7 @@ let useCompilerManager = ( | SetupFailed(_) => () | Ready(ready) => let url = createUrl(router.route, ready) - Webapi.Window.History.replaceState(null, ~url) + WebAPI.History.replaceState(history, ~data=JSON.Null, ~unused="", ~url) } } diff --git a/src/common/EvalIFrame.res b/src/common/EvalIFrame.res index 507268f66..3531edcf1 100644 --- a/src/common/EvalIFrame.res +++ b/src/common/EvalIFrame.res @@ -69,23 +69,24 @@ let srcDoc = ` ` -type message = { - code: string, - imports: Dict.t, -} - let sendOutput = (code, imports) => { - open Webapi - - let frame = Document.document->Element.getElementById("iframe-eval") + let frame = document->WebAPI.Document.querySelector("#iframe-eval") switch frame { | Value(element) => - switch element->Element.contentWindow { - | Some(win) => win->Element.postMessageAny({code, imports}, ~targetOrigin="*") - | None => Console.error("contentWindow not found") + let element: WebAPI.DOMAPI.htmliFrameElement = element->Obj.magic + switch element.contentWindow { + | Value({window}) => + let message = JSON.Object( + dict{ + "code": JSON.String(code), + "imports": JSON.Object(imports->Dict.mapValues(v => JSON.String(v))), + }, + ) + window->WebAPI.Window.postMessage(~message, ~targetOrigin="*") + | Null => Console.error("contentWindow not found") } - | Null | Undefined => Console.error("iframe not found") + | Null => Console.error("iframe not found") } } diff --git a/src/common/Hooks.res b/src/common/Hooks.res index e20fda83b..283718314 100644 --- a/src/common/Hooks.res +++ b/src/common/Hooks.res @@ -40,7 +40,7 @@ let useScrollDirection = (~topMargin=80, ~threshold=20) => { React.useEffect(() => { let onScroll = _e => { setScrollDir(prev => { - let scrollY = Webapi.Window.scrollY + let scrollY = scrollY->Float.toInt let enterTopMargin = scrollY <= topMargin let action = switch prev { @@ -60,8 +60,8 @@ let useScrollDirection = (~topMargin=80, ~threshold=20) => { } }) } - Webapi.Window.addEventListener("scroll", onScroll) - Some(() => Webapi.Window.removeEventListener("scroll", onScroll)) + WebAPI.Window.addEventListener(window, WebAPI.EventAPI.Scroll, onScroll) + Some(() => WebAPI.Window.removeEventListener(window, WebAPI.EventAPI.Scroll, onScroll)) }, [topMargin, threshold]) scrollDir diff --git a/src/common/MetaTagsApi.res b/src/common/MetaTagsApi.res index 01a3dbc90..4f28d4a5b 100644 --- a/src/common/MetaTagsApi.res +++ b/src/common/MetaTagsApi.res @@ -5,43 +5,33 @@ type t = { } /** - This function uses JSDOM to fetch a webpage and extract the meta tags from it. + This function uses JSDOM to fetch a webpage and extract the meta tags from it. JSDOM is required since this runs on Node. */ let extractMetaTags = async (url: string) => { - open Webapi try { - let response = await Fetch.fetch(url) + let response = await fetch(url) - let html = await response->Fetch.Response.text + let html = await response->WebAPI.Response.text let dom = Jsdom.make(html) let document = dom.window.document - let metaTags = - document - ->Document.querySelectorAll("meta") - ->Array.fromArrayLike - ->Array.reduce(Dict.fromArray([]), (tags, meta) => { - let name = meta->Element.getAttribute("name")->Nullable.toOption - let property = meta->Element.getAttribute("property")->Nullable.toOption - let itemprop = meta->Element.getAttribute("itemprop")->Nullable.toOption + let nodeList = document->WebAPI.Document.querySelectorAll("meta") - let name = switch (name, property, itemprop) { - | (Some(name), _, _) => Some(name) - | (_, Some(property), _) => Some(property) - | (_, _, Some(itemprop)) => Some(itemprop) - | _ => None - } + let nodesArray = [] - let content = meta->Element.getAttribute("content")->Nullable.toOption + for i in 0 to nodeList.length { + let node = WebAPI.NodeList.item(nodeList, i) + nodesArray->Array.push(node) + } - switch (name, content) { - | (Some(name), Some(content)) => tags->Dict.set(name, content) - | _ => () - } + let metaTags = nodesArray->Array.reduce(Dict.fromArray([]), (tags, meta) => { + let name = meta->Obj.magic->WebAPI.Element.getAttribute("name") - tags - }) + let content = meta->Obj.magic->WebAPI.Element.getAttribute("content") + tags->Dict.set(name, content) + tags + }) let title = metaTags->Dict.get("og:title") let description = metaTags->Dict.get("og:description") diff --git a/src/components/CodeExample.res b/src/components/CodeExample.res index 0c84e3a9c..1418e498c 100644 --- a/src/components/CodeExample.res +++ b/src/components/CodeExample.res @@ -8,26 +8,6 @@ let langShortname = (lang: string) => | rest => rest } -module DomUtil = { - @scope("document") @val external createElement: string => Dom.element = "createElement" - @scope("document") @val external createTextNode: string => Dom.element = "createTextNode" - @send external appendChild: (Dom.element, Dom.element) => unit = "appendChild" - @send external removeChild: (Dom.element, Dom.element) => unit = "removeChild" - - @set external setClassName: (Dom.element, string) => unit = "className" - - type classList - @get external classList: Dom.element => classList = "classList" - @send external toggle: (classList, string) => unit = "toggle" - - type animationFrameId - @scope("window") @val - external requestAnimationFrame: (unit => unit) => animationFrameId = "requestAnimationFrame" - - @scope("window") @val - external cancelAnimationFrame: animationFrameId => unit = "cancelAnimationFrame" -} - module CopyButton = { let copyToClipboard: string => bool = %raw(` function(str) { @@ -77,31 +57,29 @@ module CopyButton = { React.useEffect(() => { switch state { | Copied => - open DomUtil let buttonEl = Nullable.toOption(buttonRef.current)->Option.getOrThrow // Note on this imperative DOM nonsense: // For Tailwind transitions to behave correctly, we need to first paint the DOM element in the tree, // and in the next tick, add the opacity-100 class, so the transition animation actually takes place. // If we don't do that, the banner will essentially pop up without any animation - let bannerEl = createElement("div") - bannerEl->setClassName( - "opacity-0 absolute -top-6 right-0 -mt-5 -mr-4 px-4 py-2 w-40 rounded-lg captions text-white bg-gray-100 text-gray-80-tr transition-all duration-1000 ease-in-out ", - ) - let textNode = createTextNode("Copied to clipboard") + let bannerEl = WebAPI.Document.createElement(document, "div") + bannerEl.className = "opacity-0 absolute -top-6 right-0 -mt-5 -mr-4 px-4 py-2 w-40 rounded-lg captions text-white bg-gray-100 text-gray-80-tr transition-all duration-1000 ease-in-out " + + let textNode = WebAPI.Document.createTextNode(document, "Copied to clipboard") - bannerEl->appendChild(textNode) - buttonEl->appendChild(bannerEl) + WebAPI.Element.appendChild(bannerEl, textNode)->ignore + WebAPI.Element.appendChild(buttonEl, bannerEl)->ignore - let nextFrameId = requestAnimationFrame(() => { - bannerEl->classList->toggle("opacity-0") - bannerEl->classList->toggle("opacity-100") + let nextFrameId = WebAPI.Window.requestAnimationFrame(window, _ => { + WebAPI.DOMTokenList.toggle(bannerEl.classList, ~token="opacity-0")->ignore + WebAPI.DOMTokenList.toggle(bannerEl.classList, ~token="opacity-100")->ignore }) - let timeoutId = setTimeout(() => { - buttonEl->removeChild(bannerEl) + let timeoutId = setTimeout(~handler=() => { + buttonEl->WebAPI.Element.removeChild(bannerEl)->ignore setState(_ => Init) - }, 3000) + }, ~timeout=3000) Some( () => { @@ -114,7 +92,10 @@ module CopyButton = { }, [state]) //Copy-Button