From a374accd499500093004afb3bbfb58e5c3ceef58 Mon Sep 17 00:00:00 2001 From: Denis Strelkov Date: Mon, 13 Oct 2025 12:00:27 +0800 Subject: [PATCH 01/48] Emit js --- src/dune | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/dune b/src/dune index 009d259..847557e 100644 --- a/src/dune +++ b/src/dune @@ -5,3 +5,19 @@ (libraries melange.belt reason-react styled-ppx.melange) (public_name reshowcase) (modes melange)) + +(melange.emit + (target browser) + (alias browser) + (libraries reshowcase) + (preprocess + (pps melange.ppx reason-react-ppx)) + (modules) + (module_systems es6)) + +(melange.emit + (target node) + (alias node) + (libraries reshowcase) + (modules) + (module_systems (commonjs mjs))) From 69743a2c4812e37faf8272536e46c6b6cc288585 Mon Sep 17 00:00:00 2001 From: Denis Strelkov Date: Mon, 13 Oct 2025 13:35:10 +0800 Subject: [PATCH 02/48] Boilerplate for the new api --- dune | 2 +- example/Demo.re | 10 +++- newexample/.ocamlformat | 0 newexample/ButtonHuge.re | 59 ++++++++++++++++++++++ newexample/ButtonNormal.re | 53 ++++++++++++++++++++ newexample/CodeExample.re | 43 ++++++++++++++++ newexample/H1.re | 15 ++++++ newexample/H2.re | 4 ++ newexample/Italic.re | 4 ++ newexample/NewDemo.re | 66 +++++++++++++++++++++++++ newexample/OneTwoFourSeven.re | 2 + newexample/OneTwoThreeFive.re | 2 + newexample/OneTwoThreeFour.re | 2 + newexample/Paragraph.re | 4 ++ newexample/dune | 7 +++ src/Foo.re | 6 +++ src/NewEntry.re | 15 ++++++ src/Process.re | 19 ++++++++ src/ReshowcaseUi.re | 3 +- src/Utils.re | 92 +++++++++++++++++++++++++++++++++++ src/dune | 12 ++--- tests/HighlightTermsTest.re | 9 +++- 22 files changed, 419 insertions(+), 10 deletions(-) create mode 100644 newexample/.ocamlformat create mode 100644 newexample/ButtonHuge.re create mode 100644 newexample/ButtonNormal.re create mode 100644 newexample/CodeExample.re create mode 100644 newexample/H1.re create mode 100644 newexample/H2.re create mode 100644 newexample/Italic.re create mode 100644 newexample/NewDemo.re create mode 100644 newexample/OneTwoFourSeven.re create mode 100644 newexample/OneTwoThreeFive.re create mode 100644 newexample/OneTwoThreeFour.re create mode 100644 newexample/Paragraph.re create mode 100644 newexample/dune create mode 100644 src/Foo.re create mode 100644 src/NewEntry.re create mode 100644 src/Process.re create mode 100644 src/Utils.re diff --git a/dune b/dune index 67071b8..35149f0 100644 --- a/dune +++ b/dune @@ -1,4 +1,4 @@ -(dirs commands example src tests) +(dirs commands example newexample src tests) (install (section bin) diff --git a/example/Demo.re b/example/Demo.re index 877d3da..f607a5d 100644 --- a/example/Demo.re +++ b/example/Demo.re @@ -113,7 +113,15 @@ demo(({addDemo: _, addCategory}) => addCategory("Headings", ({addDemo, addCategory: _}) => { addDemo("H1", ({string, int, _}) => { let size = - int("Font size", {min: 0, max: 100, initial: 30, step: 1}); + int( + "Font size", + { + min: 0, + max: 100, + initial: 30, + step: 1, + }, + );

{string("Text", "hello")->React.string} diff --git a/newexample/.ocamlformat b/newexample/.ocamlformat new file mode 100644 index 0000000..e69de29 diff --git a/newexample/ButtonHuge.re b/newexample/ButtonHuge.re new file mode 100644 index 0000000..b55d6b3 --- /dev/null +++ b/newexample/ButtonHuge.re @@ -0,0 +1,59 @@ +let spaceConcat = (x1, x2) => + switch (x1, x2) { + | ("", x) + | (x, "") => x + | (x1, x2) => x1 ++ " " ++ x2 + }; + +let (+++) = spaceConcat; + +module Cn = { + let ifTrue = (cn, x) => x ? cn : ""; +}; + +module Css = { + let button = [%cx + {| + color: #fff; + border: none; + padding: 10px; + border-radius: 10px; + font-family: inherit; + font-size: inherit; + |} + ]; + + let buttonHuge = [%cx {| + padding: 20px; + font-size: 30px; + |}]; + + let buttonDisabled = [%cx {| + cursor: default; + opacity: 0.5; + |}]; + + let buttonColor = color => { + let color = `hex(color); + [%cx {| + background-color: $(color); + |}]; + }; +}; + +[@react.component] +let make = () => { + let disabled = false; + let color = "0091FF"; + + ; +}; diff --git a/newexample/ButtonNormal.re b/newexample/ButtonNormal.re new file mode 100644 index 0000000..27572e0 --- /dev/null +++ b/newexample/ButtonNormal.re @@ -0,0 +1,53 @@ +let spaceConcat = (x1, x2) => + switch (x1, x2) { + | ("", x) + | (x, "") => x + | (x1, x2) => x1 ++ " " ++ x2 + }; + +let (+++) = spaceConcat; + +module Cn = { + let ifTrue = (cn, x) => x ? cn : ""; +}; + +module Css = { + let button = [%cx + {| + color: #fff; + border: none; + padding: 10px; + border-radius: 10px; + font-family: inherit; + font-size: inherit; + |} + ]; + + let buttonDisabled = [%cx {| + cursor: default; + opacity: 0.5; + |}]; + + let buttonColor = color => { + let color = `hex(color); + [%cx {| + background-color: $(color); + |}]; + }; +}; + +[@react.component] +let make = () => { + let disabled = false; + let color = "0091FF"; + + ; +}; diff --git a/newexample/CodeExample.re b/newexample/CodeExample.re new file mode 100644 index 0000000..0f8ec9b --- /dev/null +++ b/newexample/CodeExample.re @@ -0,0 +1,43 @@ +module Css = { + let code = [%cx + {| + white-space: pre; + padding: 0; + background-color: #f5f6f6; + |} + ]; +}; + +[@react.component] +let make = () => { + + {js|open Reshowcase.Entry; + +demo(({addDemo: _, addCategory}) => + addCategory("Typography", ({addDemo: _, addCategory}) => { + addCategory("Headings", ({addDemo, addCategory: _}) => { + addDemo("H1", ({string, int, _}) => { + let size = + int("Font size", {min: 0, max: 100, initial: 30, step: 1}); + +

+ {string("Text", "hello")->React.string} +

; + }); + addDemo("H2", ({string, _}) => +

{string("Text", "hello")->React.string}

+ ); + }); + addCategory("Text", ({addDemo, addCategory: _}) => { + addDemo("Paragraph", ({string, _}) => +

{string("Text", "hello")->React.string}

+ ); + addDemo("Italic", ({string, _}) => + {string("Text", "hello")->React.string} + ); + }); + }) +);|js} + ->React.string +
; +}; diff --git a/newexample/H1.re b/newexample/H1.re new file mode 100644 index 0000000..ddf0c52 --- /dev/null +++ b/newexample/H1.re @@ -0,0 +1,15 @@ +module Css = { + let h1Size = size => { + let fontSize = `px(size); + [%cx {| + font-size: $(fontSize); + |}]; + }; +}; + +[@react.component] +let make = () => { + let size = 30; + +

{React.string("hello")}

; +}; diff --git a/newexample/H2.re b/newexample/H2.re new file mode 100644 index 0000000..ee203f9 --- /dev/null +++ b/newexample/H2.re @@ -0,0 +1,4 @@ +[@react.component] +let make = () => { +

{React.string("hello")}

; +}; diff --git a/newexample/Italic.re b/newexample/Italic.re new file mode 100644 index 0000000..08d11ac --- /dev/null +++ b/newexample/Italic.re @@ -0,0 +1,4 @@ +[@react.component] +let make = () => { + {React.string("hello")} ; +}; diff --git a/newexample/NewDemo.re b/newexample/NewDemo.re new file mode 100644 index 0000000..26f29ac --- /dev/null +++ b/newexample/NewDemo.re @@ -0,0 +1,66 @@ +open Reshowcase.NewEntry; + +let demos: list(t) = [ + Category({ + name: "Buttons", + demos: [ + { + name: "Normal", + modulePath: "ButtonNormal", + }, + { + name: "Huge", + modulePath: "ButtonHuge", + }, + ], + }), + Category({ + name: "Headings", + demos: [ + { + name: "H1", + modulePath: "H1", + }, + { + name: "H2", + modulePath: "H2", + }, + ], + }), + Category({ + name: "Text", + demos: [ + { + name: "Paragraph", + modulePath: "Paragraph", + }, + { + name: "Italic", + modulePath: "Italic", + }, + ], + }), + Demo({ + name: "Code example", + modulePath: "CodeExample", + }), + Category({ + name: "Test search", + demos: [ + { + name: "OneTwoThreeFour", + modulePath: "OneTwoThreeFour", + }, + { + name: "OneTwoThreeFive", + modulePath: "OneTwoThreeFive", + }, + { + name: "OneTwoFourSeven", + modulePath: "OneTwoFourSeven", + }, + ], + }), +]; + +start(~demos); diff --git a/newexample/OneTwoFourSeven.re b/newexample/OneTwoFourSeven.re new file mode 100644 index 0000000..5ea2489 --- /dev/null +++ b/newexample/OneTwoFourSeven.re @@ -0,0 +1,2 @@ +[@react.component] +let make = () => React.null; diff --git a/newexample/OneTwoThreeFive.re b/newexample/OneTwoThreeFive.re new file mode 100644 index 0000000..5ea2489 --- /dev/null +++ b/newexample/OneTwoThreeFive.re @@ -0,0 +1,2 @@ +[@react.component] +let make = () => React.null; diff --git a/newexample/OneTwoThreeFour.re b/newexample/OneTwoThreeFour.re new file mode 100644 index 0000000..5ea2489 --- /dev/null +++ b/newexample/OneTwoThreeFour.re @@ -0,0 +1,2 @@ +[@react.component] +let make = () => React.null; diff --git a/newexample/Paragraph.re b/newexample/Paragraph.re new file mode 100644 index 0000000..38e9ecb --- /dev/null +++ b/newexample/Paragraph.re @@ -0,0 +1,4 @@ +[@react.component] +let make = () => { +

{React.string("hello")}

; +}; diff --git a/newexample/dune b/newexample/dune new file mode 100644 index 0000000..4ca41a6 --- /dev/null +++ b/newexample/dune @@ -0,0 +1,7 @@ +(melange.emit + (target newexample) + (alias newexample) + (libraries reshowcase reason-react styled-ppx.melange) + (module_systems es6) + (preprocess + (pps melange.ppx reason-react-ppx styled-ppx))) diff --git a/src/Foo.re b/src/Foo.re new file mode 100644 index 0000000..cf84315 --- /dev/null +++ b/src/Foo.re @@ -0,0 +1,6 @@ +[@react.component] +let make = () =>
"Hello"->React.string
; + +let filePath = Utils.getFilepath(); + +Js.log2("filePath", filePath); diff --git a/src/NewEntry.re b/src/NewEntry.re new file mode 100644 index 0000000..5cd2fdd --- /dev/null +++ b/src/NewEntry.re @@ -0,0 +1,15 @@ +type demo = { + name: string, + modulePath: string, +}; + +type category = { + name: string, + demos: list(demo), +}; + +type t = + | Demo(demo) + | Category(category); + +let start = (~demos: list(t)) => Js.log2("demos", demos); diff --git a/src/Process.re b/src/Process.re new file mode 100644 index 0000000..49b64ed --- /dev/null +++ b/src/Process.re @@ -0,0 +1,19 @@ +type process; + +external process: process = "process"; + +[@mel.send] external exit': (process, int) => 'a = "exit"; + +[@mel.get] external argv: process => array(string) = "argv"; + +let exit = int => process->exit'(int); + +let getArgs = () => process->argv; + +external env: Js.Dict.t(string) = "process.env"; + +[@mel.send] external on: (process, string, unit => unit) => unit = "on"; + +let onTerminate = callback => + [|"SIGINT", "SIGTERM"|] + ->Js.Array.forEach(~f=signal => process->on(signal, callback), _); diff --git a/src/ReshowcaseUi.re b/src/ReshowcaseUi.re index f4fb10d..fb6b18a 100644 --- a/src/ReshowcaseUi.re +++ b/src/ReshowcaseUi.re @@ -915,7 +915,8 @@ module DemoUnitFrame = { border: none; height: $(height); width: $(width); - |}]; + |} + ]; }; }; diff --git a/src/Utils.re b/src/Utils.re new file mode 100644 index 0000000..77feba3 --- /dev/null +++ b/src/Utils.re @@ -0,0 +1,92 @@ +type jsError; + +[@mel.new] external makeError: unit => jsError = "Error"; + +[@mel.get] external getStack: jsError => string = "stack"; + +external window: _ = "window"; + +// Commented to avoid error in webpack +// @module("path") external dirnameFromFilepath: string => string = "dirname" + +let dirnameFromFilepath = filepath => { + filepath + ->Js.String.split(~sep="/", _) + ->Js.Array.slice(~start=0, ~end_=-1, _) + ->Js.Array.join(~sep="/", _); +}; + +// Reusable functions that can be simply called from any module instead of +// dealing with import.meta.url etc. + +let getFilepathFromError = jsError => { + let lineWithPath = + jsError + ->getStack + ->Js.String.split(~sep="\n", _) + ->Js.Array.slice(~start=2, ~end_=3, _) + ->Belt.Array.get(0); + + switch (lineWithPath) { + | None => Js.Exn.raiseError("[getFilepathFromError] lineWithPath is None") + | Some(lineWithPath) => + lineWithPath + ->Js.String.trim + ->Js.String.replace(~search="at file://", ~replacement="", _) + ->Js.String.replaceByRe( + ~regexp=Js.Re.fromString(":[0-9]+:[0-9]+"), + ~replacement="", + _, + ) + }; +}; + +let getFilepath = () => + switch (Js.typeof(window) == "undefined") { + // Get filepath only in node + | false => "" + | true => makeError()->getFilepathFromError + }; + +let getDirname = () => makeError()->getFilepathFromError->dirnameFromFilepath; + +let getModuleNameFromModulePath = modulePath => { + let segments = modulePath->Js.String.split(~sep="/", _); + let filename = segments->Belt.Array.get(Belt.Array.length(segments) - 1); + switch (filename) { + | None + | Some("") => + Js.Console.error( + "[Utils.getModuleNameFromModulePath] Filename is empty or None, modulePath: " + ++ modulePath, + ); + Process.exit(1); + | Some(filename) => + let filenameSplit = filename->Js.String.split(~sep=".", _); + let moduleName = filenameSplit->Belt.Array.get(0); + switch (moduleName) { + | None => + Js.Console.error( + "[Utils.getModuleNameFromModulePath] moduleName is None, modulePath: " + ++ modulePath, + ); + Process.exit(1); + | Some(moduleName) => moduleName + }; + }; +}; + +let maybeAddSlashPrefix = path => + if (path->Js.String.startsWith(~prefix="http", _) + || path->Js.String.startsWith(~prefix="/", _)) { + path; + } else { + "/" ++ path; + }; + +let maybeAddSlashSuffix = path => + if (path->Js.String.endsWith(~suffix="/", _)) { + path; + } else { + path ++ "/"; + }; diff --git a/src/dune b/src/dune index 847557e..9a4a418 100644 --- a/src/dune +++ b/src/dune @@ -15,9 +15,9 @@ (modules) (module_systems es6)) -(melange.emit - (target node) - (alias node) - (libraries reshowcase) - (modules) - (module_systems (commonjs mjs))) +; (melange.emit +; (target node) +; (alias node) +; (libraries reshowcase) +; (modules) +; (module_systems (commonjs mjs))) diff --git a/tests/HighlightTermsTest.re b/tests/HighlightTermsTest.re index 0572843..8f7eb93 100644 --- a/tests/HighlightTermsTest.re +++ b/tests/HighlightTermsTest.re @@ -4,7 +4,14 @@ external process: 'a = "process"; [@mel.module] external util: 'a = "util"; let inspect = (value): string => - util##inspect(value, {"compact": false, "depth": 20, "colors": true}); + util##inspect( + value, + { + "compact": false, + "depth": 20, + "colors": true, + }, + ); let exitWithError = (): unit => process##exit(1); From 74f31cb4bca333e30dd36c6412f4e12d0dd9a6f4 Mon Sep 17 00:00:00 2001 From: Denis Strelkov Date: Mon, 13 Oct 2025 13:37:49 +0800 Subject: [PATCH 03/48] Add NewEntity module --- newexample/NewDemo.re | 5 ++--- src/NewEntity.re | 13 +++++++++++++ src/NewEntry.re | 16 +--------------- 3 files changed, 16 insertions(+), 18 deletions(-) create mode 100644 src/NewEntity.re diff --git a/newexample/NewDemo.re b/newexample/NewDemo.re index 26f29ac..e097b56 100644 --- a/newexample/NewDemo.re +++ b/newexample/NewDemo.re @@ -1,6 +1,5 @@ -open Reshowcase.NewEntry; -let demos: list(t) = [ +let demos: list(Reshowcase.NewEntity.t) = [ Category({ name: "Buttons", demos: [ @@ -63,4 +62,4 @@ let demos: list(t) = [ }), ]; -start(~demos); +Reshowcase.NewEntry.start(~demos); diff --git a/src/NewEntity.re b/src/NewEntity.re new file mode 100644 index 0000000..58d43cf --- /dev/null +++ b/src/NewEntity.re @@ -0,0 +1,13 @@ +type demo = { + name: string, + modulePath: string, +}; + +type category = { + name: string, + demos: list(demo), +}; + +type t = + | Demo(demo) + | Category(category); diff --git a/src/NewEntry.re b/src/NewEntry.re index 5cd2fdd..f64a1a6 100644 --- a/src/NewEntry.re +++ b/src/NewEntry.re @@ -1,15 +1 @@ -type demo = { - name: string, - modulePath: string, -}; - -type category = { - name: string, - demos: list(demo), -}; - -type t = - | Demo(demo) - | Category(category); - -let start = (~demos: list(t)) => Js.log2("demos", demos); +let start = (~demos: list(NewEntity.t)) => Js.log2("demos", demos); From 3c9b72afecfbdceeb8117269b9d3bf643bcfb44a Mon Sep 17 00:00:00 2001 From: Denis Strelkov Date: Mon, 13 Oct 2025 13:43:20 +0800 Subject: [PATCH 04/48] Start using actual module paths --- newexample/ButtonHuge.re | 2 ++ newexample/ButtonNormal.re | 2 ++ newexample/CodeExample.re | 2 ++ newexample/H1.re | 2 ++ newexample/H2.re | 2 ++ newexample/Italic.re | 2 ++ newexample/NewDemo.re | 20 ++++++++++---------- newexample/OneTwoFourSeven.re | 2 ++ newexample/OneTwoThreeFive.re | 2 ++ newexample/OneTwoThreeFour.re | 2 ++ newexample/Paragraph.re | 2 ++ 11 files changed, 30 insertions(+), 10 deletions(-) diff --git a/newexample/ButtonHuge.re b/newexample/ButtonHuge.re index b55d6b3..7430c38 100644 --- a/newexample/ButtonHuge.re +++ b/newexample/ButtonHuge.re @@ -1,3 +1,5 @@ +let modulePath = Reshowcase.Utils.getFilepath(); + let spaceConcat = (x1, x2) => switch (x1, x2) { | ("", x) diff --git a/newexample/ButtonNormal.re b/newexample/ButtonNormal.re index 27572e0..f159f74 100644 --- a/newexample/ButtonNormal.re +++ b/newexample/ButtonNormal.re @@ -1,3 +1,5 @@ +let modulePath = Reshowcase.Utils.getFilepath(); + let spaceConcat = (x1, x2) => switch (x1, x2) { | ("", x) diff --git a/newexample/CodeExample.re b/newexample/CodeExample.re index 0f8ec9b..1d7fb8e 100644 --- a/newexample/CodeExample.re +++ b/newexample/CodeExample.re @@ -1,3 +1,5 @@ +let modulePath = Reshowcase.Utils.getFilepath(); + module Css = { let code = [%cx {| diff --git a/newexample/H1.re b/newexample/H1.re index ddf0c52..54a078f 100644 --- a/newexample/H1.re +++ b/newexample/H1.re @@ -1,3 +1,5 @@ +let modulePath = Reshowcase.Utils.getFilepath(); + module Css = { let h1Size = size => { let fontSize = `px(size); diff --git a/newexample/H2.re b/newexample/H2.re index ee203f9..bbd8b23 100644 --- a/newexample/H2.re +++ b/newexample/H2.re @@ -1,3 +1,5 @@ +let modulePath = Reshowcase.Utils.getFilepath(); + [@react.component] let make = () => {

{React.string("hello")}

; diff --git a/newexample/Italic.re b/newexample/Italic.re index 08d11ac..b578ef4 100644 --- a/newexample/Italic.re +++ b/newexample/Italic.re @@ -1,3 +1,5 @@ +let modulePath = Reshowcase.Utils.getFilepath(); + [@react.component] let make = () => { {React.string("hello")} ; diff --git a/newexample/NewDemo.re b/newexample/NewDemo.re index e097b56..18526a1 100644 --- a/newexample/NewDemo.re +++ b/newexample/NewDemo.re @@ -5,11 +5,11 @@ let demos: list(Reshowcase.NewEntity.t) = [ demos: [ { name: "Normal", - modulePath: "ButtonNormal", + modulePath: ButtonNormal.modulePath, }, { name: "Huge", - modulePath: "ButtonHuge", + modulePath: ButtonHuge.modulePath, }, ], }), @@ -18,11 +18,11 @@ let demos: list(Reshowcase.NewEntity.t) = [ demos: [ { name: "H1", - modulePath: "H1", + modulePath: H1.modulePath, }, { name: "H2", - modulePath: "H2", + modulePath: H2.modulePath, }, ], }), @@ -31,32 +31,32 @@ let demos: list(Reshowcase.NewEntity.t) = [ demos: [ { name: "Paragraph", - modulePath: "Paragraph", + modulePath: Paragraph.modulePath, }, { name: "Italic", - modulePath: "Italic", + modulePath: Italic.modulePath, }, ], }), Demo({ name: "Code example", - modulePath: "CodeExample", + modulePath: CodeExample.modulePath, }), Category({ name: "Test search", demos: [ { name: "OneTwoThreeFour", - modulePath: "OneTwoThreeFour", + modulePath: OneTwoThreeFour.modulePath, }, { name: "OneTwoThreeFive", - modulePath: "OneTwoThreeFive", + modulePath: OneTwoThreeFive.modulePath, }, { name: "OneTwoFourSeven", - modulePath: "OneTwoFourSeven", + modulePath: OneTwoFourSeven.modulePath, }, ], }), diff --git a/newexample/OneTwoFourSeven.re b/newexample/OneTwoFourSeven.re index 5ea2489..ee24d03 100644 --- a/newexample/OneTwoFourSeven.re +++ b/newexample/OneTwoFourSeven.re @@ -1,2 +1,4 @@ +let modulePath = Reshowcase.Utils.getFilepath(); + [@react.component] let make = () => React.null; diff --git a/newexample/OneTwoThreeFive.re b/newexample/OneTwoThreeFive.re index 5ea2489..ee24d03 100644 --- a/newexample/OneTwoThreeFive.re +++ b/newexample/OneTwoThreeFive.re @@ -1,2 +1,4 @@ +let modulePath = Reshowcase.Utils.getFilepath(); + [@react.component] let make = () => React.null; diff --git a/newexample/OneTwoThreeFour.re b/newexample/OneTwoThreeFour.re index 5ea2489..ee24d03 100644 --- a/newexample/OneTwoThreeFour.re +++ b/newexample/OneTwoThreeFour.re @@ -1,2 +1,4 @@ +let modulePath = Reshowcase.Utils.getFilepath(); + [@react.component] let make = () => React.null; diff --git a/newexample/Paragraph.re b/newexample/Paragraph.re index 38e9ecb..6ca0b08 100644 --- a/newexample/Paragraph.re +++ b/newexample/Paragraph.re @@ -1,3 +1,5 @@ +let modulePath = Reshowcase.Utils.getFilepath(); + [@react.component] let make = () => {

{React.string("hello")}

; From eb80369a43d6a63edaee2b2b40a21546579306a9 Mon Sep 17 00:00:00 2001 From: Denis Strelkov Date: Mon, 13 Oct 2025 13:46:33 +0800 Subject: [PATCH 05/48] Move demo names --- newexample/ButtonHuge.re | 1 + newexample/ButtonNormal.re | 1 + newexample/CodeExample.re | 1 + newexample/H1.re | 1 + newexample/H2.re | 1 + newexample/Italic.re | 1 + newexample/NewDemo.re | 21 ++++++++++----------- newexample/OneTwoFourSeven.re | 1 + newexample/OneTwoThreeFive.re | 1 + newexample/OneTwoThreeFour.re | 1 + newexample/Paragraph.re | 1 + 11 files changed, 20 insertions(+), 11 deletions(-) diff --git a/newexample/ButtonHuge.re b/newexample/ButtonHuge.re index 7430c38..7db6efe 100644 --- a/newexample/ButtonHuge.re +++ b/newexample/ButtonHuge.re @@ -1,4 +1,5 @@ let modulePath = Reshowcase.Utils.getFilepath(); +let demoName = "Huge"; let spaceConcat = (x1, x2) => switch (x1, x2) { diff --git a/newexample/ButtonNormal.re b/newexample/ButtonNormal.re index f159f74..911e33c 100644 --- a/newexample/ButtonNormal.re +++ b/newexample/ButtonNormal.re @@ -1,4 +1,5 @@ let modulePath = Reshowcase.Utils.getFilepath(); +let demoName = "Normal"; let spaceConcat = (x1, x2) => switch (x1, x2) { diff --git a/newexample/CodeExample.re b/newexample/CodeExample.re index 1d7fb8e..7d64e23 100644 --- a/newexample/CodeExample.re +++ b/newexample/CodeExample.re @@ -1,4 +1,5 @@ let modulePath = Reshowcase.Utils.getFilepath(); +let demoName = "Code example"; module Css = { let code = [%cx diff --git a/newexample/H1.re b/newexample/H1.re index 54a078f..a99cfb2 100644 --- a/newexample/H1.re +++ b/newexample/H1.re @@ -1,4 +1,5 @@ let modulePath = Reshowcase.Utils.getFilepath(); +let demoName = "H1"; module Css = { let h1Size = size => { diff --git a/newexample/H2.re b/newexample/H2.re index bbd8b23..00bc7de 100644 --- a/newexample/H2.re +++ b/newexample/H2.re @@ -1,4 +1,5 @@ let modulePath = Reshowcase.Utils.getFilepath(); +let demoName = "H2"; [@react.component] let make = () => { diff --git a/newexample/Italic.re b/newexample/Italic.re index b578ef4..d51937c 100644 --- a/newexample/Italic.re +++ b/newexample/Italic.re @@ -1,4 +1,5 @@ let modulePath = Reshowcase.Utils.getFilepath(); +let demoName = "Italic"; [@react.component] let make = () => { diff --git a/newexample/NewDemo.re b/newexample/NewDemo.re index 18526a1..b1baaee 100644 --- a/newexample/NewDemo.re +++ b/newexample/NewDemo.re @@ -1,14 +1,13 @@ - let demos: list(Reshowcase.NewEntity.t) = [ Category({ name: "Buttons", demos: [ { - name: "Normal", + name: ButtonNormal.demoName, modulePath: ButtonNormal.modulePath, }, { - name: "Huge", + name: ButtonHuge.demoName, modulePath: ButtonHuge.modulePath, }, ], @@ -17,11 +16,11 @@ let demos: list(Reshowcase.NewEntity.t) = [ name: "Headings", demos: [ { - name: "H1", + name: H1.demoName, modulePath: H1.modulePath, }, { - name: "H2", + name: H2.demoName, modulePath: H2.modulePath, }, ], @@ -30,32 +29,32 @@ let demos: list(Reshowcase.NewEntity.t) = [ name: "Text", demos: [ { - name: "Paragraph", + name: Paragraph.demoName, modulePath: Paragraph.modulePath, }, { - name: "Italic", + name: Italic.demoName, modulePath: Italic.modulePath, }, ], }), Demo({ - name: "Code example", + name: CodeExample.demoName, modulePath: CodeExample.modulePath, }), Category({ name: "Test search", demos: [ { - name: "OneTwoThreeFour", + name: OneTwoThreeFour.demoName, modulePath: OneTwoThreeFour.modulePath, }, { - name: "OneTwoThreeFive", + name: OneTwoThreeFive.demoName, modulePath: OneTwoThreeFive.modulePath, }, { - name: "OneTwoFourSeven", + name: OneTwoFourSeven.demoName, modulePath: OneTwoFourSeven.modulePath, }, ], diff --git a/newexample/OneTwoFourSeven.re b/newexample/OneTwoFourSeven.re index ee24d03..c474bef 100644 --- a/newexample/OneTwoFourSeven.re +++ b/newexample/OneTwoFourSeven.re @@ -1,4 +1,5 @@ let modulePath = Reshowcase.Utils.getFilepath(); +let demoName = "OneTwoFourSeven"; [@react.component] let make = () => React.null; diff --git a/newexample/OneTwoThreeFive.re b/newexample/OneTwoThreeFive.re index ee24d03..bbc3523 100644 --- a/newexample/OneTwoThreeFive.re +++ b/newexample/OneTwoThreeFive.re @@ -1,4 +1,5 @@ let modulePath = Reshowcase.Utils.getFilepath(); +let demoName = "OneTwoThreeFive"; [@react.component] let make = () => React.null; diff --git a/newexample/OneTwoThreeFour.re b/newexample/OneTwoThreeFour.re index ee24d03..0c856c1 100644 --- a/newexample/OneTwoThreeFour.re +++ b/newexample/OneTwoThreeFour.re @@ -1,4 +1,5 @@ let modulePath = Reshowcase.Utils.getFilepath(); +let demoName = "OneTwoThreeFour"; [@react.component] let make = () => React.null; diff --git a/newexample/Paragraph.re b/newexample/Paragraph.re index 6ca0b08..fc386ea 100644 --- a/newexample/Paragraph.re +++ b/newexample/Paragraph.re @@ -1,4 +1,5 @@ let modulePath = Reshowcase.Utils.getFilepath(); +let demoName = "Paragraph"; [@react.component] let make = () => { From 2fe8a732aefff950b7844292cc7e3725c4c1f3c3 Mon Sep 17 00:00:00 2001 From: Denis Strelkov Date: Mon, 13 Oct 2025 13:55:03 +0800 Subject: [PATCH 06/48] Switch to array --- Makefile | 3 +++ newexample/NewDemo.re | 20 ++++++++++---------- src/NewEntity.re | 2 +- src/NewEntry.re | 5 ++++- src/Util.re | 10 ++++++++++ 5 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 src/Util.re diff --git a/Makefile b/Makefile index 764812c..a356d4d 100644 --- a/Makefile +++ b/Makefile @@ -70,3 +70,6 @@ build-example-ok: .PHONY: build-example build-example: ## Builds the example $(DUNE) build @build-example + +run: + node ./_build/default/newexample/newexample/newexample/NewDemo.js diff --git a/newexample/NewDemo.re b/newexample/NewDemo.re index b1baaee..cf6e430 100644 --- a/newexample/NewDemo.re +++ b/newexample/NewDemo.re @@ -1,7 +1,7 @@ -let demos: list(Reshowcase.NewEntity.t) = [ +let demos: array(Reshowcase.NewEntity.t) = [| Category({ name: "Buttons", - demos: [ + demos: [| { name: ButtonNormal.demoName, modulePath: ButtonNormal.modulePath, @@ -10,11 +10,11 @@ let demos: list(Reshowcase.NewEntity.t) = [ name: ButtonHuge.demoName, modulePath: ButtonHuge.modulePath, }, - ], + |], }), Category({ name: "Headings", - demos: [ + demos: [| { name: H1.demoName, modulePath: H1.modulePath, @@ -23,11 +23,11 @@ let demos: list(Reshowcase.NewEntity.t) = [ name: H2.demoName, modulePath: H2.modulePath, }, - ], + |], }), Category({ name: "Text", - demos: [ + demos: [| { name: Paragraph.demoName, modulePath: Paragraph.modulePath, @@ -36,7 +36,7 @@ let demos: list(Reshowcase.NewEntity.t) = [ name: Italic.demoName, modulePath: Italic.modulePath, }, - ], + |], }), Demo({ name: CodeExample.demoName, @@ -44,7 +44,7 @@ let demos: list(Reshowcase.NewEntity.t) = [ }), Category({ name: "Test search", - demos: [ + demos: [| { name: OneTwoThreeFour.demoName, modulePath: OneTwoThreeFour.modulePath, @@ -57,8 +57,8 @@ let demos: list(Reshowcase.NewEntity.t) = [ name: OneTwoFourSeven.demoName, modulePath: OneTwoFourSeven.modulePath, }, - ], + |], }), -]; +|]; Reshowcase.NewEntry.start(~demos); diff --git a/src/NewEntity.re b/src/NewEntity.re index 58d43cf..e62c5aa 100644 --- a/src/NewEntity.re +++ b/src/NewEntity.re @@ -5,7 +5,7 @@ type demo = { type category = { name: string, - demos: list(demo), + demos: array(demo), }; type t = diff --git a/src/NewEntry.re b/src/NewEntry.re index f64a1a6..3933010 100644 --- a/src/NewEntry.re +++ b/src/NewEntry.re @@ -1 +1,4 @@ -let start = (~demos: list(NewEntity.t)) => Js.log2("demos", demos); +let start = (~demos: array(NewEntity.t)) => { + let str = Util.inspect(demos); + Js.log2("!!! demos:\n", str); +}; diff --git a/src/Util.re b/src/Util.re new file mode 100644 index 0000000..6b149d6 --- /dev/null +++ b/src/Util.re @@ -0,0 +1,10 @@ +// https://nodejs.org/api/util.html#utilinspectobject-options + +type options = { + depth: int, + colors: bool, +}; + +[@mel.module "node:util"] external inspect: ('a, options) => string = "inspect"; + +let inspect = value => inspect(value, {depth: 5, colors: true}); From de1d823324c9d3ef7ef4d351a3ffeabe9b467e01 Mon Sep 17 00:00:00 2001 From: Denis Strelkov Date: Mon, 13 Oct 2025 13:56:35 +0800 Subject: [PATCH 07/48] Move new code --- src/dune | 2 ++ src/{ => new}/NewEntity.re | 0 src/{ => new}/NewEntry.re | 0 3 files changed, 2 insertions(+) rename src/{ => new}/NewEntity.re (100%) rename src/{ => new}/NewEntry.re (100%) diff --git a/src/dune b/src/dune index 9a4a418..4260828 100644 --- a/src/dune +++ b/src/dune @@ -1,3 +1,5 @@ +(include_subdirs unqualified) + (library (name reshowcase) (preprocess diff --git a/src/NewEntity.re b/src/new/NewEntity.re similarity index 100% rename from src/NewEntity.re rename to src/new/NewEntity.re diff --git a/src/NewEntry.re b/src/new/NewEntry.re similarity index 100% rename from src/NewEntry.re rename to src/new/NewEntry.re From fd0eed17a6747f4324ae5daf91a6d382b7245642 Mon Sep 17 00:00:00 2001 From: Denis Strelkov Date: Mon, 13 Oct 2025 14:28:12 +0800 Subject: [PATCH 08/48] Recursive type --- newexample/NewDemo.re | 48 +++++++++++++++++++++---------------------- src/Util.re | 12 +++++++++-- src/new/NewEntity.re | 7 +++---- src/new/NewEntry.re | 4 ++-- 4 files changed, 39 insertions(+), 32 deletions(-) diff --git a/newexample/NewDemo.re b/newexample/NewDemo.re index cf6e430..7c00e38 100644 --- a/newexample/NewDemo.re +++ b/newexample/NewDemo.re @@ -1,41 +1,41 @@ -let demos: array(Reshowcase.NewEntity.t) = [| +let items: array(Reshowcase.NewEntity.item) = [| Category({ name: "Buttons", - demos: [| - { + items: [| + Demo({ name: ButtonNormal.demoName, modulePath: ButtonNormal.modulePath, - }, - { + }), + Demo({ name: ButtonHuge.demoName, modulePath: ButtonHuge.modulePath, - }, + }), |], }), Category({ name: "Headings", - demos: [| - { + items: [| + Demo({ name: H1.demoName, modulePath: H1.modulePath, - }, - { + }), + Demo({ name: H2.demoName, modulePath: H2.modulePath, - }, + }), |], }), Category({ name: "Text", - demos: [| - { + items: [| + Demo({ name: Paragraph.demoName, modulePath: Paragraph.modulePath, - }, - { + }), + Demo({ name: Italic.demoName, modulePath: Italic.modulePath, - }, + }), |], }), Demo({ @@ -44,21 +44,21 @@ let demos: array(Reshowcase.NewEntity.t) = [| }), Category({ name: "Test search", - demos: [| - { + items: [| + Demo({ name: OneTwoThreeFour.demoName, modulePath: OneTwoThreeFour.modulePath, - }, - { + }), + Demo({ name: OneTwoThreeFive.demoName, modulePath: OneTwoThreeFive.modulePath, - }, - { + }), + Demo({ name: OneTwoFourSeven.demoName, modulePath: OneTwoFourSeven.modulePath, - }, + }), |], }), |]; -Reshowcase.NewEntry.start(~demos); +Reshowcase.NewEntry.start(~items); diff --git a/src/Util.re b/src/Util.re index 6b149d6..dfde816 100644 --- a/src/Util.re +++ b/src/Util.re @@ -5,6 +5,14 @@ type options = { colors: bool, }; -[@mel.module "node:util"] external inspect: ('a, options) => string = "inspect"; +[@mel.module "node:util"] +external inspect: ('a, options) => string = "inspect"; -let inspect = value => inspect(value, {depth: 5, colors: true}); +let inspect = value => + inspect( + value, + { + depth: 5, + colors: true, + }, + ); diff --git a/src/new/NewEntity.re b/src/new/NewEntity.re index e62c5aa..21538fc 100644 --- a/src/new/NewEntity.re +++ b/src/new/NewEntity.re @@ -5,9 +5,8 @@ type demo = { type category = { name: string, - demos: array(demo), -}; - -type t = + items: array(item), +} +and item = | Demo(demo) | Category(category); diff --git a/src/new/NewEntry.re b/src/new/NewEntry.re index 3933010..308414a 100644 --- a/src/new/NewEntry.re +++ b/src/new/NewEntry.re @@ -1,4 +1,4 @@ -let start = (~demos: array(NewEntity.t)) => { - let str = Util.inspect(demos); +let start = (~items: array(NewEntity.item)) => { + let str = Util.inspect(items); Js.log2("!!! demos:\n", str); }; From 212725bd55462895ca570a24636d5f22a42b6378 Mon Sep 17 00:00:00 2001 From: Denis Strelkov Date: Mon, 13 Oct 2025 14:54:46 +0800 Subject: [PATCH 09/48] Construct filepaths --- src/Util.re | 2 +- src/new/Fs.re | 3 + src/new/NewEntry.re | 73 ++- src/new/NewExample.re | 4 + src/new/NewReshowcaseUi.re | 1153 ++++++++++++++++++++++++++++++++++++ src/new/NewTemplate.re | 6 + 6 files changed, 1238 insertions(+), 3 deletions(-) create mode 100644 src/new/Fs.re create mode 100644 src/new/NewExample.re create mode 100644 src/new/NewReshowcaseUi.re create mode 100644 src/new/NewTemplate.re diff --git a/src/Util.re b/src/Util.re index dfde816..93b8e2d 100644 --- a/src/Util.re +++ b/src/Util.re @@ -12,7 +12,7 @@ let inspect = value => inspect( value, { - depth: 5, + depth: 20, colors: true, }, ); diff --git a/src/new/Fs.re b/src/new/Fs.re new file mode 100644 index 0000000..8922c43 --- /dev/null +++ b/src/new/Fs.re @@ -0,0 +1,3 @@ +[@mel.module "node:fs"] +external writeFileSync: (~path: string, ~data: string) => unit = + "writeFileSync"; diff --git a/src/new/NewEntry.re b/src/new/NewEntry.re index 308414a..d156f91 100644 --- a/src/new/NewEntry.re +++ b/src/new/NewEntry.re @@ -1,4 +1,73 @@ +let makeDemoTemplate = (~filepath: string) => {j| +import * as Demo from "$(filepath)"; +import * as Client from "react-dom/client"; +import * as JsxRuntime from "react/jsx-runtime"; + +const root = document.querySelector("#root"); + +if (!(root == null)) { + const root1 = Client.createRoot(root); + root1.render(JsxRuntime.jsx(Demo.make, {})); +} +|j}; + +type extractedDemo = { + filepath: string, + targetPath: list(string), +}; + +let targetPathToFilepath = targetPath => { + let path = + targetPath + ->List.rev + ->Belt.List.map(Js.String.toLowerCase) + ->Belt.List.toArray + ->Js.Array.join(~sep="/", _); + + path ++ ".js"; +}; + +let extractDemos = (~items: array(NewEntity.item)): list(extractedDemo) => { + let rec extractWithPath = + (~path: list(string), ~items: array(NewEntity.item)) + : list(extractedDemo) => { + Js.Array.reduce( + ~f= + (acc, item) => { + switch (item) { + | NewEntity.Demo(demo) => + let targetPath = [demo.name, ...path]; + let extracted = { + filepath: demo.modulePath, + targetPath, + }; + [extracted, ...acc]; + | NewEntity.Category(category) => + let nestedDemos = + extractWithPath( + ~path=[category.name, ...path], + ~items=category.items, + ); + List.append(nestedDemos, acc); + } + }, + ~init=[], + items, + ); + }; + + extractWithPath(~path=[], ~items); +}; + let start = (~items: array(NewEntity.item)) => { - let str = Util.inspect(items); - Js.log2("!!! demos:\n", str); + let demos = extractDemos(~items); + let finalFilepaths = + demos->Belt.List.map(extractedDemo => + targetPathToFilepath(extractedDemo.targetPath) + ); + Js.log2("!!! demos:\n", Util.inspect(demos)); + Js.log2( + "!!! finalFilepaths:\n", + Util.inspect(finalFilepaths->Belt.List.toArray), + ); }; diff --git a/src/new/NewExample.re b/src/new/NewExample.re new file mode 100644 index 0000000..89ef7fb --- /dev/null +++ b/src/new/NewExample.re @@ -0,0 +1,4 @@ +[@react.component] +let make = () => { +
{React.string("Hello, world!")}
; +}; diff --git a/src/new/NewReshowcaseUi.re b/src/new/NewReshowcaseUi.re new file mode 100644 index 0000000..fb6b18a --- /dev/null +++ b/src/new/NewReshowcaseUi.re @@ -0,0 +1,1153 @@ +open Belt; +open Prelude; +open Layout; +module URLSearchParams = Bindings.URLSearchParams; +module Window = Bindings.Window; +module LocalStorage = Bindings.LocalStorage; + +type responsiveMode = + | Mobile + | Desktop; + +module TopPanel = { + module Css = { + open StyleVars; + + let panel = [%cx + {| + display: flex; + justify-content: flex-end; + border-bottom: 1px solid $(Color.midGray); + |} + ]; + + let buttonGroup = [%cx + {| + overflow: hidden; + display: flex; + flex-direction: row; + align-items: stretch; + border-radius: $(BorderRadius.default); + |} + ]; + + let button = [%cx + {| + height: 32px; + width: 48px; + cursor: pointer; + font-size: $(FontSize.sm); + background-color: $(Color.lightGray); + color: $(Color.darkGray); + border: none; + margin: 0; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + |} + ]; + + let buttonSquare = [%cx {| + width: 32px; + |}]; + + let buttonActive = [%cx + {| + background-color: $(Color.blue); + color: $(Color.white); + |} + ]; + + let middleSection = [%cx + {| + display: flex; + flex: 1; + justify-content: center; + |} + ]; + + let rightSection = [%cx {| + display: flex; + |}]; + + let sidebarIcon = [%cx + {| + transition: 200ms ease-in-out transform; + |} + ]; + + let sidebarIconActive = [%cx {| + transform: rotate(180deg) + |}]; + }; + + [@react.component] + let make = + ( + ~isSidebarHidden: bool, + ~responsiveMode: responsiveMode, + ~onRightSidebarToggle: unit => unit, + ~onSetResponsiveMode: (responsiveMode => responsiveMode) => unit, + ) => +
+
+
+ +
+ + +
+
+
+
+ +
+ +
+
+
+
; +}; + +let rightSidebarId = "rightSidebar"; + +module SidebarLink = { + module Css = { + open StyleVars; + + let link = [%cx + {| + text-decoration: none; + color: $(Color.blue); + display: block; + padding: $(Gap.xs) $(Gap.md); + border-radius: $(BorderRadius.default); + font-size: $(FontSize.md); + font-weight: 500; + |} + ]; + + let linkActive = [%cx {| + background-color: $(Color.midGray); + |}]; + }; + + [@react.component] + let make = (~activeDomRef=?, ~href, ~text: React.element) => { + let url = ReasonReactRouter.useUrl(); + let path = String.concat("/", url.path); + let isActive = + Js.String.endsWith(~suffix=href, path ++ "?" ++ url.search); + + Cn.ifTrue(isActive)} + onClick={event => + switch ( + React.Event.Mouse.metaKey(event), + React.Event.Mouse.ctrlKey(event), + ) { + | (false, false) => + React.Event.Mouse.preventDefault(event); + ReasonReactRouter.push(href); + | _ => () + } + }> + text + ; + }; +}; + +module DemoListSidebar = { + module Css = { + open StyleVars; + + let categoryName = [%cx + {| + padding: $(Gap.xs) $(Gap.xxs); + font-size: $(FontSize.md); + font-weight: 500; + |} + ]; + + let sidebarPanelWrapper = [%cx + {| + position: sticky; + top: 0; + background-color: $(Color.lightGray); + |} + ]; + + let sidebarPanel = [%cx + {| + display: flex; + align-items: center; + gap: $(Gap.xs); + |} + ]; + + let collapseButton = [%cx + {| + height: 32px; + min-width: 32px; + width: 32px; + cursor: pointer; + font-size: $(FontSize.sm); + background-color: $(Color.white); + color: $(Color.darkGray); + border: 1px solid $(Color.midGray); + border-radius: $(BorderRadius.default); + margin: 0; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + |} + ]; + }; + + module SearchInput = { + module Css = { + open StyleVars; + + let inputWrapper = [%cx + {| + position: relative; + display: flex; + align-items: center; + background-color: $(Color.midGray); + border-radius: $(BorderRadius.default); + |} + ]; + + let input = [%cx + {| + padding: $(Gap.xs) $(Gap.md); + width: 100%; + margin: 0; + height: 32px; + box-sizing: border-box; + font-family: inherit; + font-size: $(FontSize.md); + border: none; + background-color: transparent; + border-radius: $(BorderRadius.default); + |} + ]; + + let clearButton = [%cx + {| + position: absolute; + right: 7px; + display: flex; + cursor: pointer; + border: none; + padding: 0; + margin: 0; + background-color: transparent; + top: 50%; + transform: translateY(-50%); + |} + ]; + }; + + [@react.component] + let make = (~autoFocus=?, ~value, ~onChange, ~onClear) => +
+ + {value == "" + ? React.null + : } +
; + }; + + let renderMenu = + ( + ~isCategoriesCollapsedByDefault: bool, + ~urlSearchParams: URLSearchParams.t, + ~searchString, + demos: Demos.t, + ) => { + let activeElementRef = UseScrollIntoView.use(); + + let rec renderMenu = + ( + ~parentCategoryMatchedSearch: bool, + ~nestingLevel, + ~categoryQuery, + demos: Demos.t, + ) => { + let demos = demos->Js.Dict.entries; + demos + ->Array.map(((entityName, entity)) => { + let searchMatchingTerms = + HighlightTerms.getMatchingTerms(~searchString, ~entityName); + + let isEntityNameMatchSearch = + searchString == "" || searchMatchingTerms->Belt.Array.size > 0; + + switch (entity) { + | Entity.Demo(_) => + if (isEntityNameMatchSearch || parentCategoryMatchedSearch) { + + + {renderMenu( + ~parentCategoryMatchedSearch= + isEntityNameMatchSearch || parentCategoryMatchedSearch, + ~nestingLevel=nestingLevel + 1, + ~categoryQuery= + ( + (({js|&category|js} ++ levelStr) ++ {js|=|js}) + ++ entityName->Js.Global.encodeURIComponent + ) + ++ categoryQuery, + demos, + )} + + + ; + } else { + React.null; + } + }; + }) + ->React.array; + }; + + renderMenu( + ~parentCategoryMatchedSearch=false, + ~nestingLevel=0, + ~categoryQuery="", + demos: Demos.t, + ); + }; + + [@react.component] + let make = + ( + ~urlSearchParams: URLSearchParams.t, + ~demos: Demos.t, + ~isCategoriesCollapsedByDefault: bool, + ~onToggleCollapsedCategoriesByDefault: unit => unit, + ) => { + let (filterValue, setFilterValue) = React.useState(() => None); + +
+ +
+ + Option.getWithDefault("")} + onChange={event => { + let value = event->React.Event.Form.target##value; + + setFilterValue(_ => + if (value->Js.String.trim == "") { + None; + } else { + Some(value); + } + ); + }} + onClear={() => setFilterValue(_ => None)} + /> +
+
+
+ + {renderMenu( + ~isCategoriesCollapsedByDefault, + ~searchString= + filterValue->Option.mapWithDefault("", Js.String.toLowerCase), + ~urlSearchParams, + demos, + )} + +
; + }; +}; + +module DemoUnitSidebar = { + module Css = { + open StyleVars; + + let label = [%cx + {| + display: block; + background-color: $(Color.white); + border-radius: $(BorderRadius.default); + box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.07); + |} + ]; + + let labelText = [%cx + {| + font-size: $(FontSize.md); + text-align: center; + |} + ]; + + let textInput = [%cx + {| + font-size: $(FontSize.md); + width: 100%; + box-sizing: border-box; + background-color: $(Color.lightGray); + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); + border: none; + padding: $(Gap.md); + border-radius: $(BorderRadius.default); + |} + ]; + + let select = [%cx + {| + font-size: $(FontSize.md); + width: 100%; + box-sizing: border-box; + background-color: $(Color.lightGray); + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); + border: none; + padding: $(Gap.md); + border-radius: $(BorderRadius.default); + appearance: none; + -webkit-appearance: none; + padding-right: 30px; + background-image: + url("data:image/svg+xml,%3Csvg width='36' height='36' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='%2342484E' stroke-width='2' d='M12.246 14.847l5.826 5.826 5.827-5.826' fill='none' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-position: center right; + background-size: contain; + background-repeat: no-repeat; + |} + ]; + + let checkbox = [%cx + {| + font-size: $(FontSize.md); + margin: 0 auto; + display: block; + |} + ]; + }; + + module PropBox = { + [@react.component] + let make = (~propName: string, ~children) => + ; + }; + + [@react.component] + let make = + ( + ~strings: + Map.String.t( + ( + Configs.stringConfig, + string, + option(array((string, string))), + ), + ), + ~ints: Map.String.t((Configs.numberConfig(int), int)), + ~floats: Map.String.t((Configs.numberConfig(float), float)), + ~bools: Map.String.t((Configs.boolConfig, bool)), + ~onStringChange, + ~onIntChange, + ~onFloatChange, + ~onBoolChange, + ) => + + + {strings + ->Map.String.toArray + ->Array.map(((propName, (_config, value, options))) => + + {switch (options) { + | None => + + onStringChange( + propName, + event->React.Event.Form.target##value, + ) + } + /> + | Some(options) => + + }} + + ) + ->React.array} + {ints + ->Map.String.toArray + ->Array.map(((propName, ({Configs.min, max, _}, value))) => + + + onIntChange( + propName, + event->React.Event.Form.target##value->int_of_string, + ) + } + /> + + ) + ->React.array} + {floats + ->Map.String.toArray + ->Array.map(((propName, ({Configs.min, max, _}, value))) => + + + onFloatChange( + propName, + event->React.Event.Form.target##value->float_of_string, + ) + } + /> + + ) + ->React.array} + {bools + ->Map.String.toArray + ->Array.map(((propName, (_config, checked))) => + + + onBoolChange( + propName, + event->React.Event.Form.target##checked, + ) + } + /> + + ) + ->React.array} + + ; +}; + +module DemoUnit = { + module Css = { + let container = [%cx + {| + flex-grow: 1; + display: flex; + align-items: stretch; + flex-direction: row; + |} + ]; + + let contents = [%cx + {| + flex-grow: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + -webkit-overflow-scrolling: touch; + |} + ]; + }; + + type state = { + strings: + Map.String.t( + (Configs.stringConfig, string, option(array((string, string)))), + ), + ints: Map.String.t((Configs.numberConfig(int), int)), + floats: Map.String.t((Configs.numberConfig(float), float)), + bools: Map.String.t((Configs.boolConfig, bool)), + }; + + type action = + | SetString(string, string) + | SetInt(string, int) + | SetFloat(string, float) + | SetBool(string, bool); + + let getRightSidebarElement = (): option(Dom.element) => + Window.window##parent##document##getElementById(rightSidebarId) + ->Js.Nullable.toOption; + + [@react.component] + let make = (~demoUnit: Configs.demoUnitProps => React.element) => { + let (parentWindowRightSidebarElem, setParentWindowRightSidebarElem) = + React.useState(() => None); + + React.useEffect0(() => { + switch (getRightSidebarElement()) { + | Some(elem) => setParentWindowRightSidebarElem(_ => Some(elem)) + | None => () + }; + None; + }); + React.useEffect0(() => { + Window.addMessageListener(event => + if (Window.window##parent === event##source) { + let message: string = event##data; + switch (message->Window.Message.fromStringOpt) { + | Some(RightSidebarDisplayed) => + switch (getRightSidebarElement()) { + | Some(elem) => setParentWindowRightSidebarElem(_ => Some(elem)) + | None => () + } + | None => Js.Console.error("Unexpected message received") + }; + } + ); + None; + }); + let (state, dispatch) = + React.useReducer( + (state, action) => + switch (action) { + | SetString(name, newValue) => { + ...state, + strings: + state.strings + ->Map.String.update(name, value => + value->Option.map(((config, _value, options)) => + (config, newValue, options) + ) + ), + } + | SetInt(name, newValue) => { + ...state, + ints: + state.ints + ->Map.String.update(name, value => + value->Option.map(((config, _value)) => + (config, newValue) + ) + ), + } + | SetFloat(name, newValue) => { + ...state, + floats: + state.floats + ->Map.String.update(name, value => + value->Option.map(((config, _value)) => + (config, newValue) + ) + ), + } + | SetBool(name, newValue) => { + ...state, + bools: + state.bools + ->Map.String.update(name, value => + value->Option.map(((config, _value)) => + (config, newValue) + ) + ), + } + }, + { + let strings = ref(Map.String.empty); + let ints = ref(Map.String.empty); + let floats = ref(Map.String.empty); + let bools = ref(Map.String.empty); + let props: Configs.demoUnitProps = { + string: (name, ~options=?, config) => { + strings := + strings.contents + ->Map.String.set(name, (config, config, options)); + config; + }, + int: (name, config) => { + ints := + ints.contents->Map.String.set(name, (config, config.initial)); + config.initial; + }, + float: (name, config) => { + floats := + floats.contents + ->Map.String.set(name, (config, config.initial)); + config.initial; + }, + bool: (name, config) => { + bools := bools.contents->Map.String.set(name, (config, config)); + config; + }, + }; + + let _ = demoUnit(props); + { + strings: strings.contents, + ints: ints.contents, + floats: floats.contents, + bools: bools.contents, + }; + }, + ); + + let props: Configs.demoUnitProps = { + string: (name, ~options as _=?, _config) => { + let (_, value, _) = state.strings->Map.String.getExn(name); + value; + }, + int: (name, _config) => { + let (_, value) = state.ints->Map.String.getExn(name); + value; + }, + float: (name, _config) => { + let (_, value) = state.floats->Map.String.getExn(name); + value; + }, + bool: (name, _config) => { + let (_, value) = state.bools->Map.String.getExn(name); + value; + }, + }; + +
+
{demoUnit(props)}
+ {switch (parentWindowRightSidebarElem) { + | None => React.null + | Some(element) => + ReactDOM.createPortal( + + dispatch(SetString(name, value)) + } + onIntChange={(name, value) => dispatch(SetInt(name, value))} + onFloatChange={(name, value) => + dispatch(SetFloat(name, value)) + } + onBoolChange={(name, value) => dispatch(SetBool(name, value))} + />, + element, + ) + }} +
; + }; +}; + +module DemoUnitFrame = { + module Css = { + open StyleVars; + + let container = [%cx + {| + flex: 1; + display: flex; + justify-content: center; + align-items: center; + height: 1px; + overflow-y: auto; + |} + ]; + + let containerBackground = responsiveMode => { + let backgroundColor = + switch (responsiveMode) { + | Mobile => Color.midGray + | Desktop => Color.white + }; + [%cx {| + background-color: $(backgroundColor); + |}]; + }; + + let iframe = responsiveMode => { + let height = + switch (responsiveMode) { + | Mobile => `px(667) + | Desktop => `percent(100.) + }; + let width = + switch (responsiveMode) { + | Mobile => `px(375) + | Desktop => `percent(100.) + }; + [%cx + {| + border: none; + height: $(height); + width: $(width); + |} + ]; + }; + }; + + let useFullframeUrl: bool = [%mel.raw + {js|typeof USE_FULL_IFRAME_URL === "boolean" ? USE_FULL_IFRAME_URL : false|js} + ]; + + [@react.component] + let make = + (~queryString: string, ~responsiveMode, ~onLoad: Js.t('a) => unit) => { + let iframePath = if (useFullframeUrl) {"demo/index.html"} else {"demo"}; +
+