README.md

# 🎨 Gleam UI lustre library

🚧 **Work in progress** not production ready.

[Gleam](https://gleam.run/) UI [lustre](https://lustre.build/) library by @gleam-br

Library based on [TailAdmin](https://tailadmin.com/)

🌝 Nothing stateful only stateless uses only lustre render functions.

[![Package Version](https://img.shields.io/hexpm/v/gbr_ui)](https://hex.pm/packages/gbr_ui)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/gbr_ui/)

## Light mode

<img width="1908" height="835" alt="image" src="https://github.com/user-attachments/assets/3828500a-8303-471d-bb97-8f793d9aff06" />

## Dark mode

<img width="1907" height="783" alt="image" src="https://github.com/user-attachments/assets/df92dd99-6784-4288-ae84-f2fd96d9d91e" />

## How to use

```sh
gleam add gbr_ui@1
```

```gleam
import lustre
import lustre/element.{type Element}

import gbr/ui
import gbr/ui/svg
import gbr/ui/svg/alert as svg_alert

type Msg

type Model

pub fn main() -> Nil {
  lustre.simple(init, update, view)
  |> lustre.start("body", Nil)
}

fn view(model: Model) -> Element(Msg) {
  let Model(header:, sidebar:) = model
  let content =
    svg.of(32, 32)
      |> svg_alert.success()
      |> svg.render()

  ui.main(header:, sidebar:, content:)
}
// init, update ...
```

Further documentation can be found at <https://hexdocs.pm/gbr_ui>.

## Issues

- https://tailwindcss.com/blog/tailwindcss-v4#automatic-content-detection

Tailwindcss v4 has automatic content detection and search only into src directory.

If you want use gleam libraries that uses tailwind do not forget configure your `./build/dev/javascript` files:

Put `@source` ref. into your css file, e.g. `main.css`:

```css
@import "tailwindcss";

@source "./build/dev/javascript";
```

## ✍ Design

In view element at render type show with render function.

"No elemento de visualização no tipo de renderização mostrar com função de renderização." `pt-BR`

Then, the module functions always reveice an element view.

"Então, as funções dos módulos recebem sempre um elemento de visualização."

Could be function to set attributes or others.

"Poderiam ser funções para altera atributos ou outras coisas."

Then, the element view by a transform function at a render type to render function return lustre element.

"Então, o element de visualização por uma função de transformação em um tipo de renderização p/ a função de renderização retornar um elemento `lustre/element.{type Element}`."

> Not is required, could be direct in element view to render function.
>
> "Não é obrigatório, pode ser direto no elemento de visualização p/ a função de rederização"

Supose, a button text and svg at left side:

"Suponha, um botão de texto e um svg do lado esquerdo:"

```gleam
import gbr/ui/button
import gbr/ui/svg
import gbr/ui/svg/icons svg_icons
import gbr/ui/core/model.{type UIRender, type UILabel, uilabel}

pub fn show(id: String, text: UILabel, onclick: a) -> UIRender(a) {
  // render inner details
  let inner = [
    // new element view svg
    // "Novo elemento de visualização svg :)"
    svg.new("btn-icon-back", 20, 20)
    // set behavior to back icon
    // "Altera o atributo class"
    |> svg_icons.back()
    // render function direct transform element view to lustre element
    // "Função de renderização transforma p/ lustre"
    |> svg.render(),
  ]

  // new element view
  // "Novo elemento de visualização"
  button.new(id)
  // set attribute class
  // "Altera o atributo class"
  |> button.class(class_back)
  // set attribute label
  // "Altera o atributo label"
  |> button.label(text)
  // render type function
  // "A função de tipo de renderização"
  |> button.at_left(inner)
  // set render type attribute on click event
  // "Altera o tipo de renderização p/ evento on click"
  |> button.on_click(onclick)
  // render function transform to lustre element
  // "Função de renderização transforma p/ lustre"
  |> button.render()
}
```

## ✏️ Styling

#### Lustre [dev-tools](https://hexdocs.pm/lustre_dev_tools/toml-reference.html)

Gleam config `gleam.toml`:

```toml
[tools.lustre.html]
stylesheets = [
  { href = "/build/dev/javascript/gbr_ui/priv/gbr/ui.css" }
]
```

### Tailwind

[Tailwindcss](https://tailwindcss.com/docs/installation) is required to uses this library and show styling classes correctly.

### Vite @tailwindcss/vite

Using tailwind in vite project with [@tailwindcss/vite](https://tailwindcss.com/docs/installation/using-vite) and:

- with [vite-plugin-gleam](https://github.com/gleam-br/vite-plugin-gleam) to translate `import * from "./*.gleam`.
- with alias `@gleam` to gleam build directory, this help the dev team.

Vite config `vite.config.js`:

```js
import { resolve } from 'path'
import { defineConfig } from 'vite'

// plugins
import gleam from 'vite-plugin-gleam'
import tailwindcss from "@tailwindcss/vite"

export default defineConfig({
	plugins: [gleam(), tailwindcss()],
	resolve: {
		alias: {
			'@gleam': resolve(__dirname, "./build/dev/javascript")
		}
	}
})
```

My style file `main.css`:

```css
@import "@gleam/gbr_ui/priv/gbr/ui.css";

/* my custom styles here */
```

> Use alias `@gleam`, the dev team greatful 😊.

#### gleam.toml

Add this property:

```toml
[javascript]
typescript_declarations = true
```

### Font

Default is google `Outfit`:

```css
@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap");

@theme {
  --font-*: initial;
  --font-gbr-ui: Outfit, sans-serif;
  --default-font-family: --var(--font-gbr-ui);
}
```

#### Lustre [dev-tools](https://hexdocs.pm/lustre_dev_tools/toml-reference.html)

Gleam config `gleam.toml`:

```toml
[tools.lustre.html]
stylesheets = [
  { href = "https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap" }
]
```

### Breakpoints

```css
@theme {
  --breakpoint-*: initial;
  --breakpoint-2xsm: 375px;
  --breakpoint-xsm: 425px;
  --breakpoint-3xl: 2000px;
  --breakpoint-sm: 640px;
  --breakpoint-md: 768px;
  --breakpoint-lg: 1024px;
  --breakpoint-xl: 1280px;
  --breakpoint-2xl: 1536px;
}
```

### Texts

```css
@theme {
  --text-title-2xl: 72px;
  --text-title-2xl--line-height: 90px;
  --text-title-xl: 60px;
  --text-title-xl--line-height: 72px;
  --text-title-lg: 48px;
  --text-title-lg--line-height: 60px;
  --text-title-md: 36px;
  --text-title-md--line-height: 44px;
  --text-title-sm: 30px;
  --text-title-sm--line-height: 38px;
  --text-theme-xl: 20px;
  --text-theme-xl--line-height: 30px;
  --text-theme-sm: 14px;
  --text-theme-sm--line-height: 20px;
  --text-theme-xs: 12px;
  --text-theme-xs--line-height: 18px;
}
```

### Colors

```css
@theme {
  --color-current: currentColor;
  --color-transparent: transparent;
  --color-white: #ffffff;
  --color-black: #101828;

  --color-theme-pink-500: #ee46bc;
  --color-theme-purple-500: #7a5af8;

  --color-brand-25: #f2f7ff;
  --color-brand-50: #ecf3ff;
  --color-brand-100: #dde9ff;
  --color-brand-200: #c2d6ff;
  --color-brand-300: #9cb9ff;
  --color-brand-400: #7592ff;
  --color-brand-500: #465fff;
  --color-brand-600: #3641f5;
  --color-brand-700: #2a31d8;
  --color-brand-800: #252dae;
  --color-brand-900: #262e89;
  --color-brand-950: #161950;

  --color-blue-light-25: #f5fbff;
  --color-blue-light-50: #f0f9ff;
  --color-blue-light-100: #e0f2fe;
  --color-blue-light-200: #b9e6fe;
  --color-blue-light-300: #7cd4fd;
  --color-blue-light-400: #36bffa;
  --color-blue-light-500: #0ba5ec;
  --color-blue-light-600: #0086c9;
  --color-blue-light-700: #026aa2;
  --color-blue-light-800: #065986;
  --color-blue-light-900: #0b4a6f;
  --color-blue-light-950: #062c41;

  --color-gray-25: #fcfcfd;
  --color-gray-50: #f9fafb;
  --color-gray-100: #f2f4f7;
  --color-gray-200: #e4e7ec;
  --color-gray-300: #d0d5dd;
  --color-gray-400: #98a2b3;
  --color-gray-500: #667085;
  --color-gray-600: #475467;
  --color-gray-700: #344054;
  --color-gray-800: #1d2939;
  --color-gray-900: #101828;
  --color-gray-950: #0c111d;
  --color-gray-dark: #1a2231;

  --color-orange-25: #fffaf5;
  --color-orange-50: #fff6ed;
  --color-orange-100: #ffead5;
  --color-orange-200: #fddcab;
  --color-orange-300: #feb273;
  --color-orange-400: #fd853a;
  --color-orange-500: #fb6514;
  --color-orange-600: #ec4a0a;
  --color-orange-700: #c4320a;
  --color-orange-800: #9c2a10;
  --color-orange-900: #7e2410;
  --color-orange-950: #511c10;

  --color-success-25: #f6fef9;
  --color-success-50: #ecfdf3;
  --color-success-100: #d1fadf;
  --color-success-200: #a6f4c5;
  --color-success-300: #6ce9a6;
  --color-success-400: #32d583;
  --color-success-500: #12b76a;
  --color-success-600: #039855;
  --color-success-700: #027a48;
  --color-success-800: #05603a;
  --color-success-900: #054f31;
  --color-success-950: #053321;

  --color-error-25: #fffbfa;
  --color-error-50: #fef3f2;
  --color-error-100: #fee4e2;
  --color-error-200: #fecdca;
  --color-error-300: #fda29b;
  --color-error-400: #f97066;
  --color-error-500: #f04438;
  --color-error-600: #d92d20;
  --color-error-700: #b42318;
  --color-error-800: #912018;
  --color-error-900: #7a271a;
  --color-error-950: #55160c;

  --color-warning-25: #fffcf5;
  --color-warning-50: #fffaeb;
  --color-warning-100: #fef0c7;
  --color-warning-200: #fedf89;
  --color-warning-300: #fec84b;
  --color-warning-400: #fdb022;
  --color-warning-500: #f79009;
  --color-warning-600: #dc6803;
  --color-warning-700: #b54708;
  --color-warning-800: #93370d;
  --color-warning-900: #7a2e0e;
  --color-warning-950: #4e1d09;
}
```

### Shadows

```css
@theme {
  --shadow-theme-md: 0px 4px 8px -2px rgba(16, 24, 40, 0.1),
    0px 2px 4px -2px rgba(16, 24, 40, 0.06);
  --shadow-theme-lg: 0px 12px 16px -4px rgba(16, 24, 40, 0.08),
    0px 4px 6px -2px rgba(16, 24, 40, 0.03);
  --shadow-theme-sm: 0px 1px 3px 0px rgba(16, 24, 40, 0.1),
    0px 1px 2px 0px rgba(16, 24, 40, 0.06);
  --shadow-theme-xs: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
  --shadow-theme-xl: 0px 20px 24px -4px rgba(16, 24, 40, 0.08),
    0px 8px 8px -4px rgba(16, 24, 40, 0.03);
  --shadow-datepicker: -5px 0 0 #262d3c, 5px 0 0 #262d3c;
  --shadow-focus-ring: 0px 0px 0px 4px rgba(70, 95, 255, 0.12);
  --shadow-slider-navigation: 0px 1px 2px 0px rgba(16, 24, 40, 0.1),
    0px 1px 3px 0px rgba(16, 24, 40, 0.1);
  --shadow-tooltip: 0px 4px 6px -2px rgba(16, 24, 40, 0.05),
    -8px 0px 20px 8px rgba(16, 24, 40, 0.05);

  --drop-shadow-4xl: 0 35px 35px rgba(0, 0, 0, 0.25),
    0 45px 65px rgba(0, 0, 0, 0.15);
}
```

### Z indexes

```css
@theme {
  --z-index-1: 1;
  --z-index-9: 9;
  --z-index-99: 99;
  --z-index-999: 999;
  --z-index-9999: 9999;
  --z-index-99999: 99999;
  --z-index-999999: 999999;
}
```

### Menus

```css
@utility menu-item {
  @apply relative flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-theme-sm;
}

@utility menu-item-active {
  @apply bg-brand-50 text-brand-500 dark:bg-brand-500/[0.12] dark:text-brand-400;
}

@utility menu-item-inactive {
  @apply text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-gray-300;
}

@utility menu-item-icon-active {
  @apply fill-brand-500 dark:fill-brand-400;
}

@utility menu-item-icon-inactive {
  @apply fill-gray-500 group-hover:fill-gray-700 dark:fill-gray-400 dark:group-hover:fill-gray-300;
}

@utility menu-item-arrow {
  @apply absolute top-1/2 right-2.5 -translate-y-1/2;
}

@utility menu-item-arrow-active {
  @apply rotate-180 stroke-brand-500 dark:stroke-brand-400;
}

@utility menu-item-arrow-inactive {
  @apply stroke-gray-500 group-hover:stroke-gray-700 dark:stroke-gray-400 dark:group-hover:stroke-gray-300;
}

@utility menu-dropdown-item {
  @apply text-theme-sm relative flex items-center gap-3 rounded-lg px-3 py-2.5 font-medium;
}

@utility menu-dropdown-item-active {
  @apply bg-brand-50 text-brand-500 dark:bg-brand-500/[0.12] dark:text-brand-400;
}

@utility menu-dropdown-item-inactive {
  @apply text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5;
}

@utility menu-dropdown-badge {
  @apply text-brand-500 dark:text-brand-400 block rounded-full px-2.5 py-0.5 text-xs font-medium uppercase;
}

@utility menu-dropdown-badge-active {
  @apply bg-brand-100 dark:bg-brand-500/20;
}

@utility menu-dropdown-badge-inactive {
  @apply bg-brand-50 group-hover:bg-brand-100 dark:bg-brand-500/15 dark:group-hover:bg-brand-500/20;
}
```

### Scrollbars

```css
@utility no-scrollbar {
  /* Chrome, Safari and Opera */
  &::-webkit-scrollbar {
    display: none;
  }

  -ms-overflow-style: none;
  /* IE and Edge */
  scrollbar-width: none;
  /* Firefox */
}

@utility custom-scrollbar {
  &::-webkit-scrollbar {
    @apply size-1.5;
  }

  &::-webkit-scrollbar-track {
    @apply rounded-full;
  }

  &::-webkit-scrollbar-thumb {
    @apply bg-gray-200 rounded-full;
  }
}
```

### Inputs

```css
@layer utilities {
  /* For Remove Date Icon */
  input[type="date"]::-webkit-inner-spin-button,
  input[type="time"]::-webkit-inner-spin-button,
  input[type="date"]::-webkit-calendar-picker-indicator,
  input[type="time"]::-webkit-calendar-picker-indicator {
    display: none;
    -webkit-appearance: none;
  }
}
```

### Sidebar ajusts

```css
.sidebar:hover {
  width: 290px;
}

.sidebar:hover .logo {
  display: block;
}

.sidebar:hover .logo-icon {
  display: none;
}

.sidebar:hover .sidebar-header {
  justify-content: space-between;
}

.sidebar:hover .menu-group-title {
  display: block;
}

.sidebar:hover .menu-group-icon {
  display: none;
}

.sidebar:hover .menu-item-text {
  display: inline;
}

.sidebar:hover .menu-item-arrow {
  display: block;
}

.sidebar:hover .menu-dropdown {
  display: flex;
}
```

## Development

```sh
gleam run   # Run the project
gleam test  # Run the tests
```

## 🌄 Roadmap

- [ ] Unit tests
- [ ] More docs
- [ ] UI notify
- [ ] UI login
  - [ ] basic
  - [ ] provider
- [x] UI box
- [x] UI header
- [x] UI sidebar
- [x] UI search
- [x] UI profile
- [x] UI layout
- [x] UI core
- [x] UI input
- [x] UI typo
- [x] UI alert
- [x] UI svg
- [x] UI form
- [x] UI button
- [x] UI select
- [x] UI checkbox
- [x] UI logotype
- [x] UI breadcrumb
- [x] UI separator
- [x] GH workflow
  - [x] test & build
  - [x] changelog & issue to doc
  - [x] ~~auto publish~~ manual publish
    - [x] `gleam publish`