Current section

Files

Jump to
glendix README.md
Raw

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