diff --git a/package.json b/package.json index 284b15bdf..dba69ff87 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@glennsl/bs-json": "^3.0.0", + "@headlessui/react": "^1.0.0", "@mdx-js/loader": "^1.5.5", "@next/mdx": "^8.1.0", "@octokit/graphql-schema": "^7.4.0", diff --git a/src/Packages.js b/src/Packages.js index 068afcdc0..b77387141 100644 --- a/src/Packages.js +++ b/src/Packages.js @@ -13,13 +13,107 @@ import * as Js_dict from "bs-platform/lib/es6/js_dict.js"; import * as Js_null from "bs-platform/lib/es6/js_null.js"; import FuseJs from "fuse.js"; import * as Process from "process"; +import * as Dropdown from "./components/Dropdown.js"; import * as Markdown from "./components/Markdown.js"; -import * as SearchBox from "./components/SearchBox.js"; import * as Belt_Array from "bs-platform/lib/es6/belt_Array.js"; import * as Navigation from "./components/Navigation.js"; import * as Belt_Option from "bs-platform/lib/es6/belt_Option.js"; import * as Caml_option from "bs-platform/lib/es6/caml_option.js"; +function Packages$SearchBox(Props) { + var completionValuesOpt = Props.completionValues; + var value = Props.value; + var onClear = Props.onClear; + var placeholderOpt = Props.placeholder; + var onValueChange = Props.onValueChange; + var completionValues = completionValuesOpt !== undefined ? completionValuesOpt : []; + var placeholder = placeholderOpt !== undefined ? placeholderOpt : ""; + var match = React.useState(function () { + return /* Inactive */1; + }); + var setState = match[1]; + var state = match[0]; + var textInput = React.useRef(null); + var onMouseDownClear = function (evt) { + evt.preventDefault(); + return Curry._1(onClear, undefined); + }; + var onAreaFocus = function (evt) { + var el = evt.target; + var isDiv = (el.type == null); + if (isDiv && state === /* Inactive */1) { + return Belt_Option.forEach(Caml_option.nullable_to_opt(textInput.current), (function (el) { + el.focus(); + + })); + } + + }; + var onFocus = function (param) { + return Curry._1(setState, (function (param) { + return /* Active */0; + })); + }; + var onBlur = function (param) { + return Curry._1(setState, (function (param) { + return /* Inactive */1; + })); + }; + var onKeyDown = function (evt) { + var key = evt.key; + var ctrlKey = evt.ctrlKey; + var full = ( + ctrlKey ? "CTRL+" : "" + ) + key; + switch (full) { + case "Escape" : + return Curry._1(onClear, undefined); + case "Tab" : + if (completionValues.length !== 1) { + return ; + } + var targetValue = Belt_Array.getExn(completionValues, 0); + if (targetValue !== value) { + evt.preventDefault(); + return Curry._1(onValueChange, targetValue); + } else { + return ; + } + default: + return ; + } + }; + var onChange = function (evt) { + evt.preventDefault(); + return Curry._1(onValueChange, evt.target.value); + }; + return React.createElement("div", { + className: " flex bg-white items-center rounded-lg py-4 px-5", + tabIndex: -1, + onFocus: onAreaFocus, + onBlur: onBlur + }, React.createElement(Icon.MagnifierGlass.make, { + className: ( + state === /* Active */0 ? "text-gray-100" : "text-gray-60" + ) + " w-5 h-5" + }), React.createElement("input", { + ref: textInput, + className: "text-16 font-medium text-gray-95 outline-none ml-4 w-full", + placeholder: placeholder, + type: "text", + value: value, + onKeyDown: onKeyDown, + onFocus: onFocus, + onChange: onChange + }), React.createElement("button", { + className: value === "" ? "hidden" : "block", + onFocus: onFocus, + onMouseDown: onMouseDownClear + }, React.createElement(Icon.Close.make, { + className: "w-4 h-4 text-gray-60 hover:text-gray-100" + }))); +} + function shouldFilter(res) { if (res.TAG === /* Npm */0 && res._0.name.startsWith("@elm-react")) { return true; @@ -153,7 +247,7 @@ function Packages$Card(Props) { repoEl = null; } linkBox = React.createElement("div", { - className: "text-14 space-x-2 mt-1" + className: "text-12 text-gray-40 space-x-2 mt-1" }, React.createElement("a", { className: "hover:text-fire", href: pkg.npmHref, @@ -185,16 +279,24 @@ function Packages$Card(Props) { match$2.keywords ]; } + var versionEl; + versionEl = value.TAG === /* Npm */0 ? React.createElement("span", { + className: "text-12 text-gray-40 font-medium" + }, value._0.version) : null; return React.createElement("div", { - className: "bg-gray-5-tr py-6 rounded-lg p-4" + className: "bg-white py-6 shadow-xs rounded-lg p-4" }, React.createElement("div", { className: "flex justify-between" - }, React.createElement("div", undefined, React.createElement("a", { - className: "font-bold hover:text-fire text-18", - href: titleHref, - target: "_blank" - }, React.createElement("span", undefined, match[0])), linkBox), React.createElement("div", undefined, icon)), React.createElement("div", { - className: "mt-4 text-16" + }, React.createElement("div", undefined, React.createElement("div", { + className: "space-x-2" + }, React.createElement("a", { + className: "font-bold hover:text-fire font-semibold text-18", + href: titleHref, + target: "_blank" + }, React.createElement("span", undefined, match[0])), versionEl), linkBox), React.createElement("div", { + className: "text-gray-90" + }, icon)), React.createElement("div", { + className: "mt-4 text-14" }, match[1]), React.createElement("div", { className: "space-x-2 mt-4" }, Belt_Array.map(match[2], (function (keyword) { @@ -204,7 +306,7 @@ function Packages$Card(Props) { })); var tmp = { key: keyword, - className: "hover:pointer px-2 rounded-lg text-white bg-fire-70 text-14" + className: "hover:pointer border border-fire-40 hover:border-gray-100 px-2 rounded text-gray-60-tr hover:text-gray-95 bg-fire-40 text-12" }; if (onMouseDown !== undefined) { tmp.onMouseDown = Caml_option.valFromOption(onMouseDown); @@ -213,12 +315,12 @@ function Packages$Card(Props) { })))); } -function Packages$Category(Props) { - var title = Props.title; - var children = Props.children; - return React.createElement("div", undefined, React.createElement("h3", { - className: "font-sans font-medium text-gray-100 tracking-wide text-14 uppercase mb-2" - }, title), React.createElement("div", undefined, children)); +function toString(t) { + if (t) { + return "Community Resources"; + } else { + return "Official Resources"; + } } function Packages$InfoSidebar$Toggle(Props) { @@ -249,41 +351,12 @@ function Packages$InfoSidebar(Props) { }, "Filter for"), React.createElement("div", { className: "space-y-2" }, React.createElement(Packages$InfoSidebar$Toggle, { - enabled: filter.includeOfficial, - toggle: (function (param) { - return Curry._1(setFilter, (function (prev) { - return { - searchterm: prev.searchterm, - includeOfficial: !filter.includeOfficial, - includeCommunity: prev.includeCommunity, - includeNpm: prev.includeNpm, - includeUrlResource: prev.includeUrlResource - }; - })); - }), - children: "Official" - }), React.createElement(Packages$InfoSidebar$Toggle, { - enabled: filter.includeCommunity, - toggle: (function (param) { - return Curry._1(setFilter, (function (prev) { - return { - searchterm: prev.searchterm, - includeOfficial: prev.includeOfficial, - includeCommunity: !filter.includeCommunity, - includeNpm: prev.includeNpm, - includeUrlResource: prev.includeUrlResource - }; - })); - }), - children: "Community" - }), React.createElement(Packages$InfoSidebar$Toggle, { enabled: filter.includeNpm, toggle: (function (param) { return Curry._1(setFilter, (function (prev) { return { searchterm: prev.searchterm, - includeOfficial: prev.includeOfficial, - includeCommunity: prev.includeCommunity, + category: prev.category, includeNpm: !filter.includeNpm, includeUrlResource: prev.includeUrlResource }; @@ -296,8 +369,7 @@ function Packages$InfoSidebar(Props) { return Curry._1(setFilter, (function (prev) { return { searchterm: prev.searchterm, - includeOfficial: prev.includeOfficial, - includeCommunity: prev.includeCommunity, + category: prev.category, includeNpm: prev.includeNpm, includeUrlResource: !filter.includeUrlResource }; @@ -333,12 +405,12 @@ function $$default(props) { var match$1 = React.useState(function () { return { searchterm: "", - includeOfficial: true, - includeCommunity: true, + category: /* Official */0, includeNpm: true, includeUrlResource: true }; }); + var setFilter = match$1[1]; var filter = match$1[0]; var npms = Belt_Array.map(props.packages, (function (pkg) { return { @@ -383,9 +455,9 @@ function $$default(props) { var isResourceIncluded; isResourceIncluded = next.TAG === /* Npm */0 ? filter.includeNpm : filter.includeUrlResource; if (isResourceIncluded) { - if (filter.includeOfficial && isOfficial(next)) { + if (filter.category === /* Official */0 && isOfficial(next)) { official.push(next); - } else if (filter.includeCommunity && !shouldFilter(next)) { + } else if (filter.category === /* Community */1 && !shouldFilter(next)) { community.push(next); } @@ -406,30 +478,56 @@ function $$default(props) { }; })); }; - var officialCategory = officialResources.length !== 0 ? React.createElement(Packages$Category, { - title: "Official Resources", - children: React.createElement("div", { - className: "space-y-4" - }, Belt_Array.map(officialResources, (function (res) { - return React.createElement(Packages$Card, { - value: res, - onKeywordSelect: onKeywordSelect, - key: res._0.name - }); - }))) - }) : null; - var communityCategory = communityResources.length !== 0 ? React.createElement(Packages$Category, { - title: "Community Resources", - children: React.createElement("div", { - className: "space-y-4" - }, Belt_Array.map(communityResources, (function (res) { - return React.createElement(Packages$Card, { - value: res, - onKeywordSelect: onKeywordSelect, - key: res._0.name - }); - }))) - }) : null; + var officialCategory = officialResources.length !== 0 ? React.createElement("div", { + className: "space-y-4" + }, Belt_Array.map(officialResources, (function (res) { + return React.createElement(Packages$Card, { + value: res, + onKeywordSelect: onKeywordSelect, + key: res._0.name + }); + }))) : null; + var communityCategory = communityResources.length !== 0 ? React.createElement("div", { + className: "space-y-4" + }, Belt_Array.map(communityResources, (function (res) { + return React.createElement(Packages$Card, { + value: res, + onKeywordSelect: onKeywordSelect, + key: res._0.name + }); + }))) : null; + var searchOverview; + if (state) { + var match$3 = filter.category; + var match$4 = match$3 ? [ + communityResources.length, + "\"" + ( + filter.category ? "Community Resources" : "Official Resources" + ) + "\"" + ] : [ + officialResources.length, + "\"" + ( + filter.category ? "Community Resources" : "Official Resources" + ) + "\"" + ]; + var numOfPackages = match$4[0]; + var packagePluralSingular = numOfPackages > 1 || numOfPackages === 0 ? "packages" : "package"; + searchOverview = React.createElement("div", { + className: "font-medium" + }, React.createElement("div", { + className: "text-42 text-gray-95" + }, state._0), React.createElement("div", { + className: "text-gray-60-tr" + }, React.createElement("span", { + className: "text-gray-95" + }, numOfPackages), " " + packagePluralSingular + " found in ", React.createElement("span", { + className: "text-gray-95" + }, match$4[1]))); + } else { + searchOverview = null; + } + var match$5 = filter.category; + var searchResult = match$5 ? communityCategory : officialCategory; var router = Next.Router.useRouter(undefined); var firstRenderDone = React.useRef(false); React.useEffect((function () { @@ -464,7 +562,7 @@ function $$default(props) { description: "Official and unofficial resources, libraries and bindings for ReScript", title: "Package Index | ReScript Documentation" }), React.createElement("div", { - className: "mt-16 pt-2" + className: "mt-16" }, React.createElement("div", { className: "text-gray-80 text-lg" }, React.createElement(Navigation.make, { @@ -472,32 +570,60 @@ function $$default(props) { }), React.createElement("div", { className: "flex overflow-hidden" }, React.createElement("div", { - className: "flex justify-between min-w-320 px-4 pt-16 lg:align-center w-full lg:px-8 pb-48" + className: "flex justify-between min-w-320 lg:align-center w-full" }, React.createElement(Mdx.Provider.make, { components: Markdown.$$default, - children: null - }, React.createElement("main", { - className: "max-w-1280 w-full flex justify-center" - }, React.createElement("div", { - className: "w-full", - style: { - maxWidth: "44.0625rem" - } - }, React.createElement(Markdown.H1.make, { - children: "Libraries & Bindings" - }), React.createElement(SearchBox.make, { - value: searchValue, - onClear: onClear, - placeholder: "Enter a search term, name, keyword, etc", - onValueChange: onValueChange - }), React.createElement("div", { - className: "mt-12 space-y-8" - }, officialCategory, communityCategory))), React.createElement("div", { - className: "hidden lg:block h-full " - }, React.createElement(Packages$InfoSidebar, { - setFilter: match$1[1], - filter: filter - }))))), React.createElement(Footer.make, {})))); + children: React.createElement("main", { + className: "w-full" + }, React.createElement("div", { + className: "relative w-full bg-gray-100 py-16" + }, React.createElement("div", { + className: "px-4 relative z-10 max-w-1280 flex justify-center" + }, React.createElement("div", { + className: "w-full", + style: { + maxWidth: "47.5625rem" + } + }, React.createElement("h1", { + className: "text-white mb-10 md:mb-2 text-42 leading-1 font-medium antialiased" + }, "Libraries and Bindings"), React.createElement(Packages$SearchBox, { + value: searchValue, + onClear: onClear, + placeholder: "Enter a search term, keyword, etc", + onValueChange: onValueChange + }))), React.createElement("img", { + className: "h-48 absolute bottom-0 right-0", + src: "/static/illu_index_rescript@2x.png" + })), React.createElement("div", { + className: "bg-gray-5 px-4 lg:px-8 pb-48" + }, React.createElement("div", { + className: "pt-6" + }, searchOverview), React.createElement("div", undefined, React.createElement(Dropdown.make, { + value: filter.category, + itemToString: toString, + onChange: (function (category) { + return Curry._1(setFilter, (function (prev) { + return { + searchterm: prev.searchterm, + category: category, + includeNpm: prev.includeNpm, + includeUrlResource: prev.includeUrlResource + }; + })); + }), + items: [ + /* Official */0, + /* Community */1 + ] + })), React.createElement("div", { + className: "mt-12 space-y-8" + }, searchResult), React.createElement("div", { + className: "hidden lg:block h-full " + }, React.createElement(Packages$InfoSidebar, { + setFilter: setFilter, + filter: filter + })))) + }))), React.createElement(Footer.make, {})))); } function getStaticProps(_ctx) { diff --git a/src/Packages.res b/src/Packages.res index a64b454cf..d0c88e617 100644 --- a/src/Packages.res +++ b/src/Packages.res @@ -25,6 +25,104 @@ type npmPackage = { npmHref: string, } +module SearchBox = { + @bs.send external focus: Dom.element => unit = "focus" + + type state = + | Active + | Inactive + + @react.component + let make = ( + ~completionValues: array=[], // set of possible values + ~value: string, + ~onClear: unit => unit, + ~placeholder: string="", + ~onValueChange: string => unit, + ) => { + let (state, setState) = React.useState(_ => Inactive) + let textInput = React.useRef(Js.Nullable.null) + + let onMouseDownClear = evt => { + ReactEvent.Mouse.preventDefault(evt) + onClear() + } + + let focusInput = () => + textInput.current->Js.Nullable.toOption->Belt.Option.forEach(el => el->focus) + + let onAreaFocus = evt => { + let el = ReactEvent.Focus.target(evt) + let isDiv = Js.Null_undefined.isNullable(el["type"]) + + if isDiv && state === Inactive { + focusInput() + } + } + + let onFocus = _ => { + setState(_ => Active) + } + + let onBlur = _ => { + setState(_ => Inactive) + } + + let onKeyDown = evt => { + let key = ReactEvent.Keyboard.key(evt) + let ctrlKey = ReactEvent.Keyboard.ctrlKey(evt) + + let full = (ctrlKey ? "CTRL+" : "") ++ key + + switch full { + | "Escape" => onClear() + | "Tab" => + if Js.Array.length(completionValues) === 1 { + let targetValue = Belt.Array.getExn(completionValues, 0) + + if targetValue !== value { + ReactEvent.Keyboard.preventDefault(evt) + onValueChange(targetValue) + } else { + () + } + } + | _ => () + } + } + + let onChange = evt => { + ReactEvent.Form.preventDefault(evt) + let value = ReactEvent.Form.target(evt)["value"] + onValueChange(value) + } + +
+ + + +
+ } +} module Resource = { type t = Npm(npmPackage) | Url(urlResource) @@ -150,7 +248,7 @@ module Card = { | None => React.null } -
+
{React.string("NPM")} @@ -169,18 +267,31 @@ module Card = { | Url({name, description, keywords}) => (name, description, keywords) } -
+ let versionEl = switch value { + | Resource.Npm({version}) => + {React.string(version)} + | _ => React.null + } + +
-
{icon}
+
{icon}
-
{React.string(description)}
-
{Belt.Array.map(keywords, keyword => { +
{React.string(description)}
+
+ {Belt.Array.map(keywords, keyword => { let onMouseDown = Belt.Option.map(onKeywordSelect, cb => { evt => { ReactEvent.Mouse.preventDefault(evt) @@ -189,11 +300,12 @@ module Card = { }) - })->React.array}
+ })->React.array} +
} } @@ -208,23 +320,12 @@ module Category = { | Official => "Official Resources" | Community => "Community Resources" } - - @react.component - let make = (~title: string, ~children) => { -
-

- {React.string(title)} -

-
children
-
- } } module Filter = { type t = { searchterm: string, - includeOfficial: bool, - includeCommunity: bool, + category: Category.t, includeNpm: bool, includeUrlResource: bool, } @@ -254,24 +355,6 @@ module InfoSidebar = {

{React.string("Filter for")}

- { - setFilter(prev => { - {...prev, Filter.includeOfficial: !filter.includeOfficial} - }) - }}> - {React.string("Official")} - - { - setFilter(prev => { - {...prev, Filter.includeCommunity: !filter.includeCommunity} - }) - }}> - {React.string("Community")} - { @@ -315,26 +398,21 @@ type state = | All | Filtered(string) // search term -let scrollToTop: unit => unit = %raw( - `function() { +let scrollToTop: unit => unit = %raw(`function() { window.scroll({ top: 0, left: 0, behavior: 'smooth' }); } -` -) +`) let default = (props: props) => { - open Markdown - let (state, setState) = React.useState(_ => All) let (filter, setFilter) = React.useState(_ => { Filter.searchterm: "", - includeOfficial: true, - includeCommunity: true, + category: Category.Official, includeNpm: true, includeUrlResource: true, }) @@ -369,25 +447,24 @@ let default = (props: props) => { setState(_ => All) } - let (officialResources, communityResources) = Belt.Array.reduce( - resources, - ([], []), - (acc, next) => { - let (official, community) = acc - let isResourceIncluded = switch next { - | Npm(_) => filter.includeNpm - | Url(_) => filter.includeUrlResource - } - if !isResourceIncluded { - () - } else if filter.includeOfficial && Resource.isOfficial(next) { - Js.Array2.push(official, next)->ignore - } else if filter.includeCommunity && !Resource.shouldFilter(next) { - Js.Array2.push(community, next)->ignore - } - (official, community) - }, - ) + let (officialResources, communityResources) = Belt.Array.reduce(resources, ([], []), ( + acc, + next, + ) => { + let (official, community) = acc + let isResourceIncluded = switch next { + | Npm(_) => filter.includeNpm + | Url(_) => filter.includeUrlResource + } + if !isResourceIncluded { + () + } else if filter.category === Category.Official && Resource.isOfficial(next) { + Js.Array2.push(official, next)->ignore + } else if filter.category === Category.Community && !Resource.shouldFilter(next) { + Js.Array2.push(community, next)->ignore + } + (official, community) + }) let onKeywordSelect = keyword => { scrollToTop() @@ -399,21 +476,49 @@ let default = (props: props) => { let officialCategory = switch officialResources { | [] => React.null | resources => - -
{Belt.Array.map(resources, res => { - - })->React.array}
-
+
+ {Belt.Array.map(resources, res => { + + })->React.array} +
} let communityCategory = switch communityResources { | [] => React.null | resources => - -
{Belt.Array.map(resources, res => { - - })->React.array}
-
+
+ {Belt.Array.map(resources, res => { + + })->React.array} +
+ } + + let searchOverview = switch state { + | Filtered(search) => + let (numOfPackages, categoryName) = switch filter.category { + | Official => (officialResources->Js.Array2.length, `"${Category.toString(filter.category)}"`) + | Community => (communityResources->Js.Array2.length, `"${Category.toString(filter.category)}"`) + } + + let packagePluralSingular = if numOfPackages > 1 || numOfPackages === 0 { + "packages" + } else { + "package" + } +
+
{React.string(search)}
+
+ {React.int(numOfPackages)} + {React.string(` ${packagePluralSingular} found in `)} + {categoryName->React.string} +
+
+ | All => React.null + } + + let searchResult = switch filter.category { + | Official => officialCategory + | Community => communityCategory } let router = Next.Router.useRouter() @@ -456,26 +561,56 @@ let default = (props: props) => { title="Package Index | ReScript Documentation" description="Official and unofficial resources, libraries and bindings for ReScript" /> -
+
-
+
-
-
-

{React.string("Libraries & Bindings")}

- +
+
+ // Centered Searchbox header thing +
+

+ {React.string("Libraries and Bindings")} +

+ +
+
+ -
officialCategory communityCategory
+
+ // Actual content +
+
searchOverview
+ // Box for the filter +
+ { + setFilter(prev => { + ...prev, + category: category, + }) + }} + items=[Category.Official, Community] + itemToString=Category.toString + /> +
+ // Box for the results +
searchResult
+
-
diff --git a/src/bindings/HeadlessUI.js b/src/bindings/HeadlessUI.js new file mode 100644 index 000000000..e581087a1 --- /dev/null +++ b/src/bindings/HeadlessUI.js @@ -0,0 +1,36 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + + +var Button = {}; + +var Items = {}; + +var Item = {}; + +var Menu = { + Button: Button, + Items: Items, + Item: Item +}; + +var Button$1 = {}; + +var Options = {}; + +var $$Option = {}; + +var Listbox = { + Button: Button$1, + Options: Options, + $$Option: $$Option +}; + +var Transition = {}; + +export { + Menu , + Listbox , + Transition , + +} +/* No side effect */ diff --git a/src/bindings/HeadlessUI.res b/src/bindings/HeadlessUI.res new file mode 100644 index 000000000..a429ce16b --- /dev/null +++ b/src/bindings/HeadlessUI.res @@ -0,0 +1,72 @@ +module Menu = { + type state = {@as("open") open_: bool} + + @module("@headlessui/react") @react.component + external make: ( + ~_as: [#div]=?, + ~className: string=?, + ~children: state => React.element, + ) => React.element = "Menu" + + module Button = { + @module("@headlessui/react") @scope("Menu") @react.component + external make: (~className: string=?, ~children: React.element) => React.element = "Button" + } + + module Items = { + @module("@headlessui/react") @scope("Menu") @react.component + external make: (~static: bool=?, ~children: React.element) => React.element = "Items" + } + + module Item = { + type state = {active: bool} + @module("@headlessui/react") @scope("Menu") @react.component + external make: (~children: state => React.element) => React.element = "Item" + } +} + +module Listbox = { + type state = {@as("open") open_: bool} + + @module("@headlessui/react") @react.component + external make: ( + ~value: 'state=?, + ~onChange: 'state => unit=?, + ~className: string=?, + ~children: state => React.element, + ) => React.element = "Listbox" + + module Button = { + @module("@headlessui/react") @scope("Listbox") @react.component + external make: (~className: string=?, ~children: React.element=?) => React.element = "Button" + } + + module Options = { + @module("@headlessui/react") @scope("Listbox") @react.component + external make: (~static: bool=?, ~className: string=?, ~children: React.element) => React.element = "Options" + } + + module Option = { + type state = {selected: bool, active: bool} + @module("@headlessui/react") @scope("Listbox") @react.component + external make: ( + ~children: state => React.element, + ~value: 'item, + ~disabled: bool=?, + ) => React.element = "Option" + } +} + +module Transition = { + @module("@headlessui/react") @react.component + external make: ( + ~show: bool, + ~enter: string=?, + ~enterFrom: string=?, + ~enterTo: string=?, + ~leave: string=?, + ~leaveFrom: string=?, + ~leaveTo: string=?, + ~children: React.element, + ) => React.element = "Transition" +} diff --git a/src/components/Dropdown.js b/src/components/Dropdown.js new file mode 100644 index 000000000..8c446a2a1 --- /dev/null +++ b/src/components/Dropdown.js @@ -0,0 +1,59 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Icon from "./Icon.js"; +import * as Curry from "bs-platform/lib/es6/curry.js"; +import * as React from "react"; +import * as Caml_option from "bs-platform/lib/es6/caml_option.js"; +import * as React$1 from "@headlessui/react"; + +function Dropdown(Props) { + var value = Props.value; + var itemToString = Props.itemToString; + var onChange = Props.onChange; + var itemsOpt = Props.items; + var items = itemsOpt !== undefined ? itemsOpt : []; + var tmp = { + value: value, + children: (function (param) { + return React.createElement(React.Fragment, undefined, React.createElement(React$1.Listbox.Button, { + className: "py-2 px-2 focus:outline-none rounded border border-gray-60-tr", + children: Curry._1(itemToString, value) + }), React.createElement(React$1.Transition, { + show: param.open, + leave: "transition ease-in duration-100", + leaveFrom: "opacity-100", + leaveTo: "opacity-0", + children: React.createElement(React$1.Listbox.Options, { + static: true, + className: "border border-gray-60-tr shadow-xs rounded focus:outline-none", + children: items.map(function (item) { + var itemStr = Curry._1(itemToString, item); + return React.createElement(React$1.Listbox.Option, { + children: (function (state) { + return React.createElement("li", { + className: state.active ? "text-red-500" : "" + }, state.selected ? React.createElement(Icon.Check.make, { + className: "inline-block w-4 h-4 mr-2" + }) : null, itemStr); + }), + value: item, + key: itemStr + }); + }) + }) + })); + }) + }; + if (onChange !== undefined) { + tmp.onChange = Caml_option.valFromOption(onChange); + } + return React.createElement(React$1.Listbox, tmp); +} + +var make = Dropdown; + +export { + make , + +} +/* Icon Not a pure module */ diff --git a/src/components/Dropdown.res b/src/components/Dropdown.res new file mode 100644 index 000000000..c7feac3a8 --- /dev/null +++ b/src/components/Dropdown.res @@ -0,0 +1,87 @@ +@react.component +let make = ( + ~value: 'item, + ~itemToString: 'item => string, + ~onChange: option<'item => unit>=?, + ~items: array<'item>=[], +) => { + open HeadlessUI + + /* + + {({open_}) => { + <> + + {selected->itemToString->React.string} + + + + {items + ->Js.Array2.map(item => { + + {state => { + let {active} = state + let onClick = switch onSelect { + | Some(onSelect) => + Some( + e => { + ReactEvent.Mouse.preventDefault(e) + onSelect(item) + }, + ) + | None => None + } +
+ {item->itemToString->React.string} +
+ }} +
+ }) + ->React.array} +
+
+ + }} +
+ */ + + + {({open_}) => { + <> + + {value->itemToString->React.string} + + + + {items + ->Js.Array2.map(item => { + let itemStr = itemToString(item) + + {state => { + let {selected, active} = state +
  • + {selected ? : React.null} + {itemStr->React.string} +
  • + }} +
    + }) + ->React.array} +
    +
    + + }} +
    +} diff --git a/src/components/Icon.js b/src/components/Icon.js index 8bdcd7db4..a81575e04 100644 --- a/src/components/Icon.js +++ b/src/components/Icon.js @@ -373,6 +373,26 @@ var Copy = { make: Icon$Copy }; +function Icon$Check(Props) { + var classNameOpt = Props.className; + var className = classNameOpt !== undefined ? classNameOpt : ""; + return React.createElement("svg", { + className: "stroke-current " + className, + height: "9.111", + width: "12.513", + viewBox: "0 0 12.513 9.111", + xmlns: "http://www.w3.org/2000/svg" + }, React.createElement("path", { + d: "M.703 3.588l4.108 4.109 6.99-6.99", + fill: "none", + strokeWidth: "2" + })); +} + +var Check = { + make: Icon$Check +}; + export { Github , Npm , @@ -389,6 +409,7 @@ export { TriangleDown , ExternalLink , Copy , + Check , } /* react Not a pure module */ diff --git a/src/components/Icon.res b/src/components/Icon.res index ff0628be0..19c99ff9b 100644 --- a/src/components/Icon.res +++ b/src/components/Icon.res @@ -259,3 +259,17 @@ module Copy = { } + +module Check = { + @react.component + let make = (~className: string="") => { + + + + } +} diff --git a/src/components/Icon.resi b/src/components/Icon.resi index c7d6ccb1a..a63769549 100644 --- a/src/components/Icon.resi +++ b/src/components/Icon.resi @@ -73,3 +73,8 @@ module Copy: { @react.component let make: (~className: string=?) => React.element } + +module Check: { + @react.component + let make: (~className: string=?) => React.element +} diff --git a/tailwind.config.js b/tailwind.config.js index 25ab9edb4..11956e89f 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -151,6 +151,7 @@ module.exports = { "21": "1.3125rem", "28": "1.5rem", "32": "2rem", + "36": "2.25rem", "42": "2.625rem", "56": "3.5rem", "96": "6rem", @@ -228,7 +229,8 @@ module.exports = { cursor: ["hover"], width: ["responsive"], border: ["hover", "responsive"], - borderWidth: ["active", "responsive", "last", "first"], + borderColor: ["hover"], + borderWidth: ["hover", "active", "responsive", "last", "first"], borderRadius: ["first", "responsive"], padding: ["hover", "responsive", "last"], margin: ["hover", "responsive", "first", "last"], diff --git a/yarn.lock b/yarn.lock index 8a987ea78..698a4b4f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1261,6 +1261,11 @@ resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6" integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw== +"@headlessui/react@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.0.0.tgz#661b50ebfd25041abb45d8eedd85e7559056bcaf" + integrity sha512-mjqRJrgkbcHQBfAHnqH0yRxO/y/22jYrdltpE7WkurafREKZ+pj5bPBwYHMt935Sdz/n16yRcVmsSCqDFHee9A== + "@mdx-js/loader@^1.5.5": version "1.6.16" resolved "https://registry.yarnpkg.com/@mdx-js/loader/-/loader-1.6.16.tgz#5a9c3b0ab41885cd2df85bcf360644ca63e44e88"