diff --git a/app/src/components/ui/select.gleam b/app/src/components/ui/select.gleam new file mode 100644 index 0000000..56b0e7d --- /dev/null +++ b/app/src/components/ui/select.gleam @@ -0,0 +1,129 @@ +import gleam/string +import lustre/attribute.{type Attribute, class} +import lustre/element.{type Element} +import lustre/element/html.{div, label} + +pub type Colors { + Neutral + Primary + Secondary + Success + Info + Warning + Danger +} + +pub fn select( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + label([class("relative")], [ + html.select( + [ + class( + "appearance-none enabled:cursor-pointer disabled:opacity-50 [&:disabled+div]:opacity-50 disabled:cursor-not-allowed w-full", + ), + ..attributes + ], + children, + ), + div( + [ + class( + [ + "pointer-events-none", + "before:absolute before:size-1 before:right-4 before:pointer-events-none", + "before:border-4 before:border-x-transparent before:border-t-0 before:border-b-current before:top-1/2 before:-translate-y-[133.333333%]", + "after:absolute after:size-1 after:right-4 after:pointer-events-none", + "after:border-4 after:border-x-transparent after:border-b-0 after:border-t-current after:top-1/2 after:translate-y-1/3", + ] + |> string.join(" "), + ), + ], + [], + ), + ]) +} + +pub fn flat(color: Colors) -> Attribute(a) { + case color { + Neutral -> + "text-neutral bg-neutral/20 hover:enabled:bg-neutral/30 [&+div]:text-neutral" + Primary -> + "text-primary bg-primary/20 hover:enabled:bg-primary/30 [&+div]:text-primary" + Secondary -> + "text-secondary bg-secondary/20 hover:enabled:bg-secondary/30 [&+div]:text-secondary" + Success -> + "text-success bg-success/20 hover:enabled:bg-success/30 [&+div]:text-success" + Info -> "text-info bg-info/20 hover:enabled:bg-info/30 [&+div]:text-info" + Warning -> + "text-warning bg-warning/20 hover:enabled:bg-warning/30 [&+div]:text-warning" + Danger -> + "text-danger bg-danger/20 hover:enabled:bg-danger/30 [&+div]:text-danger" + } + |> string.append( + " focus:outline-none focus:ring-2 focus:ring-current open:outline-none open:ring-2 open:ring-current", + ) + |> class +} + +pub fn outlined(color: Colors) -> Attribute(a) { + case color { + Neutral -> + "border-neutral focus:enabled:border-neutral open:enabled:border-neutral hover:enabled:border-neutral text-neutral [&+div]:text-neutral" + Primary -> + "border-primary focus:enabled:border-primary open:enabled:border-primary hover:enabled:border-primary text-primary [&+div]:text-primary" + Secondary -> + "border-secondary focus:enabled:border-secondary open:enabled:border-secondary hover:enabled:border-secondary text-secondary [&+div]:text-secondary" + Success -> + "border-success focus:enabled:border-success open:enabled:border-success hover:enabled:border-success text-success [&+div]:text-success" + Info -> + "border-info focus:enabled:border-info open:enabled:border-info hover:enabled:border-info text-info [&+div]:text-info" + Warning -> + "border-warning focus:enabled:border-warning open:enabled:border-warning hover:enabled:border-warning text-warning [&+div]:text-warning" + Danger -> + "border-danger focus:enabled:border-danger open:enabled:border-danger hover:enabled:border-danger text-danger [&+div]:text-danger" + } + |> string.append( + [ + " bg-transparent border-2 border-opacity-50 placeholder-opacity-70", + "hover:enabled:border-opacity-100 focus:enabled:border-opacity-100 open:enabled:border-opacity-100", + "focus:outline-none focus:ring-2 focus:ring-current", + "open:outline-none open:ring-2 open:ring-current", + ] + |> string.join(" "), + ) + |> class +} + +pub fn underlined(color: Colors) -> Attribute(a) { + case color { + Neutral -> "border-neutral text-neutral [&+div]:text-neutral" + Primary -> "border-primary text-primary [&+div]:text-primary" + Secondary -> "border-secondary text-secondary [&+div]:text-secondary" + Success -> "border-success text-success [&+div]:text-success" + Info -> "border-info text-info [&+div]:text-info" + Warning -> "border-warning text-warning [&+div]:text-warning" + Danger -> "border-danger text-danger [&+div]:text-danger" + } + |> string.append( + [ + " bg-transparent border-opacity-20 border-b-2 outline-none rounded-b-none", + "focus:enabled:border-opacity-100 open:enabled:border-opacity-100", + ] + |> string.join(" "), + ) + |> class +} + +pub fn sm() -> Attribute(a) { + class("rounded-sm pl-3.5 pr-9 py-1.5 text-sm") +} + +pub fn md() -> Attribute(a) { + class("rounded-md pl-4 pr-10 py-2 text-base") +} + +pub fn lg() -> Attribute(a) { + class("rounded-lg pl-5 pr-12 py-2.5 text-lg") +} diff --git a/app/src/gleez_ui.gleam b/app/src/gleez_ui.gleam index 509c22f..3d397f8 100644 --- a/app/src/gleez_ui.gleam +++ b/app/src/gleez_ui.gleam @@ -50,6 +50,7 @@ fn on_url_change(uri: Uri) -> Msg { ["docs", "components", "spinner"] -> OnRouteChange(route.Spinner) ["docs", "components", "skeleton"] -> OnRouteChange(route.Skeleton) ["docs", "components", "slider"] -> OnRouteChange(route.Slider) + ["docs", "components", "select"] -> OnRouteChange(route.Select) _ -> OnRouteChange(route.Home) } } @@ -144,6 +145,7 @@ fn with_aside(model: Model) -> Element(Msg) { route.Spinner -> page.spinner() route.Skeleton -> page.skeleton() route.Slider -> page.slider() + route.Select -> page.select() _ -> page.home() }, ]), diff --git a/app/src/model/route.gleam b/app/src/model/route.gleam index 64a8e2c..769b85e 100644 --- a/app/src/model/route.gleam +++ b/app/src/model/route.gleam @@ -26,6 +26,7 @@ pub type Pages { Spinner Skeleton Slider + Select } pub const home = "/" @@ -74,6 +75,8 @@ pub const skeleton = "/docs/components/skeleton/" pub const slider = "/docs/components/slider/" +pub const select = "/docs/components/select/" + pub type Status { None New @@ -102,17 +105,17 @@ pub fn pages() -> List(Page) { Page(input, [], None), Page(link, [], None), Page(chip, [], None), - Page(divider, [], Updated), + Page(divider, [], None), Page(tooltip, [], None), Page(avatar, [], None), Page(badge, [], None), Page(breadcrumbs, [], None), Page(switch, [], None), - Page(kbd, [], New), - Page(checkbox, [], New), - Page(spinner, [], New), - Page(skeleton, [], New), - Page(slider, [], New), + Page(kbd, [], None), + Page(checkbox, [], None), + Page(spinner, [], None), + Page(skeleton, [], None), + Page(select, [], New), ] |> sort_pages, None, @@ -144,6 +147,7 @@ pub fn to_path(page: Pages) -> String { Spinner -> spinner Skeleton -> skeleton Slider -> slider + Select -> select } } @@ -170,6 +174,7 @@ pub fn to_pages(uri: Uri) -> Pages { p if p == spinner -> Spinner p if p == skeleton -> Skeleton p if p == slider -> Slider + p if p == select -> Select _ -> Home } } diff --git a/app/src/pages/demo/demo.gleam b/app/src/pages/demo/demo.gleam index 15ddb4e..c6bdcd4 100644 --- a/app/src/pages/demo/demo.gleam +++ b/app/src/pages/demo/demo.gleam @@ -1,12 +1,16 @@ -import components/ui/slider.{slider} +import components/ui/select.{select} import lustre/attribute.{class} import lustre/element.{type Element} -import lustre/element/html.{div} +import lustre/element/html.{div, option} pub fn demo() -> Element(a) { - div([class("flex flex-col gap-2 py-2 w-full max-w-xs")], [ - slider([slider.solid(slider.Neutral), slider.sm()]), - slider([slider.solid(slider.Neutral), slider.md()]), - slider([slider.solid(slider.Neutral), slider.lg()]), + div([class("flex flex-col flex-wrap gap-4 justify-center w-full pt-8")], [ + select([select.outlined(select.Neutral), select.md()], [option([], "Neutral")]), + select([select.outlined(select.Primary), select.md()], [option([], "Primary")]), + select([select.outlined(select.Secondary), select.md()], [option([], "Secondary")]), + select([select.outlined(select.Success), select.md()], [option([], "Success")]), + select([select.outlined(select.Info), select.md()], [option([], "Info")]), + select([select.outlined(select.Warning), select.md()], [option([], "Warning")]), + select([select.outlined(select.Danger), select.md()], [option([], "Danger")]), ]) } diff --git a/app/src/pages/docs/components/select/attributes.gleam b/app/src/pages/docs/components/select/attributes.gleam new file mode 100644 index 0000000..1e8ee95 --- /dev/null +++ b/app/src/pages/docs/components/select/attributes.gleam @@ -0,0 +1,46 @@ +import components/ui/select.{select} +import lustre/element.{type Element} +import lustre/element/html.{option} +import pages/docs/sections/section + +pub fn attributes() -> Element(a) { + section.attributes([ + section.attribute( + "Size", + " +- `sm()`: Small Size +- `md()`: Medium Size +- `lg()`: Large Size + ", + [ + select([select.outlined(select.Neutral), select.sm()], [ + option([], "Small"), + ]), + select([select.outlined(select.Neutral), select.md()], [ + option([], "Medium"), + ]), + select([select.outlined(select.Neutral), select.lg()], [ + option([], "Large"), + ]), + ], + size_code(), + ), + ]) +} + +fn size_code() -> String { + " +import components/ui/select.{select} +import lustre/attribute.{class} +import lustre/element.{type Element} +import lustre/element/html.{div, option} + +pub fn demo() -> Element(a) { + div([class(\"flex flex-col flex-wrap gap-4 justify-center w-full\")], [ + select([select.outlined(select.Neutral), select.sm()], [option([], \"Small\")]), + select([select.outlined(select.Neutral), select.md()], [option([], \"Medium\")]), + select([select.outlined(select.Neutral), select.lg()], [option([], \"Large\")]), + ]) +} +" +} diff --git a/app/src/pages/docs/components/select/select.gleam b/app/src/pages/docs/components/select/select.gleam new file mode 100644 index 0000000..122cc69 --- /dev/null +++ b/app/src/pages/docs/components/select/select.gleam @@ -0,0 +1,17 @@ +import components/prose.{prose} +import lustre/element.{type Element} +import pages/docs/components/select/attributes.{attributes} +import pages/docs/components/select/variants.{variants} +import pages/docs/sections/section + +pub fn docs() -> Element(a) { + prose([], [ + section.intro( + "Select", + "Select components are used for collecting user provided information from a list of options.", + ), + section.installation("gleam run -m gleez add select"), + variants(), + attributes(), + ]) +} diff --git a/app/src/pages/docs/components/select/variants.gleam b/app/src/pages/docs/components/select/variants.gleam new file mode 100644 index 0000000..5b7ff81 --- /dev/null +++ b/app/src/pages/docs/components/select/variants.gleam @@ -0,0 +1,150 @@ +import components/ui/select.{select} +import lustre/element.{type Element} +import lustre/element/html.{option} +import pages/docs/sections/section + +pub fn variants() -> Element(a) { + section.variants([ + section.variant( + "Outlined", + "", + [ + select([select.outlined(select.Neutral), select.md()], [ + option([], "Neutral"), + ]), + select([select.outlined(select.Primary), select.md()], [ + option([], "Primary"), + ]), + select([select.outlined(select.Secondary), select.md()], [ + option([], "Secondary"), + ]), + select([select.outlined(select.Success), select.md()], [ + option([], "Success"), + ]), + select([select.outlined(select.Info), select.md()], [option([], "Info")]), + select([select.outlined(select.Warning), select.md()], [ + option([], "Warning"), + ]), + select([select.outlined(select.Danger), select.md()], [ + option([], "Danger"), + ]), + ], + outlined_code(), + ), + section.variant( + "Underlined", + "", + [ + select([select.underlined(select.Neutral), select.md()], [ + option([], "Neutral"), + ]), + select([select.underlined(select.Primary), select.md()], [ + option([], "Primary"), + ]), + select([select.underlined(select.Secondary), select.md()], [ + option([], "Secondary"), + ]), + select([select.underlined(select.Success), select.md()], [ + option([], "Success"), + ]), + select([select.underlined(select.Info), select.md()], [ + option([], "Info"), + ]), + select([select.underlined(select.Warning), select.md()], [ + option([], "Warning"), + ]), + select([select.underlined(select.Danger), select.md()], [ + option([], "Danger"), + ]), + ], + underlined_code(), + ), + section.variant( + "Flat", + "", + [ + select([select.flat(select.Neutral), select.md()], [ + option([], "Neutral"), + ]), + select([select.flat(select.Primary), select.md()], [ + option([], "Primary"), + ]), + select([select.flat(select.Secondary), select.md()], [ + option([], "Secondary"), + ]), + select([select.flat(select.Success), select.md()], [ + option([], "Success"), + ]), + select([select.flat(select.Info), select.md()], [option([], "Info")]), + select([select.flat(select.Warning), select.md()], [ + option([], "Warning"), + ]), + select([select.flat(select.Danger), select.md()], [option([], "Danger")]), + ], + flat_code(), + ), + ]) +} + +fn outlined_code() -> String { + " +import components/ui/select.{select} +import lustre/attribute.{class} +import lustre/element.{type Element} +import lustre/element/html.{div, option} + +pub fn demo() -> Element(a) { + div([class(\"flex flex-col flex-wrap gap-4 justify-center w-full\")], [ + select([select.outlined(select.Neutral), select.md()], [option([], \"Neutral\")]), + select([select.outlined(select.Primary), select.md()], [option([], \"Primary\")]), + select([select.outlined(select.Secondary), select.md()], [option([], \"Secondary\")]), + select([select.outlined(select.Success), select.md()], [option([], \"Success\")]), + select([select.outlined(select.Info), select.md()], [option([], \"Info\")]), + select([select.outlined(select.Warning), select.md()], [option([], \"Warning\")]), + select([select.outlined(select.Danger), select.md()], [option([], \"Danger\")]), + ]) +} +" +} + +fn underlined_code() -> String { + " +import components/ui/select.{select} +import lustre/attribute.{class} +import lustre/element.{type Element} +import lustre/element/html.{div, option} + +pub fn demo() -> Element(a) { + div([class(\"flex flex-col flex-wrap gap-4 justify-center w-full\")], [ + select([select.underlined(select.Neutral), select.md()], [option([], \"Neutral\")]), + select([select.underlined(select.Primary), select.md()], [option([], \"Primary\")]), + select([select.underlined(select.Secondary), select.md()], [option([], \"Secondary\")]), + select([select.underlined(select.Success), select.md()], [option([], \"Success\")]), + select([select.underlined(select.Info), select.md()], [option([], \"Info\")]), + select([select.underlined(select.Warning), select.md()], [option([], \"Warning\")]), + select([select.underlined(select.Danger), select.md()], [option([], \"Danger\")]), + ]) +} +" +} + +fn flat_code() -> String { + " +import components/ui/select.{select} +import lustre/attribute.{class} +import lustre/element.{type Element} +import lustre/element/html.{div, option} + +pub fn demo() -> Element(a) { + div([class(\"flex flex-col flex-wrap gap-4 justify-center w-full\")], [ + select([select.flat(select.Neutral), select.md()], [option([], \"Neutral\")]), + select([select.flat(select.Primary), select.md()], [option([], \"Primary\")]), + select([select.flat(select.Secondary), select.md()], [option([], \"Secondary\")]), + select([select.flat(select.Success), select.md()], [option([], \"Success\")]), + select([select.flat(select.Info), select.md()], [option([], \"Info\")]), + select([select.flat(select.Warning), select.md()], [option([], \"Warning\")]), + select([select.flat(select.Danger), select.md()], [option([], \"Danger\")]), + ]) +} +" +} diff --git a/app/src/pages/page.gleam b/app/src/pages/page.gleam index e3e0740..ce20966 100644 --- a/app/src/pages/page.gleam +++ b/app/src/pages/page.gleam @@ -8,11 +8,12 @@ import pages/docs/components/checkbox/checkbox import pages/docs/components/chip/chip import pages/docs/components/divider/divider import pages/docs/components/input/input -import pages/docs/components/slider/slider import pages/docs/components/kbd/kbd import pages/docs/components/link/link -import pages/docs/components/spinner/spinner +import pages/docs/components/select/select import pages/docs/components/skeleton/skeleton +import pages/docs/components/slider/slider +import pages/docs/components/spinner/spinner import pages/docs/components/switch/switch import pages/docs/components/tooltip/tooltip import pages/docs/guide/colors/colors @@ -63,3 +64,5 @@ pub const spinner = spinner.docs pub const skeleton = skeleton.docs pub const slider = slider.docs + +pub const select = select.docs