-
-
Array.get(index) {
+ | None => React.null
+ | Some(src) =>
+
+
setSelected(_ => index + 1 < count ? index + 1 : 0)}
>
-
-
+ Int.toString}`}
+ loading=?imgLoading
+ />
+
+
+ {imgSrcs
+ ->Array.mapWithIndex((src, i) => {
+ let color = i === index ? "text-gray-40" : "text-gray-70"
+ Int.toString}`}
+ ariaPressed={i === index ? #"true" : #"false"}
+ className={`gallery-selector flex items-center hover:cursor-pointer hover:text-gray-40 h-8 w-8 ${color}`}
+ onClick={_ => setSelected(_ => i)}
+ />
+ })
+ ->React.array}
+
- {lineEls->React.array}
-
+ }
}
diff --git a/apps/docs/src/components/LandingPageCopyButton.res b/apps/docs/src/components/LandingPageCopyButton.res
new file mode 100644
index 000000000..a65b87e35
--- /dev/null
+++ b/apps/docs/src/components/LandingPageCopyButton.res
@@ -0,0 +1,56 @@
+type state =
+ | Idle
+ | Pending
+ | Copied
+ | Failed
+
+@react.component
+let make = (~code, ~writeClipboard=Clipboard.writeText) => {
+ let (state, setState) = React.useState(_ => Idle)
+
+ let feedbackRef = React.useCallback(_ => {
+ let timer = setTimeout(~handler=() => setState(_ => Idle), ~timeout=2000)
+ Some(() => clearTimeout(timer))
+ }, [])
+
+ let copy = async () => {
+ setState(_ => Pending)
+ let result = await writeClipboard(code)
+ setState(_ => {
+ switch result {
+ | Ok() => Copied
+ | Error(Clipboard.WriteFailed) => Failed
+ }
+ })
+ }
+
+ let feedback = switch state {
+ | Idle | Pending => React.null
+ | Copied =>
+
+ {React.string("Copied!")}
+
+ | Failed =>
+
+ {React.string("Could not copy. Try again.")}
+
+ }
+
+ <>
+
copy()->ignore}
+ ariaLabel={"Copy " ++ code ++ " command"}
+ >
+
+
+
feedback
+ >
+}
diff --git a/apps/docs/src/components/LandingPageCopyButton.resi b/apps/docs/src/components/LandingPageCopyButton.resi
new file mode 100644
index 000000000..2f0b04179
--- /dev/null
+++ b/apps/docs/src/components/LandingPageCopyButton.resi
@@ -0,0 +1,5 @@
+@react.component
+let make: (
+ ~code: string,
+ ~writeClipboard: string => promise
>=?,
+) => React.element
diff --git a/apps/docs/src/components/LandingPageInstallInstructions.res b/apps/docs/src/components/LandingPageInstallInstructions.res
new file mode 100644
index 000000000..f3e4d0562
--- /dev/null
+++ b/apps/docs/src/components/LandingPageInstallInstructions.res
@@ -0,0 +1,25 @@
+let copyBox = text => {
+
+ {React.string(text)}
+
+
+}
+
+@react.component
+let make = (~className="") => {
+
+
{React.string("Quick Install")}
+
+ {React.string(
+ "You can quickly add ReScript to your existing JavaScript codebase via npm / yarn:",
+ )}
+
+ {copyBox("npm install rescript")}
+
+ {React.string("Or generate a new project from the official template with npx:")}
+
+ {copyBox("npx create-rescript-app")}
+
+}
diff --git a/apps/docs/src/components/LandingPageInstallInstructions.resi b/apps/docs/src/components/LandingPageInstallInstructions.resi
new file mode 100644
index 000000000..193c81cf3
--- /dev/null
+++ b/apps/docs/src/components/LandingPageInstallInstructions.resi
@@ -0,0 +1,2 @@
+@react.component
+let make: (~className: string=?) => React.element
diff --git a/apps/docs/src/components/LandingPageQuickInstall.res b/apps/docs/src/components/LandingPageQuickInstall.res
index b24d8f6f9..bb263356a 100644
--- a/apps/docs/src/components/LandingPageQuickInstall.res
+++ b/apps/docs/src/components/LandingPageQuickInstall.res
@@ -1,122 +1,3 @@
-module CopyButton = {
- let copyToClipboard: string => bool = %raw(`
- function(str) {
- try {
- const el = document.createElement('textarea');
- el.value = str;
- el.setAttribute('readonly', '');
- el.style.position = 'absolute';
- el.style.left = '-9999px';
- document.body.appendChild(el);
- const selected =
- document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false;
- el.select();
- document.execCommand('copy');
- document.body.removeChild(el);
- if (selected) {
- document.getSelection().removeAllRanges();
- document.getSelection().addRange(selected);
- }
- return true;
- } catch(e) {
- return false;
- }
- }
- `)
-
- type state =
- | Init
- | Copied
- | Failed
-
- @react.component
- let make = (~code) => {
- let (state, setState) = React.useState(_ => Init)
- let buttonRef = React.useRef(Nullable.null)
-
- let onClick = evt => {
- ReactEvent.Mouse.preventDefault(evt)
- if copyToClipboard(code) {
- setState(_ => Copied)
- } else {
- setState(_ => Failed)
- }
- }
-
- React.useEffect(() => {
- switch state {
- | Copied =>
- let buttonEl = Nullable.toOption(buttonRef.current)->Option.getOrThrow
-
- let bannerEl = WebAPI.Document.createElement(document, "div")
- bannerEl.className = "foobar opacity-0 absolute top-0 mt-4 -mr-1 px-2 rounded right-0
- bg-turtle text-gray-80-tr body-sm
- transition-all duration-500 ease-in-out "
- let textNode = WebAPI.Document.createTextNode(document, "Copied!")
-
- WebAPI.Element.appendChild(bannerEl, textNode)->ignore
- WebAPI.Element.appendChild(buttonEl, bannerEl)->ignore
-
- 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(~handler=() => {
- buttonEl->WebAPI.Element.removeChild(bannerEl)->ignore
- setState(_ => Init)
- }, ~timeout=2000)
-
- Some(
- () => {
- cancelAnimationFrame(nextFrameId)
- clearTimeout(timeoutId)
- },
- )
- | _ => None
- }
- }, [state])
-
- >))}
- disabled={state === Copied}
- className="relative h-10 w-10 flex justify-center items-center "
- onClick
- ariaLabel={"Copy " ++ code ++ " command"}
- >
-
-
- }
-}
-
-module Instructions = {
- let copyBox = text => {
-
- {React.string(text)}
-
-
- }
-
- @react.component
- let make = (~className: string="") => {
-
-
{React.string("Quick Install")}
-
- {React.string(
- "You can quickly add ReScript to your existing JavaScript codebase via npm / yarn:",
- )}
-
- {copyBox("npm install rescript")}
-
- {React.string("Or generate a new project from the official template with npx:")}
-
- {copyBox("npx create-rescript-app")}
-
- }
-}
-
@react.component
let make = () => {
@@ -135,7 +16,7 @@ let make = () => {
>
{React.string(`ReScript is used to ship and maintain mission-critical products with good UI and UX.`)}
-
+
diff --git a/apps/docs/src/components/LandingPageQuickInstall.resi b/apps/docs/src/components/LandingPageQuickInstall.resi
index 1ca44ce26..9d792611d 100644
--- a/apps/docs/src/components/LandingPageQuickInstall.resi
+++ b/apps/docs/src/components/LandingPageQuickInstall.resi
@@ -1,2 +1,3 @@
+/** Presentational install section with event-driven clipboard controls. */
@react.component
let make: unit => React.element
diff --git a/apps/docs/styles/main.css b/apps/docs/styles/main.css
index ac10e3e5c..b538e8db8 100644
--- a/apps/docs/styles/main.css
+++ b/apps/docs/styles/main.css
@@ -569,6 +569,29 @@
display: inline-block;
}
+.gallery-selector::after {
+ content: "";
+ inline-size: 100%;
+ block-size: 1px;
+ background-color: currentColor;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .gallery-photo {
+ animation: gallery-fade-in 1s ease-in-out;
+ }
+}
+
+@keyframes gallery-fade-in {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
.version-popover[popover] {
inset: unset;
width: 100%;
diff --git a/apps/docs/vitest.config.mjs b/apps/docs/vitest.config.mjs
index 7bd863a6a..c39aeb9fa 100644
--- a/apps/docs/vitest.config.mjs
+++ b/apps/docs/vitest.config.mjs
@@ -46,6 +46,7 @@ export default defineConfig({
provider: playwright({
contextOptions: {
deviceScaleFactor: 1,
+ permissions: ["clipboard-read", "clipboard-write"],
},
}),
ui: false,
diff --git a/packages/shared/src/Vitest.res b/packages/shared/src/Vitest.res
index 2555d707b..1135b0bd2 100644
--- a/packages/shared/src/Vitest.res
+++ b/packages/shared/src/Vitest.res
@@ -33,6 +33,9 @@ external render: Jsx.element => promise