Current section
Files
Jump to
Current section
Files
src/gbr/ui.gleam
////
//// 🎨 Gleam UI library by @gleam-br
////
//// Loader:
////
//// ```gleam
//// fn view(model: Model) -> Element(ModelEvent) {
//// let Model(loading:) = model
////
//// loading
//// |> ui.loader()
//// }
//// ```
////
//// Horizontal:
////
//// ```gleam
//// pub fn render(in: Login) -> Element(LoginEvent) {
//// let Login(basic:, logo:) = in
////
//// [basic, logo]
//// |> ui.horizontal()
//// }
//// ```
////
//// Main:
////
//// ```gleam
//// pub fn render(in: AdminHome) -> Element(AdminEvent) {
//// let AdminHome(sidebar:, header:, content:, breadcrumb:) = in
////
//// ui.primary(header:, sidebar:, content:, breadcrumb:)
//// }
//// ```
////
import gleam/bool
import gleam/list
import gleam/option.{None}
import lustre/attribute.{class}
import lustre/element
import lustre/element/html
import gbr/ui/svg
import gbr/ui/core/model.{
type UIAttrs, type UIBoxes, type UIOptRender, type UIRender, type UIRenders,
UIBox,
}
/// Construct new super svg element `gbr/ui/svg.{new}`.
///
pub const svg = svg.new
/// UI loader layout with ui screen full blur and a center spin loader.
///
/// Better choice to work with [gbr_js](https://github.com/gleam-br/gbr_js) and `gbr/js/global.{dom_content_loaded}`.
///
/// Supose on dom loaded dispatch OnLoading(False) when:
/// - init: Model(loading: True)
/// - after: Model(loading: False):
///
/// ```gleam
/// fn on_dom_loaded(runtime) {
/// use _ <- function.tap(runtime)
/// use _ <- global.dom_content_loaded()
///
/// AdminEventOnLoading(False)
/// |> lustre.dispatch()
/// |> lustre.send(runtime, _)
/// }
///
/// fn view(model: Model) -> Element(ModelEvent) {
/// let Model(loading:) = model
///
/// loading
/// |> ui.loader()
/// }
/// ```
///
/// ### Fn desc: Trasform one `1 -> 1`
///
/// Bool type to loader element layout
///
/// - `True`: Render loader
/// - `False`: Render `lustre/element.{none}`
///
pub fn loader(loading: Bool) -> UIRender(a) {
use <- bool.guard(!loading, element.none())
html.div([class(loader_class)], [
html.div([class(loader_spin_class)], []),
])
}
/// UI layout with horizontal inner elements.
///
/// Better choise to work with `gbr/ui/login.{basic}`.
///
/// Supose login render function:
///
/// ```gleam
/// pub fn render(in: Login) -> Element(LoginEvent) {
/// let Login(basic:, logo:) = in
///
/// [basic, logo]
/// |> ui.horizontal()
/// }
/// ```
/// ### Fn desc: Reduce `* -> 1`
///
/// List of elements to one element with horizontal layout inner elements.
///
pub fn horizontal(inner: UIRenders(a)) -> UIRender(a) {
html.div([class(horizontal_class)], [
html.div([class(horizontal_main_class)], inner),
])
}
/// UI layout partial with header and sidebar only.
///
/// Better choise when you need only partial primary layout.
///
/// ### Fn desc: Transform two `2 -> 1`
///
/// Two elements to one element with partial primary layout ui.
///
pub fn partial(
header header: UIRender(a),
sidebar sidebar: UIRender(a),
) -> UIRender(a) {
primary(header:, sidebar:, content: None)
}
/// UI primary layout with header, sidebar, breadcrumb and content.
///
/// Excelent layout to admin home page.
///
/// Better choise to work with [gbr_ui_router](https://github.com/gleam-br/gbr_ui_router) links in sidebar to update content element.
///
/// Supose admin home render function:
///
/// ```gleam
/// pub fn render(in: AdminHome) -> Element(AdminEvent) {
/// let AdminHome(sidebar:, header:, content:, breadcrumb:) = in
///
/// ui.primary(header:, sidebar:, content:, breadcrumb:)
/// }
/// ```
///
/// ### Fn desc: Transform three `3 -> 1`
///
/// Three elements to one element with primary layout.
///
pub fn primary(
header header: UIRender(a),
sidebar sidebar: UIRender(a),
content content: UIOptRender(a),
) -> UIRender(a) {
primary_with_breadcrumb(header:, sidebar:, content:, breadcrumb: None)
}
/// UI grid layout with left, right, inner elements.
///
/// Excelent layout to one page message from errors, e.g. 404 or page to payment success message.
///
/// Supose page 404 with grid img background render function:
///
/// ```gleam
/// pub fn render() -> Element(Msg) {
/// let img = html.img([src("/assets/grid-01.svg"), alt("Grid")])
/// let inner = [
/// html.h1([], [html.text("404: Not found")])
/// ]
///
/// ui.grid(left: img, right: img, inner:)
/// }
/// ```
///
/// ### Fn desc: Transform three `3 -> 1`
///
/// Three elements to one element with grid layout.
///
pub fn grid(
left: UIRenders(a),
right: UIRenders(a),
inner: UIRenders(a),
) -> UIRender(a) {
html.div([class(grid_content_class)], [
html.div([class(grid_main_class)], [
html.div([class(grid_left_class)], left),
html.div([class(grid_right_class)], right),
..inner
]),
])
}
/// UI boxes layout
///
pub fn box(in: UIBoxes(a), attrs: UIAttrs(a)) -> UIRender(a) {
let attrs = [class(box_class), ..attrs]
let inner = box_inner(in)
html.div(attrs, inner)
}
/// Same of `gbr/ui.primary` with optional breadcrumb element.
///
/// TODO: WIP
///
pub fn primary_with_breadcrumb(
header header: UIRender(a),
sidebar sidebar: UIRender(a),
content content: UIOptRender(a),
breadcrumb breadcrumb: UIOptRender(a),
) -> UIRender(a) {
let breadcrumb = option.unwrap(breadcrumb, element.none())
let content = option.unwrap(content, element.none())
// page wrapper
html.div([class(main_class)], [
// sidebar area
sidebar,
// content area
html.div([class(main_content_class)], [
//element.none(), overlay mobile, close when outside menu ... todo ...
// header area
header,
// main area
html.main([], [
html.div([class(main_body_class)], [
breadcrumb,
content,
]),
]),
//element.none(), footer area ... todo ...
]),
])
}
// PRIVATE
//
fn box_inner(in) {
use box <- list.map(list.reverse(in))
let UIBox(title:, content:, footer:, attrs:) = box
html.div(attrs, [
html.h3([class(box_title_class)], [title]),
html.p([class(box_body_class)], [content]),
html.div([class(box_footer_class)], [footer]),
])
}
const loader_class = "fixed left-0 top-0 z-999999 flex h-screen w-screen items-center justify-center bg-white dark:bg-black"
const loader_spin_class = "h-16 w-16 animate-spin rounded-full border-4 border-solid border-brand-500 border-t-transparent"
const main_class = "flex h-screen overflow-hidden bg-gray-100 dark:bg-gray-900"
const main_content_class = "relative flex flex-1 flex-col overflow-x-hidden overflow-y-auto"
const main_body_class = "mx-auto max-w-(--breakpoint-2xl) p-4 md:p-6"
const horizontal_class = "relative p-6 sm:p-0"
const horizontal_main_class = "relative flex flex-col justify-center w-full h-screen bg-white dark:bg-gray-900 sm:p-0 lg:flex-row"
const grid_content_class = "relative items-center hidden w-full h-full bg-brand-950 dark:bg-white/5 lg:grid lg:w-1/2"
const grid_main_class = "flex items-center justify-center z-1"
const grid_left_class = "absolute right-0 top-0 -z-1 w-full max-w-[250px] xl:max-w-[450px]"
const grid_right_class = "absolute bottom-0 left-0 -z-1 w-full max-w-[250px] rotate-180 xl:max-w-[450px]"
const box_class = "mx-auto mb-10 w-full max-w-60 rounded-2xl bg-gray-50 px-4 py-5 text-center dark:bg-white/[0.03]"
const box_title_class = "mb-2 font-semibold text-gray-900 dark:text-white"
const box_body_class = "mb-4 text-theme-sm text-gray-500 dark:text-gray-400"
const box_footer_class = "flex items-center justify-center rounded-lg bg-brand-500 p-3 text-theme-sm font-medium text-white hover:bg-brand-600"