bedrock-ddui-forms 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.es.md ADDED
@@ -0,0 +1,363 @@
1
+ # bedrock-ddui-forms
2
+
3
+ > [🇬🇧 English](./README.md) · 🇪🇸 Español
4
+
5
+ Una capa declarativa para crear formularios **DDUI** (`CustomForm`) de `@minecraft/server-ui` a partir de objetos.
6
+
7
+ En lugar de encadenar llamadas sobre `CustomForm`, describes la vista como datos y un renderizador la traduce:
8
+
9
+ ```js
10
+ {
11
+ title: "Ajustes",
12
+ content: [
13
+ { type: "header", text: "General" },
14
+ { type: "toggle", label: "Activar función", toggled: new ObservableBoolean(true, { clientWritable: true }) },
15
+ { type: "close-button" }
16
+ ]
17
+ }
18
+ ```
19
+
20
+ > No afiliado a Mojang ni a Microsoft. "Minecraft" y "Bedrock" se usan solo para describir la plataforma a la que apunta esta librería.
21
+
22
+ ## Características
23
+
24
+ - **Vistas declarativas**: un formulario es un objeto con `title` y un arreglo `content`.
25
+ - **Tipado con la API oficial**: los modelos son typedefs de JSDoc que reutilizan los tipos de `@minecraft/server-ui` (`ButtonOptions`, `SliderOptions`, …), así que se mantienen sincronizados con el módulo.
26
+ - **Navegación sencilla**: un botón puede abrir otra vista con `to`, combinable con `onClick`.
27
+ - **Router**: registra vistas por nombre, pásales parámetros y vuelve atrás con `"back"`.
28
+ - **Fácil de extender**: añadir un tipo de elemento nuevo es una entrada en una tabla y una función.
29
+ - **Fallback seguro**: un `type` no soportado muestra un label visible y registra una advertencia, en vez de romper todo el formulario.
30
+
31
+ ## Requisitos
32
+
33
+ - `@minecraft/server-ui` **2.2.0** (estable)
34
+
35
+ ## Instalación
36
+
37
+ El motor de scripts de Minecraft **no** resuelve paquetes de npm (dentro del juego no existe `node_modules`). Solo los módulos `@minecraft/*` declarados en el `manifest.json` los aporta el juego, así que la librería tiene que terminar como archivos dentro de la carpeta `scripts/` de tu addon. Hay dos formas de hacerlo.
38
+
39
+ ### Opción 1: copiar los archivos
40
+
41
+ Copia la librería dentro de tu behavior pack e impórtala con rutas relativas:
42
+
43
+ ```
44
+ BP/
45
+ manifest.json
46
+ scripts/
47
+ main.js
48
+ libs/bedrock-ddui-forms/
49
+ index.js
50
+ core/
51
+ models/
52
+ views/
53
+ ```
54
+
55
+ ```js
56
+ import { customFormRender } from "../libs/bedrock-ddui-forms/index";
57
+ ```
58
+
59
+ No necesita herramientas, pero para actualizar tienes que copiar los archivos de nuevo.
60
+
61
+ ### Opción 2: usar un bundler (recomendada si instalas desde npm)
62
+
63
+ ```bash
64
+ npm install bedrock-ddui-forms
65
+ ```
66
+
67
+ Después empaqueta tu código y la librería en un solo archivo dentro de `scripts/`. Por ejemplo, con [esbuild](https://esbuild.github.io/):
68
+
69
+ ```bash
70
+ npx esbuild src/index.js --bundle --format=esm --outfile=scripts/libs/bedrock-ddui-forms/index.js --external:@minecraft/server --external:@minecraft/server-ui
71
+ ```
72
+
73
+ Los `--external` son importantes: los módulos `@minecraft/*` los aporta el juego y no deben incluirse en el bundle. Con este flujo, `npm update` basta para actualizar la librería.
74
+
75
+ ### Siempre necesario
76
+
77
+ - Declara `@minecraft/server-ui` en tu `manifest.json` (esta librería apunta a la versión `2.2.0`). Nunca se copia ni se empaqueta:
78
+
79
+ ```json
80
+ {
81
+ "module_name": "@minecraft/server-ui",
82
+ "version": "2.2.0"
83
+ }
84
+ ```
85
+
86
+ - La propia librería no debe usar módulos de Node.js (`node:fs`, etc.), porque no existen en el juego.
87
+ - Los imports internos de la librería están escritos sin extensión (`./propertiesFactory`), y los bundlers los resuelven. Si copias los archivos y tu entorno exige extensiones explícitas, añade `.js` a los imports relativos.
88
+
89
+ ## Inicio rápido
90
+
91
+ ```js
92
+ import { ObservableBoolean, ObservableString } from "@minecraft/server-ui";
93
+ import { customFormRender } from "../libs/bedrock-ddui-forms/index";
94
+
95
+ const createSettingsPage = (player) => ({
96
+ title: "Ajustes",
97
+ content: [
98
+ { type: "header", text: "General" },
99
+ { type: "spacer" },
100
+ {
101
+ type: "text-field",
102
+ label: "Nombre",
103
+ text: new ObservableString("Steve", { clientWritable: true })
104
+ },
105
+ {
106
+ type: "toggle",
107
+ label: "Activar función",
108
+ toggled: new ObservableBoolean(true, { clientWritable: true })
109
+ },
110
+ { type: "divider" },
111
+ { type: "close-button" }
112
+ ]
113
+ });
114
+
115
+ export const openSettings = (player) => customFormRender(player, createSettingsPage(player));
116
+ ```
117
+
118
+ > **Consejo:** define cada vista como una función (`createXPage(player)`) para que los `Observable` se creen de nuevo cada vez que se abre la vista y no se arrastre estado anterior.
119
+
120
+ ## Elementos soportados
121
+
122
+ | `type` | Propiedades principales | Método de `CustomForm` |
123
+ | --- | --- | --- |
124
+ | `button` | `label`, `onClick?`, `to?`, `options?` | `button()` |
125
+ | `close-button` | – | `closeButton()` |
126
+ | `divider` | `options?` | `divider()` |
127
+ | `dropdown` | `label`, `value` (`ObservableNumber`), `items`, `options?` | `dropdown()` |
128
+ | `header` | `text`, `options?` | `header()` |
129
+ | `image` | `src`, `pack`, `options?` (`onClick`, `tooltip`, `visible`, `width`) | `image()` |
130
+ | `label` | `text`, `options?` | `label()` |
131
+ | `slider` | `label`, `value` (`ObservableNumber`), `min`, `max`, `options?` (`step`, …) | `slider()` |
132
+ | `spacer` | `options?` | `spacer()` |
133
+ | `text-field` | `label`, `text` (`ObservableString`), `options?` | `textField()` |
134
+ | `toggle` | `label`, `toggled` (`ObservableBoolean`), `options?` | `toggle()` |
135
+
136
+ `options` siempre coincide con el tipo de opciones correspondiente de `@minecraft/server-ui` (por ejemplo `SliderOptions`). `image.options.width` es una fracción del ancho del formulario (`0.5` es la mitad).
137
+
138
+ ## Navegación
139
+
140
+ Un botón puede abrir otra vista con `to`. Se ejecuta **después** de `onClick`, cuando el formulario actual ya está cerrado, así que se pueden combinar:
141
+
142
+ ```js
143
+ {
144
+ type: "button",
145
+ label: "Guardar y volver",
146
+ onClick: function () { save(player); }, // se ejecuta primero
147
+ to: () => openMainMenu(player) // luego cierra este formulario y abre el siguiente
148
+ }
149
+ ```
150
+
151
+ - Si el botón solo navega, usa `to`.
152
+ - Si hace algo más, usa `onClick`, y añade `to` si además debe navegar.
153
+ - Dentro de `onClick` y de `image.options.onClick`, `this` es el `CustomForm`, así que `this.close()` funciona si necesitas cerrar sin abrir nada.
154
+
155
+ Para muchas vistas, parámetros y un botón de volver automático, mira el [Router](#router-vistas-con-nombre-y-atrás).
156
+
157
+ ## Router: vistas con nombre y "atrás"
158
+
159
+ Cuando tus menús crecen, conectar cada botón con el siguiente `openX(player)` se vuelve repetitivo. `createRouter` registra tus vistas por nombre, permite que los botones apunten a ellas y guarda un **historial por jugador** para que un botón pueda volver atrás.
160
+
161
+ ```js
162
+ import { ObservableBoolean } from "@minecraft/server-ui";
163
+ import { createRouter } from "bedrock-ddui-forms/index"; // o desde tu carpeta libs/
164
+
165
+ const router = createRouter({
166
+ menu: (player) => ({
167
+ title: "Menú",
168
+ content: [
169
+ { type: "button", label: "Mi perfil", to: { view: "profile", params: { name: player.name } } },
170
+ { type: "button", label: "Ajustes", to: "settings" },
171
+ { type: "close-button" }
172
+ ]
173
+ }),
174
+
175
+ profile: (player, params) => ({
176
+ title: `Perfil: ${params.name}`,
177
+ content: [
178
+ { type: "label", text: "Todavía no hay nada aquí." },
179
+ { type: "button", label: "Volver", to: "back" }
180
+ ]
181
+ }),
182
+
183
+ settings: (player) => ({
184
+ title: "Ajustes",
185
+ content: [
186
+ { type: "toggle", label: "Notificaciones", toggled: new ObservableBoolean(true, { clientWritable: true }) },
187
+ { type: "button", label: "Volver", to: "back" }
188
+ ]
189
+ })
190
+ });
191
+
192
+ // Iniciar un flujo
193
+ router.open(player, "menu");
194
+ ```
195
+
196
+ Cada vista es una función `(player, params) => formData`. Devuelve el mismo objeto que le darías a `customFormRender`, incluido `onExit`.
197
+
198
+ ### Valores que acepta `to`
199
+
200
+ | `to` | Resultado |
201
+ | --- | --- |
202
+ | `"settings"` | Abre la vista registrada con ese nombre. |
203
+ | `{ view: "profile", params: { ... } }` | Lo mismo, pasándole `params` a la vista. |
204
+ | `"back"` | Vuelve a la vista anterior del historial del jugador. Si no hay ninguna, el formulario simplemente se cierra. |
205
+ | una función | Funciona como el `to` normal (ver [Navegación](#navegación)), pero queda **fuera** del router: el historial del jugador se descarta. |
206
+
207
+ Si usas el nombre de una vista que no registraste, el router lanza `Unknown View: "nombre"`.
208
+
209
+ ### Parámetros
210
+
211
+ `params` es el segundo argumento de la función de la vista. Puedes pasarlos desde un botón (`to: { view, params }`) o al iniciar el flujo (`router.open(player, "profile", { name: "Alex" })`).
212
+
213
+ Qué pasas es decisión tuya. El router guarda los `params` de cada paso en el historial y los vuelve a usar al volver atrás, de modo que la vista se reconstruye con los mismos `params`. Por eso los datos simples (nombres, ids, números) son la opción más segura: una entidad o un objeto de jugador guardado ahí puede dejar de ser válido cuando el jugador regrese a esa vista.
214
+
215
+ Volver atrás **reconstruye** la vista llamando otra vez a su función, así que los valores que el jugador escribió (tus `Observable`) no se conservan: vuelven a sus valores iniciales.
216
+
217
+ ### Opciones del router
218
+
219
+ ```js
220
+ const router = createRouter(views, { onInvalid: "skip" });
221
+ ```
222
+
223
+ El segundo argumento acepta las mismas opciones que el tercer argumento de `customFormRender` (ver [Validación y mensajes de error](#validación-y-mensajes-de-error)) y se aplica a todas las vistas.
224
+
225
+ ### Qué devuelve `open`
226
+
227
+ `router.open(...)` devuelve una promesa que se resuelve con el motivo de cierre de **la vista que abrió**, no de todo el flujo. Cuando un botón navega, el servidor cierra esa vista, así que se resuelve con `ServerClosed`. El `onExit` de cada vista sigue la misma regla descrita en [Cerrar una vista](#cerrar-una-vista-onexit).
228
+
229
+ ### Vida del historial y limpieza
230
+
231
+ El router guarda en memoria el historial de cada jugador. Se borra cuando:
232
+
233
+ - el jugador cierra el formulario,
234
+ - un formulario se cierra desde código sin navegar (por ejemplo `this.close()` dentro de `onClick`),
235
+ - vuelves a llamar a `router.open(...)`: un flujo nuevo siempre empieza limpio,
236
+ - llamas a `router.clear(playerId)`.
237
+
238
+ Si un jugador sale del servidor a mitad de un flujo, su historial queda en memoria hasta que ocurra una de esas cosas. Es una entrada pequeña por jugador, pero puedes liberarla cuando se vaya:
239
+
240
+ ```js
241
+ import { world } from "@minecraft/server";
242
+
243
+ world.afterEvents.playerLeave.subscribe(({ playerId }) => router.clear(playerId));
244
+ ```
245
+
246
+ La librería no se suscribe a este evento por ti, a propósito:
247
+
248
+ - así la librería depende solo de `@minecraft/server-ui`,
249
+ - crear un router no engancha ningún evento global,
250
+ - tú decides cómo y cuándo limpiar según tu addon.
251
+
252
+ Suscríbete **una sola vez** (como arriba), no una vez por vista: una suscripción por vista se iría acumulando mientras los jugadores sigan conectados.
253
+
254
+ ## Leer valores
255
+
256
+ Los valores son los `Observable` que tú creas, así que guarda una referencia y léelos cuando los necesites:
257
+
258
+ ```js
259
+ const createPage = (player) => {
260
+ const name = new ObservableString("", { clientWritable: true });
261
+
262
+ return {
263
+ title: "Renombrar",
264
+ content: [
265
+ { type: "text-field", label: "Nuevo nombre", text: name },
266
+ {
267
+ type: "button",
268
+ label: "Guardar",
269
+ onClick: function () { rename(player, name.getData()); },
270
+ to: () => openMainMenu(player)
271
+ }
272
+ ]
273
+ };
274
+ };
275
+ ```
276
+
277
+ ## Cerrar una vista: `onExit`
278
+
279
+ `formData.onExit` se ejecuta cuando el formulario se cierra, por cualquier motivo excepto `UserBusy` (cuando el formulario no pudo mostrarse). `this` es el `CustomForm` y el primer argumento es el `DataDrivenScreenClosedReason`:
280
+
281
+ ```js
282
+ {
283
+ title: "Ajustes",
284
+ content: [ /* ... */ ],
285
+ onExit(reason) {
286
+ // reason: ClientClosed | ServerClosed
287
+ }
288
+ }
289
+ ```
290
+
291
+ Cuando un botón navega con `to`, el servidor cierra el formulario actual, así que su `onExit` también se ejecuta, con `ServerClosed`, **después** de que `to` ya abrió la siguiente vista. Revisa `reason` si solo quieres reaccionar cuando el jugador cierra el formulario (`ClientClosed`).
292
+
293
+ ## Validación y mensajes de error
294
+
295
+ Antes de abrir una vista, `customFormRender` revisa cada elemento y reporta **todos** los problemas que encuentra en un solo mensaje en la consola, con el lugar exacto de cada uno:
296
+
297
+ ```
298
+ [bedrock-ddui-forms] View "Stored Inventory": 3 errors, 2 warnings
299
+ ERROR content[0] slider "Protection duration" > label: is required. Expected string | ObservableString | ObservableUIRawMessage | UIRawMessage.
300
+ WARNING content[0] slider "Protection duration" > text: unknown property "text". Valid properties: label, value, min, max, options.
301
+ WARNING content[0] slider "Protection duration" > options.steps: unknown option "steps". Did you mean "step"?
302
+ ERROR content[1] textfield "Owner" > type: unknown type "textfield". Did you mean "text-field"?
303
+ ERROR content[2] toggle "Keep armor" > toggled: expected ObservableBoolean, received boolean true. Create it with new ObservableBoolean(<initial value>, { clientWritable: true }).
304
+ ```
305
+
306
+ Los mensajes están en inglés, igual que el resto de la salida de la librería.
307
+
308
+ - Los **errores** dejan el elemento inutilizable. Las **advertencias** señalan equivocaciones probables (propiedades u opciones desconocidas, un valor de dropdown que ningún item tiene, `min` mayor que `max`, un botón sin acción...) y no detienen nada.
309
+ - Los errores de escritura reciben una sugerencia ("Did you mean ...?"), y si pasas un valor simple donde se necesita un `Observable`, el mensaje explica cómo crearlo.
310
+ - También se detecta un error muy común: `to: openMenu(player)` llama a la función en vez de pasarla (`to: () => openMenu(player)`).
311
+ - Los errores lanzados dentro de `onClick`, `to`, `image.options.onClick` y `onExit` se registran con el elemento al que pertenecen, en vez de fallar en silencio.
312
+
313
+ Qué pasa con un elemento con errores lo decides tú, con el tercer argumento de `customFormRender`:
314
+
315
+ ```js
316
+ customFormRender(player, formData, { onInvalid: "label" }); // por defecto
317
+ ```
318
+
319
+ | `onInvalid` | Comportamiento |
320
+ | --- | --- |
321
+ | `"label"` | Muestra un marcador visible en su lugar (útil mientras desarrollas). |
322
+ | `"skip"` | Deja el elemento fuera (mejor en producción o con contenido que viene de fuera). |
323
+ | `"throw"` | No abre el formulario y lanza un `FormValidationError` (su propiedad `issues` lista todos los problemas). |
324
+
325
+ Si el problema está en la vista misma (falta `title`, `content` no es un arreglo, no hay `player`), el formulario no se puede construir: se registra el reporte y `customFormRender` devuelve `undefined`.
326
+
327
+ También puedes validar sin renderizar, por ejemplo en una prueba o con vistas que vienen de un servidor:
328
+
329
+ ```js
330
+ import { validateForm } from "bedrock-ddui-forms";
331
+
332
+ const report = validateForm(formData, player);
333
+ report.errors; // problemas que rompen un elemento
334
+ report.warnings; // equivocaciones probables
335
+ report.message; // el texto mostrado arriba
336
+ ```
337
+
338
+ ## Valor de retorno
339
+
340
+ `customFormRender(player, formData)` devuelve una promesa que se resuelve con el `DataDrivenScreenClosedReason` (`ClientClosed`, `ServerClosed` o `UserBusy`), o `undefined` si la vista no se pudo construir o hubo un error al mostrarla. Un resultado `UserBusy` además registra una advertencia.
341
+
342
+ ## Estructura del proyecto
343
+
344
+ ```
345
+ index.js punto de entrada público
346
+ core/
347
+ customFormRender.js renderiza una vista y la muestra al jugador
348
+ propertiesFactory.js asocia cada `type` con su llamada a CustomForm
349
+ validation.js revisa las vistas y construye los mensajes de error
350
+ createRouter.js vistas con nombre y historial de "atrás" por jugador
351
+ models/properties/
352
+ *.js typedefs de JSDoc de cada elemento
353
+ ```
354
+
355
+ ## Añadir un tipo de elemento nuevo
356
+
357
+ 1. Crea su typedef en `models/properties/` y añádelo a la unión `Property`.
358
+ 2. Escribe su función en `propertiesFactory.js`.
359
+ 3. Regístrala en la tabla `properties`.
360
+
361
+ ## Aviso legal
362
+
363
+ Este proyecto no está afiliado, respaldado ni patrocinado por Mojang Studios ni Microsoft.