# mcbe-ts-ui

A TypeScript library for generating Minecraft Bedrock JSON UI files with a fluent builder API.

## Overview

Writing Minecraft Bedrock JSON UI files by hand is tedious and error-prone. This library provides:

- **Type-safe builders** - Fluent API with full TypeScript intellisense
- **Automatic compilation** - Write `.ts` files, get `.json` output
- **Clean organization** - Source files in `scripts/ui/`, generated files in `ui/__generated__/`
- **Automatic `_ui_defs.json`** - Updates the UI definitions file automatically

## Installation

```bash
npm install mcbe-ts-ui
```

## Quick Start

### 1. Create a UI Source File

Create a file in `scripts/ui/` (e.g., `scripts/ui/my_hud.ts`):

```typescript
import { defineMain, panel, label, image } from "mcbe-ts-ui";

// Define child elements as variables
const title = label("title")
  .text("Hello Minecraft!")
  .color([1, 1, 0])
  .shadow()
  .anchor("center");

const icon = image("icon").texture("textures/ui/my_icon").size(16, 16);

// Export the main element
export default defineMain(
  "my_hud",
  panel("main").fullSize().anchor("center").controls(title, icon)
);
```

### 2. Compile

```bash
npx mcbe-ts-ui
```

This will:

- Compile `scripts/ui/my_hud.ts` → `ui/__generated__/my_hud.json`
- Compile `scripts/ui/hud/health.ts` → `ui/__generated__/hud/health.json`
- Update `ui/_ui_defs.json` to include all generated files

The compiler recursively scans `scripts/ui/` and preserves your subdirectory structure in the output.

### 3. Generated Output

```json
{
  "namespace": "my_hud",
  "main": {
    "type": "panel",
    "size": ["100%", "100%"],
    "anchor_from": "center",
    "anchor_to": "center",
    "controls": [
      {
        "title": {
          "type": "label",
          "text": "Hello Minecraft!",
          "color": [1, 1, 0],
          "shadow": true,
          "anchor_from": "center",
          "anchor_to": "center"
        }
      },
      {
        "icon": {
          "type": "image",
          "texture": "textures/ui/my_icon",
          "size": [16, 16]
        }
      }
    ]
  }
}
```

## API Reference

### Defining Namespaces

#### `defineMain()` - Simple (Recommended)

Most namespaces only need a `main` element. Use `defineMain()` for the cleanest syntax:

```typescript
import { defineMain, panel, label } from "mcbe-ts-ui";

const title = label("title").text("Hello").color([1, 1, 0]);

export default defineMain(
  "my_namespace",
  panel("main").fullSize().controls(title)
);
```

#### `defineUI()` - With Callback

Use when you need multiple top-level elements or complex logic:

```typescript
import { defineUI, panel, label, ref } from "mcbe-ts-ui";

export default defineUI("my_namespace", (ns) => {
  ns.add(panel("element_a").size(100, 100));
  ns.add(panel("element_b").size(100, 100));
  ns.add(
    panel("main")
      .fullSize()
      .controls(
        ref("a@my_namespace.element_a"),
        ref("b@my_namespace.element_b")
      )
  );
});
```

### Adding Children with `.controls()`

The `.controls()` method accepts:

- **ElementBuilder instances** (recommended) - automatically embedded
- **Control references** - for external elements
- **Inline objects** - for overrides

```typescript
// Using builders directly (cleanest)
const child1 = panel("child1").size(50, 50);
const child2 = label("child2").text("Hello");

panel("parent").controls(child1, child2);

// Using ref() for external elements
panel("parent").controls(
  ref("button@common.button"),
  ref("panel@other_namespace.panel")
);

// Inline with overrides
panel("parent").controls({ "btn@common.button": { size: [100, 30] } });
```

### Cross-File References with `nsRef()`

Share elements across files without duplication:

```typescript
// shared.ts
export const sharedButton = panel("action_btn")
  .extends("common.button")
  .size(100, 30);

// my_ui.ts
import { sharedButton } from "./shared";
import { defineMain, panel, nsRef } from "mcbe-ts-ui";

export default defineMain(
  "my_ui",
  panel("main")
    .fullSize()
    .controls(
      nsRef("shared", sharedButton), // Basic reference
      nsRef("shared", sharedButton, { offset: [0, 40] }) // With overrides
    )
);
// Generates: { "action_btn@shared.action_btn": { offset: [0, 40] } }
```

### Element Builders

#### Panel

```typescript
panel("name")
  .size(100, 50)           // Fixed size in pixels
  .size("100%", "50%")     // Percentage size
  .fullSize()              // 100% x 100%
  .fitContent()            // Size to children (100%c x 100%c)
  .offset(10, 20)          // Position offset
  .anchor("center")        // Both anchors
  .layer(5)                // Z-layer
  .visible(true)           // Visibility
  .enabled(true)           // Enabled state
  .clipsChildren()         // Clip overflow
  .alpha(0.5)              // Transparency
  .controls(...)           // Add children
```

#### Stack Panel

```typescript
stackPanel("name")
  .horizontal() // Horizontal layout
  .vertical(); // Vertical layout (default)
```

#### Label

```typescript
label("name")
  .text("Hello World") // Static text
  .text("#binding_name") // Bound text
  .color([1, 0.5, 0]) // RGB color
  .shadow() // Drop shadow
  .fontSize("large") // small/normal/large/extra_large
  .fontScaleFactor(1.5) // Scale multiplier
  .textAlignment("center") // left/center/right
  .localize() // Enable localization
  .enableProfanityFilter(false);
```

#### Image

```typescript
image("name")
  .texture("textures/ui/my_image")
  .uv(0, 0) // UV coordinates
  .uvSize(16, 16) // UV size
  .nineslice(4) // 9-slice border
  .tiled() // Tile texture
  .keepRatio() // Maintain aspect ratio
  .color([1, 0, 0]) // Tint color
  .grayscale(); // Grayscale filter
```

#### Button

```typescript
button("name")
  .defaultControl("default_state")
  .hoverControl("hover_state")
  .pressedControl("pressed_state");
```

#### Custom Renderer

```typescript
custom("name").renderer("hover_text_renderer").renderer("paper_doll_renderer");
```

### Bindings

```typescript
panel("name")
  // Global binding
  .globalBinding("#title_text", "#title_text", "none")

  // View binding (computed expression)
  .viewBinding("(not (#value = ''))", "#visible")

  // Shorthand visibility/enabled
  .visibilityBinding("(#is_visible)")
  .enabledBinding("(#is_enabled)")

  // Raw bindings array
  .bindings(
    { binding_type: "global", binding_name: "#my_binding" },
    {
      binding_type: "view",
      source_property_name: "...",
      target_property_name: "...",
    }
  );
```

### Variables

```typescript
panel("name")
  .variable("my_color", [1, 0, 0])
  .variable("offset", [10, 20])
  .rawProp("$custom_var", "value"); // For arbitrary properties
```

### Modifications (Vanilla UI)

```typescript
// Using defineUI for modifications
export default defineUI("hud", (ns) => {
  ns.addRaw("root_panel", {
    modifications: [
      {
        array_name: "controls",
        operation: "insert_back",
        value: [{ "my_element@my_namespace.main": {} }],
      },
    ],
  });
});
```

## CLI Options

```bash
npx mcbe-ts-ui [options]

Options:
  --source, -s <dir>    Source directory (default: "scripts/ui")
  --output, -o <dir>    Output directory (default: "ui/__generated__")
  --ui-defs, -d <file>  UI defs path (default: "ui/_ui_defs.json")
  --clean, -c           Clean output directory first
  --watch, -w           Watch mode (not yet implemented)
  --help, -h            Show help
```

## Project Structure

The compiler preserves your source directory structure. Files in subdirectories are output to matching subdirectories:

```
your-resource-pack/
├── ui/
│   ├── _ui_defs.json              # Auto-updated
│   ├── _global_variables.json     # Global vars (optional)
│   └── __generated__/             # Generated output (mirrors source structure)
│       ├── my_hud.json            # from scripts/ui/my_hud.ts
│       └── hud/                   # subdirectory preserved
│           ├── health.json        # from scripts/ui/hud/health.ts
│           └── inventory.json     # from scripts/ui/hud/inventory.ts
├── scripts/
│   └── ui/                        # Your UI source files
│       ├── my_hud.ts
│       └── hud/                   # Organize with subdirectories
│           ├── health.ts
│           └── inventory.ts
└── package.json
```

### Organizing with Subdirectories

Use subdirectories to organize related UI components:

```
scripts/ui/
├── main_menu.ts           → ui/__generated__/main_menu.json
├── hud/
│   ├── health.ts          → ui/__generated__/hud/health.json
│   ├── hotbar.ts          → ui/__generated__/hud/hotbar.json
│   └── minimap.ts         → ui/__generated__/hud/minimap.json
└── screens/
    ├── inventory.ts       → ui/__generated__/screens/inventory.json
    └── settings.ts        → ui/__generated__/screens/settings.json
```

### Shared/Utility Files

Not every `.ts` file needs to export a UI definition. The compiler handles this gracefully:

- **Files without a default export** are scanned but skipped from JSON generation
- Use `_` prefix for shared utility files as a naming convention (e.g., `_helpers.ts`)

```
scripts/ui/
├── _shared.ts             → (imported only - no JSON)
├── _constants.ts          → (imported only - no JSON)
├── utils.ts               → (imported only - no JSON)
├── my_hud.ts              → ui/__generated__/my_hud.json
└── hud/
    ├── _helpers.ts        → (imported only - no JSON)
    └── health.ts          → ui/__generated__/hud/health.json
```

Example shared file:

```typescript
// _helpers.ts - No default export, just shared components
import { panel, label } from "mcbe-ts-ui";

export const sharedHeader = panel("header").size("100%", 30);
export const sharedButton = panel("btn").extends("common.button").size(100, 30);
```

Import and use in your UI files:

```typescript
// my_hud.ts
import { defineMain, panel } from "mcbe-ts-ui";
import { sharedHeader } from "./_helpers";

export default defineMain(
  "my_hud",
  panel("main").fullSize().controls(sharedHeader)
);
```
