Current section
Files
Jump to
Current section
Files
README.md
# glendix
Gleam FFI bindings for React 19 and Mendix Pluggable Widget API.
**JSX μμ΄, μμ GleamμΌλ‘ Mendix Pluggable Widgetμ μμ±νλ€.**
## What's new in v2.0
v2.0μ [redraw](https://github.com/ghivert/redraw) νλ‘μ νΈμ ν¨ν΄μ μ°Έκ³ νμ¬ React λ°μΈλ©μ λν κ°μ νλ€. redrawλ Gleamμ© νλ‘λμ
React λ°μΈλ© λΌμ΄λΈλ¬λ¦¬λ‘, νμ
μμ μ±κ³Ό λͺ¨λ κ΅¬μ‘°κ° μ μ€κ³λμ΄ μλ€. glendixλ Mendix Pluggable Widget νΉν λΌμ΄λΈλ¬λ¦¬μ΄λ―λ‘ redrawμ λ²μ© SPA ν¨ν΄(bootstrap/compose, jsx-runtime λ±)μ μ±ννμ§ μκ³ , μ€μ§μ μΌλ‘ μ μ©ν κ°μ μ μ§μ€νλ€.
### μ£Όμ λ³κ²½μ¬ν
- **FFI λͺ¨λ λΆλ¦¬**: `react_ffi.mjs` νλμ λͺ¨μ¬ μλ FFIλ₯Ό `hook_ffi.mjs`, `event_ffi.mjs`, `attribute_ffi.mjs`λ‘ λΆλ¦¬νμ¬ λͺ¨λλ³ λ¨μΌ μ±
μ λ¬μ±
- **Attribute 리μ€νΈ API**: κΈ°μ‘΄ `prop.gleam` νμ΄νλΌμΈ λΉλλ₯Ό `attribute.gleam` μ μΈμ 리μ€νΈ ν¨ν΄μΌλ‘ κ΅μ²΄ β `[attribute.class("x"), event.on_click(handler)]`
- **39κ° Hook**: `useLayoutEffect`, `useInsertionEffect`, `useImperativeHandle`, `useLazyState`, `useSyncExternalStore`, `useDebugValue`, `useOptimistic` (리λμ λ³ν ν¬ν¨), `useAsyncTransition`, `useFormStatus` λ° cleanup λ³ν
- **154+ μ΄λ²€νΈ νΈλ€λ¬**: μΊ‘μ² λ¨κ³, μ»΄ν¬μ§μ
/λ―Έλμ΄/UI/λ‘λ/μλ¬/νΈλμ§μ
μ΄λ²€νΈ + 82+ μ κ·Όμ + `persist`/`is_persistent` μ νΈλ¦¬ν°
- **108+ HTML μμ±**: `dangerously_set_inner_html`, `popover`, `fetch_priority`, `enter_key_hint`, λ§μ΄ν¬λ‘λ°μ΄ν°, Shadow DOM λ±
- **85+ HTML νκ·Έ**: `fieldset`, `details`, `dialog`, `video`, `ruby`, `kbd`, `search`, `hgroup`, `meta`, `script`, `object` λ±
- **58 SVG μμ**: 16κ° νν° ν리미ν°λΈ ν¬ν¨ (`fe_convolve_matrix`, `fe_diffuse_lighting` λ±)
- **97+ SVG μμ±**: ν
μ€νΈ λ λλ§, λ§μ»€, λ§μ€ν¬/ν΄λ¦¬ν λ¨μ, νν° μμ± λ±
- **κ³ κΈ μ»΄ν¬λνΈ**: `StrictMode`, `Suspense`, `Profiler`, `portal`, `forwardRef`, `memo_`, `startTransition`, `flushSync`
## Installation
```toml
# gleam.toml
[dependencies]
glendix = { path = "../glendix" }
```
> Hex ν¨ν€μ§ λ°°ν¬ μ κΉμ§λ λ‘컬 κ²½λ‘λ‘ μ°Έμ‘°ν©λλ€.
### Peer Dependencies
μμ ― νλ‘μ νΈμ `package.json`μ λ€μμ΄ νμν©λλ€:
```json
{
"dependencies": {
"react": "^19.0.0",
"big.js": "^6.0.0"
}
}
```
## Quick Start
```gleam
import glendix/mendix
import glendix/react.{type JsProps, type ReactElement}
import glendix/react/attribute
import glendix/react/html
pub fn widget(props: JsProps) -> ReactElement {
let name = mendix.get_string_prop(props, "sampleText")
html.div([attribute.class("my-widget")], [
react.text("Hello " <> name),
])
}
```
`fn(JsProps) -> ReactElement` β μ΄κ²μ΄ Mendix Pluggable Widgetμ μ λΆμ
λλ€.
## Modules
### React
| Module | Description |
|---|---|
| `glendix/react` | ν΅μ¬ νμ
(`ReactElement`, `JsProps`, `Component`, `Promise`) + `element`, `fragment`, `keyed`, `text`, `none`, `when`, `when_some`, Context API, `define_component`, `memo` (Gleam ꡬ쑰 λλ±μ± λΉκ΅), `flush_sync` |
| `glendix/react/attribute` | Attribute νμ
+ 108+ HTML μμ± ν¨μ β `class`, `id`, `style`, `popover`, `fetch_priority`, `enter_key_hint`, λ§μ΄ν¬λ‘λ°μ΄ν°, Shadow DOM λ± |
| `glendix/react/hook` | React Hooks 40κ° β `use_state`, `use_effect`, `use_layout_effect`, `use_insertion_effect`, `use_memo`, `use_callback`, `use_ref`, `use_reducer`, `use_context`, `use_id`, `use_transition`, `use_async_transition`, `use_deferred_value`, `use_optimistic`/`use_optimistic_`, `use_imperative_handle`, `use_lazy_state`, `use_sync_external_store`, `use_debug_value`, `use_promise` (React.use), `use_form_status` |
| `glendix/react/ref` | Ref μ κ·Όμ β `current`, `assign` (hook λͺ¨λμμ λΆλ¦¬) |
| `glendix/react/event` | 16κ° μ΄λ²€νΈ νμ
+ 154+ νΈλ€λ¬ (μΊ‘μ² λ¨κ³, νΈλμ§μ
μ΄λ²€νΈ ν¬ν¨) + 82+ μ κ·Όμ |
| `glendix/react/html` | 85+ HTML νκ·Έ νΈμ ν¨μ β `div`, `span`, `input`, `details`, `dialog`, `video`, `ruby`, `kbd`, `search`, `meta`, `script`, `object` λ± (μμ Gleam, FFI μμ) |
| `glendix/react/svg` | 58 SVG μμ νΈμ ν¨μ β `svg`, `path`, `circle`, 16 νν° ν리미ν°λΈ, `discard` λ± (μμ Gleam, FFI μμ) |
| `glendix/react/svg_attribute` | 97+ SVG μ μ© μμ± ν¨μ β `view_box`, `fill`, `stroke`, λ§μ»€, νν° μμ± λ± (μμ Gleam, FFI μμ) |
| `glendix/binding` | μΈλΆ React μ»΄ν¬λνΈ λ°μΈλ© β `.mjs` μμ΄ `bindings.json`λ§μΌλ‘ μ¬μ© |
| `glendix/widget` | .mpk μμ ― μ»΄ν¬λνΈ λ°μΈλ© β `widgets/` λλ ν 리μ Mendix μμ ―μ React μ»΄ν¬λνΈλ‘ μ¬μ© |
| `glendix/classic` | Classic (Dojo) μμ ― React λνΌ β `classic.render(widget_id, properties)` ν¨ν΄ |
| `glendix/marketplace` | Mendix Marketplace μμ ― μΈν°λν°λΈ κ²μΒ·λ€μ΄λ‘λ β `gleam run -m glendix/marketplace` |
### Mendix
| Module | Description |
|---|---|
| `glendix/mendix` | ν΅μ¬ νμ
(`ValueStatus`, `ObjectItem`) + JsProps μ κ·Όμ (`get_prop`, `get_string_prop`) |
| `glendix/mendix/editable_value` | νΈμ§ κ°λ₯ν κ° β `value`, `set_value`, `set_text_value`, `display_value` |
| `glendix/mendix/action` | μ‘μ
μ€ν β `can_execute`, `execute`, `execute_if_can` |
| `glendix/mendix/dynamic_value` | λμ μ½κΈ° μ μ© κ° (ννμ μμ±) |
| `glendix/mendix/list_value` | 리μ€νΈ λ°μ΄ν° β `items`, `set_filter`, `set_sort_order`, `reload` |
| `glendix/mendix/list_attribute` | 리μ€νΈ μμ΄ν
λ³ μ κ·Ό β `ListAttributeValue`, `ListActionValue`, `ListWidgetValue` |
| `glendix/mendix/selection` | λ¨μΌ/λ€μ€ μ ν |
| `glendix/mendix/reference` | λ¨μΌ μ°κ΄ κ΄κ³ (ReferenceValue) |
| `glendix/mendix/reference_set` | λ€μ€ μ°κ΄ κ΄κ³ (ReferenceSetValue) |
| `glendix/mendix/date` | JS Date opaque λνΌ (μ: Gleam 1-based β JS 0-based μλ λ³ν) |
| `glendix/mendix/big` | Big.js κ³ μ λ° μμ§μ λνΌ (`compare` β `gleam/order.Order`) |
| `glendix/mendix/file` | `FileValue`, `WebImage` |
| `glendix/mendix/icon` | `WebIcon` β Glyph, Image, IconFont |
| `glendix/mendix/formatter` | `ValueFormatter` β `format`, `parse` |
| `glendix/mendix/filter` | FilterCondition λΉλ β `and_`, `or_`, `equals`, `contains`, `attribute`, `literal` |
| `glendix/editor_config` | Editor Configuration ν¬νΌ β μ‘°κ±΄λΆ μμ± μ¨κΈ°κΈ°, ν λ³ν, μμ± μμ λ³κ²½ (Jint νΈν) |
## Examples
### Attribute 리μ€νΈ
```gleam
import glendix/react/attribute
import glendix/react/event
import glendix/react/html
html.button(
[
attribute.class("btn btn-primary"),
attribute.type_("submit"),
attribute.disabled(False),
event.on_click(fn(_event) { Nil }),
],
[react.text("Submit")],
)
```
μ‘°κ±΄λΆ μμ±μ `attribute.none()`μΌλ‘ μ²λ¦¬νλ€:
```gleam
html.input([
attribute.class("input"),
case is_error {
True -> attribute.class("input-error")
False -> attribute.none()
},
])
```
### useState + useEffect
```gleam
import gleam/int
import glendix/react
import glendix/react/attribute
import glendix/react/event
import glendix/react/hook
import glendix/react/html
pub fn counter(_props) -> react.ReactElement {
let #(count, set_count) = hook.use_state(0)
hook.use_effect_once(fn() {
// λ§μ΄νΈ μ ν λ² μ€ν
Nil
})
html.div_([
html.button(
[event.on_click(fn(_) { set_count(count + 1) })],
[react.text("Count: " <> int.to_string(count))],
),
])
}
```
### useLayoutEffect (λ μ΄μμ μΈ‘μ )
```gleam
import glendix/react/hook
// DOM λ³κ²½ ν λΈλΌμ°μ νμΈνΈ μ λκΈ° μ€ν
let ref = hook.use_ref(0.0)
hook.use_layout_effect_cleanup(
fn() {
// λ μ΄μμ μΈ‘μ λ‘μ§
fn() { Nil } // cleanup
},
[some_dep],
)
```
### Mendix EditableValue μ½κΈ°/μ°κΈ°
```gleam
import gleam/option.{None, Some}
import glendix/mendix
import glendix/mendix/editable_value as ev
pub fn render_input(props: react.JsProps) -> react.ReactElement {
case mendix.get_prop(props, "myAttribute") {
Some(attr) -> {
let display = ev.display_value(attr)
let editable = ev.is_editable(attr)
// ...
}
None -> react.none()
}
}
```
### μ‘°κ±΄λΆ λ λλ§
```gleam
import glendix/react
import glendix/react/html
// Bool κΈ°λ°
react.when(is_visible, fn() {
html.div_([react.text("Visible!")])
})
// Option κΈ°λ°
react.when_some(maybe_user, fn(user) {
html.span_([react.text(user.name)])
})
```
### μΈλΆ React μ»΄ν¬λνΈ μ¬μ© (λ°μΈλ©)
`.mjs` νμΌ μμ± μμ΄ μΈλΆ React λΌμ΄λΈλ¬λ¦¬λ₯Ό μ¬μ©ν©λλ€.
**1. `bindings.json` μμ±:**
```json
{
"recharts": {
"components": ["PieChart", "Pie", "Cell", "Tooltip", "Legend"]
}
}
```
**2. ν¨ν€μ§ μ€μΉ** β `bindings.json`μ λ±λ‘ν ν¨ν€μ§λ `node_modules`μ μ€μΉλμ΄ μμ΄μΌ ν©λλ€:
```bash
npm install recharts
```
**3. `gleam run -m glendix/install` μ€ν** (λ°μΈλ© μλ μμ±)
**4. μμ Gleam λνΌ λͺ¨λ μμ±** (html.gleamκ³Ό λμΌν νΈμΆ ν¨ν΄):
```gleam
// src/chart/recharts.gleam
import glendix/binding
import glendix/react.{type ReactElement}
import glendix/react/attribute.{type Attribute}
fn m() { binding.module("recharts") }
pub fn pie_chart(attrs: List(Attribute), children: List(ReactElement)) -> ReactElement {
react.component_el(binding.resolve(m(), "PieChart"), attrs, children)
}
pub fn pie(attrs: List(Attribute), children: List(ReactElement)) -> ReactElement {
react.component_el(binding.resolve(m(), "Pie"), attrs, children)
}
```
**5. μμ ―μμ μ¬μ©:**
```gleam
import chart/recharts
import glendix/react/attribute
pub fn my_chart(data) -> react.ReactElement {
recharts.pie_chart(
[attribute.attribute("width", 400), attribute.attribute("height", 300)],
[
recharts.pie(
[attribute.attribute("data", data), attribute.attribute("dataKey", "value")],
[],
),
],
)
}
```
### .mpk μμ ― μ»΄ν¬λνΈ μ¬μ©
`widgets/` λλ ν 리μ `.mpk` νμΌμ React μ»΄ν¬λνΈλ‘ importνμ¬ μ¬μ©ν©λλ€.
**1. `widgets/` λλ ν 리μ `.mpk` νμΌ λ°°μΉ**
**2. `gleam run -m glendix/install` μ€ν** (μμ ― λ°μΈλ© μλ μμ±)
install μ λ κ°μ§κ° μλ μνλ©λλ€:
- `.mpk`μμ `.mjs`/`.css` μΆμΆ + `widget_ffi.mjs` μμ±
- `.mpk` XMLμ `<property>` μ μλ₯Ό νμ±νμ¬ `src/widgets/`μ λ°μΈλ© `.gleam` νμΌ μλ μμ± (μ΄λ―Έ μ‘΄μ¬νλ©΄ 건λλ)
**3. μλ μμ±λ `src/widgets/*.gleam` νμΌ νμΈ:**
```gleam
// src/widgets/switch.gleam (μλ μμ±)
import glendix/mendix
import glendix/react.{type JsProps, type ReactElement}
import glendix/react/attribute
import glendix/widget
/// Switch μμ ― λ λλ§ - propsμμ μμ±μ μ½μ΄ μμ ―μ μ λ¬
pub fn render(props: JsProps) -> ReactElement {
let boolean_attribute = mendix.get_prop_required(props, "booleanAttribute")
let action = mendix.get_prop_required(props, "action")
let comp = widget.component("Switch")
react.component_el(
comp,
[
attribute.attribute("booleanAttribute", boolean_attribute),
attribute.attribute("action", action),
],
[],
)
}
```
required/optional μμ±μ΄ μλ ꡬλΆλλ©°, νμμ λ°λΌ μμ±λ νμΌμ μμ λ‘κ² μμ ν μ μμ΅λλ€.
**4. μμ ―μμ μ¬μ©:**
```gleam
import widgets/switch
// μ»΄ν¬λνΈ λ΄λΆμμ
switch.render(props)
```
### Marketplace μμ ― λ€μ΄λ‘λ
Mendix Marketplaceμμ μμ ―(.mpk)μ μΈν°λν°λΈνκ² κ²μΒ·λ€μ΄λ‘λν©λλ€. λ€μ΄λ‘λ μλ£ ν `src/widgets/`μ λ°μΈλ© `.gleam` νμΌμ΄ μλ μμ±λ©λλ€.
**1. `.env` νμΌμ Mendix PAT μ€μ :**
```
MENDIX_PAT=your_personal_access_token
```
> PATλ [Mendix Developer Settings](https://user-settings.mendix.com/link/developersettings)μμ **Personal Access Tokens** μΉμ
μ **New Token**μ ν΄λ¦νμ¬ λ°κΈν©λλ€. νμν scope: `mx:marketplace-content:read`
**2. μ€ν:**
```bash
gleam run -m glendix/marketplace
```
**3. μΈν°λν°λΈ TUI μ¬μ©:**
```
ββ νμ΄μ§ 1/5+ ββ
[0] Star Rating (54611) v3.2.2 β Mendix
[1] Switch (50324) v4.0.0 β Mendix
...
λ²νΈ: λ€μ΄λ‘λ | κ²μμ΄: μ΄λ¦ κ²μ | n: λ€μ | p: μ΄μ | r: μ΄κΈ°ν | q: μ’
λ£
> 0 β λ²νΈ μ
λ ₯μΌλ‘ λ€μ΄λ‘λ
> star β κ²μμ΄λ‘ νν°λ§
> 0,1,3 β μΌνλ‘ μ¬λ¬ μμ ― λμ λ€μ΄λ‘λ
```
μμ ― μ ν μ λ²μ λͺ©λ‘μ΄ νμλλ©°, Pluggable/Classic νμ
μ΄ μλ ꡬλΆλ©λλ€. λ€μ΄λ‘λλ `.mpk`λ `widgets/` λλ ν 리μ μ μ₯λκ³ , `cmd.generate_widget_bindings()`κ° μλ νΈμΆλμ΄ `src/widgets/`μ λ°μΈλ© μ½λκ° μμ±λ©λλ€.
> λ²μ μ 보 μ‘°ν μ Playwright(chromium)λ₯Ό μ¬μ©ν©λλ€. 첫 λ€μ΄λ‘λ μ λΈλΌμ°μ λ‘κ·ΈμΈμ΄ νμνλ©°, μΈμ
μ `.marketplace-cache/session.json`μ μ μ₯λ©λλ€.
## Build Scripts
glendixμ λ΄μ₯λ λΉλ μ€ν¬λ¦½νΈλ‘, μμ ― νλ‘μ νΈμμ λ³λ μ€ν¬λ¦½νΈ νμΌ μμ΄ `gleam run -m`μΌλ‘ μ€ννλ€.
| λͺ
λ Ήμ΄ | μ€λͺ
|
|--------|------|
| `gleam run -m glendix/install` | μμ‘΄μ± μ€μΉ + λ°μΈλ© μμ± + μμ ― λ°μΈλ© μμ± + μμ ― `.gleam` νμΌ μμ± (PM μλ κ°μ§) |
| `gleam run -m glendix/marketplace` | Mendix Marketplace μμ ― κ²μΒ·λ€μ΄λ‘λ (μΈν°λν°λΈ TUI) |
| `gleam run -m glendix/build` | νλ‘λμ
λΉλ (.mpk μμ±) |
| `gleam run -m glendix/dev` | κ°λ° μλ² (HMR, port 3000) |
| `gleam run -m glendix/start` | Mendix ν
μ€νΈ νλ‘μ νΈ μ°λ |
| `gleam run -m glendix/lint` | ESLint μ€ν |
| `gleam run -m glendix/lint_fix` | ESLint μλ μμ |
| `gleam run -m glendix/release` | λ¦΄λ¦¬μ¦ λΉλ |
ν¨ν€μ§ λ§€λμ λ lock νμΌ κΈ°λ°μΌλ‘ μλ κ°μ§λλ€:
- `pnpm-lock.yaml` β pnpm
- `bun.lockb` / `bun.lock` β bun
- κΈ°λ³Έκ° β npm
## Architecture
```
glendix/
react.gleam β ν΅μ¬ νμ
+ createElement + Context + keyed + μ»΄ν¬λνΈ μ μ + flushSync
react_ffi.mjs β μμ μμ±, Fragment, Context, κ³ κΈ μ»΄ν¬λνΈ μ΄λν°, Gleam ꡬ쑰 λλ±μ± memo
react/
attribute.gleam β Attribute νμ
+ 108+ HTML μμ± ν¨μ
attribute_ffi.mjs β Attribute β React props λ³ν
hook.gleam β React Hooks (40κ°, use_promise, use_form_status ν¬ν¨)
hook_ffi.mjs β Hooks FFI μ΄λν°
ref.gleam β Ref μ κ·Όμ (current, assign)
event.gleam β 16 μ΄λ²€νΈ νμ
+ 154+ νΈλ€λ¬ + 82+ μ κ·Όμ
event_ffi.mjs β μ΄λ²€νΈ μ κ·Όμ FFI μ΄λν°
html.gleam β 85+ HTML νκ·Έ (μμ Gleam)
svg.gleam β 58 SVG μμ (μμ Gleam)
svg_attribute.gleam β 97+ SVG μ μ© μμ± (μμ Gleam)
mendix.gleam β Mendix ν΅μ¬ νμ
+ Props μ κ·Όμ
mendix_ffi.mjs β Mendix λ°νμ νμ
μ κ·Ό μ΄λν°
mendix/
editable_value.gleam β EditableValue
action.gleam β ActionValue
dynamic_value.gleam β DynamicValue
list_value.gleam β ListValue + Sort + Filter
list_attribute.gleam β List-linked νμ
selection.gleam β Selection
reference.gleam β ReferenceValue (λ¨μΌ μ°Έμ‘°)
reference_set.gleam β ReferenceSetValue (λ€μ€ μ°Έμ‘°)
date.gleam β JS Date λνΌ
big.gleam β Big.js λνΌ
file.gleam β File / Image
icon.gleam β Icon
formatter.gleam β ValueFormatter
filter.gleam β FilterCondition λΉλ
editor_config.gleam β Editor Configuration ν¬νΌ (Jint νΈν, List λ―Έμ¬μ©)
editor_config_ffi.mjs β @mendix/pluggable-widgets-tools λν
binding.gleam β μΈλΆ React μ»΄ν¬λνΈ λ°μΈλ© API
binding_ffi.mjs β λ°μΈλ© FFI (install μ μλ κ΅μ²΄)
widget.gleam β .mpk μμ ― μ»΄ν¬λνΈ λ°μΈλ© API
widget_ffi.mjs β μμ ― λ°μΈλ© FFI (install μ μλ κ΅μ²΄)
classic.gleam β Classic (Dojo) μμ ― React λνΌ
classic_ffi.mjs β Classic μμ ― λ°μΈλ© FFI (install μ μλ κ΅μ²΄)
marketplace.gleam β Marketplace μμ ― κ²μΒ·λ€μ΄λ‘λ
marketplace_ffi.mjs β Content API + Playwright + S3 λ€μ΄λ‘λ FFI μ΄λν°
cmd.gleam β μ
Έ λͺ
λ Ήμ΄ μ€ν + PM κ°μ§ + λ°μΈλ©/μμ ― λ°μΈλ© μμ±
cmd_ffi.mjs β Node.js child_process + fs + ZIP νμ± FFI + λ°μΈλ©/μμ ― λ°μΈλ© μμ± + μμ ― .gleam νμΌ μμ±
build.gleam β λΉλ μ€ν¬λ¦½νΈ
dev.gleam β κ°λ° μλ² μ€ν¬λ¦½νΈ
start.gleam β Mendix μ°λ μ€ν¬λ¦½νΈ
install.gleam β μμ‘΄μ± μ€μΉ + λ°μΈλ©/μμ ― λ°μΈλ© μμ± μ€ν¬λ¦½νΈ
release.gleam β λ¦΄λ¦¬μ¦ λΉλ μ€ν¬λ¦½νΈ
lint.gleam β ESLint μ€ν¬λ¦½νΈ
lint_fix.gleam β ESLint μλ μμ μ€ν¬λ¦½νΈ
```
## Design Principles
- **FFIλ μμ μ΄λν°μΌ λΏμ΄λ€.** `.mjs` νμΌμ JS λ°νμ μ κ·Όλ§ λ΄λΉνκ³ , λΉμ¦λμ€ λ‘μ§μ μ λΆ GleamμΌλ‘ μμ±νλ€. λͺ¨λλ³ λ¨μΌ μ±
μ β `react_ffi.mjs`(μμ μμ±), `hook_ffi.mjs`(ν
), `event_ffi.mjs`(μ΄λ²€νΈ μ κ·Όμ).
- **Opaque typeμΌλ‘ νμ
μμ μ± λ³΄μ₯.** `ReactElement`, `JsProps`, `EditableValue` λ± JS κ°μ Gleamμ opaque typeμΌλ‘ κ°μΈ μλͺ»λ μ κ·Όμ μ»΄νμΌ νμμ μ°¨λ¨νλ€.
- **`undefined` β `Option` μλ λ³ν.** FFI κ²½κ³μμ JS `undefined`/`null`μ Gleam `None`μΌλ‘, κ°μ΄ μμΌλ©΄ `Some(value)`μΌλ‘ λ³νλλ€.
- **Attribute 리μ€νΈ API.** HTML μμ±μ `[attribute.class("x"), event.on_click(handler)]` μ μΈμ 리μ€νΈ ν¨ν΄. `attribute.none()`μΌλ‘ μ‘°κ±΄λΆ μμ± μ²λ¦¬. μ¬λ¬ `attribute.class()` νΈμΆ μ μλ λ³ν©.
- **Gleam νν = JS λ°°μ΄.** `#(a, b)` = `[a, b]`μ΄λ―λ‘ `useState`μ λ°νκ°κ³Ό μ§μ νΈνλλ€.
## Acknowledgments
v2.0μ React λ°μΈλ© κ°μ μ [redraw](https://github.com/ghivert/redraw) νλ‘μ νΈμ μ€κ³ ν¨ν΄μ μ°Έκ³ νλ€. FFI λͺ¨λ λΆλ¦¬, Hook λ³ν ν¨ν΄, μ΄λ²€νΈ μμ€ν
ꡬ쑰 λ±μμ μκ°μ λ°μλ€.
## License
Apache-2.0